Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b98569f
Bind source entities to masters deterministically in the shared crate
lamemustafa Sep 10, 2026
407b782
Retain observed master names verbatim, and characterize the rules at …
lamemustafa Sep 10, 2026
5e3ed29
Merge remote-tracking branch 'origin/master' into claude/recursing-mi…
lamemustafa Sep 10, 2026
81bc763
Fix the review findings, and the candidate quality live data exposed
lamemustafa Sep 10, 2026
eea460e
Scope fallback assignment to the report it came from
lamemustafa Sep 10, 2026
5dba3fe
Address the four findings the fixes themselves generated
lamemustafa Sep 10, 2026
acef951
Keep period labels out of code identifiers, and drop a quadratic scan
lamemustafa Sep 10, 2026
aa9f4fb
Seed identifier-bearing ledgers, and fix what they immediately exposed
lamemustafa Sep 10, 2026
b87c8a3
Recognize period labels by shape, and bound the report at its source
lamemustafa Sep 10, 2026
03280f2
Make the comparison key an explicit contract point
lamemustafa Sep 10, 2026
eaf3dd7
Name the channel a parked identity must travel in
lamemustafa Sep 10, 2026
46a157d
Say in the producer's contract that an empty candidate list is three …
lamemustafa Sep 10, 2026
a6d95ad
Follow Tally's own rule for master sameness, and type the candidate l…
lamemustafa Sep 10, 2026
168e094
Close the six findings the contract change generated
lamemustafa Sep 10, 2026
c7cccbc
Stop a weaker rule reinstating a withheld family, and make Listed ser…
lamemustafa Sep 10, 2026
044a7d5
Test periods on the raw token, and let conflicting identifiers outran…
lamemustafa Sep 10, 2026
bc80222
Treat a non-ASCII name as a name, and a mask as identifying nothing
lamemustafa Sep 10, 2026
1406ada
Keep provenance per identifier rather than flattening it
lamemustafa Sep 11, 2026
3421266
Reject mask punctuation before extracting a numeric identifier
lamemustafa Sep 11, 2026
01d89f0
Stop tearing Indic names apart at their joins
lamemustafa Sep 11, 2026
b21f2fb
Merge remote-tracking branch 'origin/master' into claude/recursing-mi…
lamemustafa Sep 11, 2026
e97268f
Mask across tokens, and split a range on the dashes we already fold
lamemustafa Sep 11, 2026
1d37a07
Pin prefer-exact, refuse-ambiguous, never-pick
lamemustafa Sep 11, 2026
5eebe21
Pin the absent-master direction of the fold
lamemustafa Sep 11, 2026
9d0b085
Merge remote-tracking branch 'origin/master' into claude/recursing-mi…
Sep 11, 2026
9e34cd7
fix(master-binding): close four identifier holes and prove the path live
Sep 11, 2026
8189739
fix(master-binding): only a measured fold may resolve a master name
Sep 11, 2026
260b726
Merge remote-tracking branch 'origin/master' into claude/recursing-mi…
Sep 11, 2026
54f4c84
fix(master-binding): canonical equivalence is measured wrong, not unv…
Sep 11, 2026
6d953b2
fix(master-binding): a delimiter does not unmask, and ASCII is not th…
Sep 11, 2026
0d035de
Merge remote-tracking branch 'origin/master' into claude/recursing-mi…
Sep 11, 2026
dc42d90
fix(master-binding): the trailing space was directional too, and thre…
Sep 11, 2026
330bd69
fix(master-binding): the memo missed the path it was written for
Sep 11, 2026
1ba70e8
docs(corpus): the narrowed fold's live row, re-run and confirmed
Sep 11, 2026
0456b28
fix(master-binding): measure the SKU, and widen the fold to what it f…
Sep 11, 2026
35adca8
Merge origin/master, and measure that the folds compose
Sep 11, 2026
60c5cb6
fix(master-binding): four holes the previous round's own fixes opened
Sep 12, 2026
2025fc2
Merge remote-tracking branch 'origin/master' into claude/recursing-mi…
Sep 12, 2026
6858bd9
fix(master-binding): close four holes, and label what §9.4d measured
Sep 12, 2026
0ef41d4
Merge remote-tracking branch 'origin/master' into claude/recursing-mi…
Sep 12, 2026
4fe09f4
docs(tally): say what §9.4d's probe counted, and cite the guide by name
Sep 12, 2026
153905a
fix(master-binding): four more, three of them behind last round's own…
Sep 12, 2026
57380c0
Merge remote-tracking branch 'origin/master' into claude/recursing-mi…
Sep 12, 2026
404c2e3
fix(master-binding): the separator cuts both ways, and the memo exclu…
Sep 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
450 changes: 450 additions & 0 deletions docs/adr/0016-master-binding-authority.md

Large diffs are not rendered by default.

16 changes: 14 additions & 2 deletions docs/agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,20 @@ licence mode, or manually imported file, and only an unnumbered single-voucher

1. Call `voucher_schema` and produce a payload matching its schema. Transaction
IDs are client-supplied, unique within the batch, and retained in the local import ledger.
2. Call `validate_masters` with every ledger name. Correct every `near_miss`
with the exact live spelling; Bridge never creates masters.
2. Call `validate_masters` with every ledger name. **`build_import_xml` admits
`exact` only**, so replace the payload name for every row that is not
`exact`, and never invent one:
- `normalized` or `identifier` — the row is bound. Copy its
`exact_live_spelling` into the payload verbatim; the live name may differ
from yours in case, spacing, dash or quote style, and the import file
carries whatever you send byte for byte.
- `near_miss` — the row is **not** bound and Bridge chose nothing. Pick from
`candidates`, each labelled with the rule that surfaced it. A single
candidate is still not a decision. Where `reason` is
`master_binding_no_discriminating_candidate`, the name reaches
`candidate_count` masters that it does not distinguish and none is listed;
use a more complete source name, or read the ledger list and choose.
- `missing` — no live ledger matched. Bridge never creates masters.
3. Call `build_import_xml` with the payload. It checks exact decimal balance,
company date extent, live masters, and local journal integrity,
repeats the full catalogue to reject intervening changes, then writes `<data_dir>/imports/<batch_id>.xml` and records an append-only
Expand Down
137 changes: 133 additions & 4 deletions docs/tally/TALLY_PROTOCOL_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1154,14 +1154,19 @@ symmetry is exactly the property the separator result does not have.
| --- | --- |
| ASCII case folding | **VERIFIED** — lowercase matched |
| supplying a **space** where the master has a **hyphen** | **VERIFIED** — `BRIDGE PROBE LEDGER A` matched `BRIDGE-PROBE-LEDGER-A` |
| supplying a **hyphen** where the master has a **space** | **UNVERIFIED** — the reverse direction was never sent |
| supplying a **hyphen** where the master has a **space** | **UNVERIFIED here** — the reverse direction was never sent on this SKU. Measured **matched** on licensed 7.1, §9.4d |
| one trailing space ignored | **VERIFIED** |
| **two or more** trailing spaces ignored | **UNVERIFIED** — only one was sent |
| *leading* whitespace ignored | **UNVERIFIED** |
| runs of internal whitespace collapsed to one | **UNVERIFIED** — only a single space was tested |
| *leading* whitespace ignored | **UNVERIFIED here**. Measured **matched** on licensed 7.1, §9.4d |
| runs of internal whitespace collapsed to one | **UNVERIFIED here** — only a single space was tested. Measured **matched** on licensed 7.1, §9.4d |
| non-ASCII case folding (Devanagari, Tamil, Bengali, Turkish dotted I) | **UNVERIFIED** |
| **Unicode canonical equivalence (NFC/NFD)** | **MEASURED — folding it is wrong.** See below. |
| any other separator (underscore, en dash, `/`) treated as a space | **UNVERIFIED** |
| any other separator (underscore, en dash, `/`) treated as a space | **UNVERIFIED here**, and §9.4d splits it on licensed 7.1: `/` **matched**, underscore and en dash **rejected**. Not one row — do not fold them together |

**A wider result exists for a different SKU.** §9.4d re-ran this measurement on **licensed
TallyPrime 7.1** and found the gateway folds more than these rows establish. It is a separate
section on purpose: these rows are about Edit Log 7.0 Educational, and absorbing a licensed-Silver
result into them would silently widen the scope of a measurement nobody repeated here.

**The NFC/NFD row is the only one with evidence pointing the wrong way**, rather than no evidence
at all, and it is the one most likely to be folded in by accident.
Expand Down Expand Up @@ -1260,6 +1265,130 @@ Whether stock items, groups and voucher types match by the same rule is UNVERIFI
says nothing about voucher numbers — a fold shared between master names and voucher numbers is
assuming something nobody has measured.

### 9.4d Master-name matching on **licensed** TallyPrime 7.1

**VERIFIED 2026-09-12**, and it widens §9.4b rather than confirming it. §9.4b is inherited from a
2026-07-30 measurement on **Edit Log 7.0 Educational** and marks licensed TallyPrime UNVERIFIED.
This is that measurement re-run on the SKU this project actually writes to: **TallyPrime 7.1,
licence tier silver, `education_mode=false`**, ledgers, one lab company.

**Method is §9.4b's own.** Import a voucher naming a folded spelling of a ledger that exists, and
let Tally answer: a created voucher means the name resolved, a `LINEERROR` naming that ledger
means it did not. Twelve variants in the first run and six more in the second described below, one
voucher each, then the **day book was read back** to record which master each voucher actually
posted against — the counters alone would not have said. Every created voucher was then deleted by
`REMOTEID` and the day read back empty (eight from the first run, two from the second).

| Supplied against a live master | Licensed 7.1 | §9.4b on Educational |
| --- | --- | --- |
| exact | **matched** | matched |
| ASCII lowercase | **matched** | matched |
| one trailing space | **matched** | matched |
| a **space** where the master has a **hyphen** | **matched** | matched |
| a **hyphen** where the master has a **space** | **matched** | *UNVERIFIED* |
| leading whitespace | **matched** | *UNVERIFIED* |
| an internal whitespace run collapsed | **matched** | *UNVERIFIED* |
| a **slash** where the master has a **space** | **matched** | not sent |
| an **en dash** where the master has a space | **rejected** | *UNVERIFIED* |
| an **underscore** where the master has a space | **rejected** | *UNVERIFIED* |
| `AND` for `&` | **rejected** | rejected |
| a **missing** suffix word | **rejected** | rejected |
| an **added** suffix word | **rejected** | not sent |
| **NFD** against an NFC master | **rejected** | not sent |

**One row here was mislabelled and is corrected.** The first run of this probe recorded `AND` for
`&` as rejected, but what it actually sent was a name with `AND CO` **appended** — against a master
carrying no `&` at all. That measures an added suffix, not a substitution, and the label was wrong
even though the verdict happened to be. It was re-run against `Profit & Loss A/c`, a reserved
ledger present in every company:

| supplied against live `Profit & Loss A/c` | result |
| --- | --- |
| `Profit & Loss A/c` | **matched** — control |
| `profit & loss a/c` | **matched** — case folds on a name carrying `&` and `/` |
| `Profit AND Loss A/c` | **rejected** — the substitution, now measured here |
| `profit and loss a/c` | **rejected** |
| `Profit & Loss` | **rejected** — a missing suffix word |
| `Profit & Loss A/c AND CO` | **rejected** — an added suffix word, what the first run really sent |

So §9.4b's abbreviation findings hold on licensed 7.1 as well, and this section now says which
of them it measured rather than which it meant to.

**Composition was measured separately, because twelve single-axis results do not license it.**
Each row above is **one** transformation away from exact, so together they say each transformation
works alone and nothing about applying several at once — which is exactly what any fold does. Two
reviewers raised that independently, and it was worth a second run rather than an argument. Eight
more variants, same method, same readback and deletion:

| supplied | axes stacked | result |
| --- | --- | --- |
| `MB PILOT ALPHA (5550001001)` | control | **matched** |
| ` mb pilot alpha (5550001001) ` | case + leading + trailing | **matched** |
| `mb-pilot-alpha-(5550001001)` | case + hyphen-for-space | **matched** |
| ` mb-pilot-alpha-(5550001001) ` | case + hyphen + leading + trailing | **matched** |
| `MB/PILOT ALPHA (5550001001)` | slash + collapsed run | **matched** |
| `mb-pilot alpha/(5550001001)` | case + hyphen + slash, mixed in one name | **matched** |
| ` mb-pilot/alpha (5550001001) ` | all five at once | **matched** |
| ` mb probe ledger a ` against `MB-PROBE-LEDGER-A` | case + space-for-hyphen + surrounding + run | **matched** |

All eight posted against the intended master, confirmed by day-book readback. **So the folds
compose**, and a canonical form applying every measured transformation before comparing is
licensed by measurement rather than by extrapolation from the single-axis rows.

**What this says.** On licensed 7.1, Tally treats **space, hyphen and slash** as interchangeable
separators, collapses internal whitespace runs, ignores leading and trailing whitespace, folds
**ASCII** case, and is otherwise **exact on codepoints**.

> **RULE: separators fold, and the set is `space`, `-`, `/` — nothing else.** An en dash and an
> underscore are ordinary characters to Tally and are **not** separators, so a fold that treats
> "punctuation" or "separators" as a class is wider than the gateway and will merge masters it
> keeps apart.

That is the trap §9.4b warned about, arriving from the other side: the danger was never only that
a reader would fold too much, it was that "normalises separators" names no particular set. Two of
the four separators tested are folded and two are not, and nothing about their appearance predicts
which.

**Canonical equivalence is still refused**, consistent with the exact-codepoint finding recorded
elsewhere in this document: an NFD spelling of an NFC ledger does not resolve. A fold that
normalises before comparing merges masters this gateway keeps apart.

**Scope.** One instance, one build, one licence tier, **ledgers only**, one company, and the
measurement is of *import-time* name resolution — not collection filters, not stock items, groups
or voucher types, and not voucher numbers. §9.4b's Educational scope stands as its own row; this
does not retire it, and where the two disagree they disagree about different SKUs rather than
about the same one.

**Fixtures.** `MB-PROBE-LEDGER-A` and `MB CAFÉ PROBE` remain in `BRIDGE CORPUS OPENING` under
`Suspense A/c`, carrying no balances, so this is repeatable. They post-date the catalogue digest
recorded in `TEST_CORPUS.md` §9.2.


### 9.4c Real catalogues carry families a partial name cannot separate

**VERIFIED 2026-09-10** for the counts, across 16 loaded companies on both lab instances; the rule
built on them is PARTIAL. `TEST_CORPUS.md` §9.1 carries the procedure, the per-company figures and
what they do not cover.

Live books name parties in **sequentially-numbered families** — one observed catalogue runs a single
prefix across more than a hundred ledgers that differ only in a trailing number. A source name that
is a truncation of one of them reaches the whole family and distinguishes no member of it.

**Why that is a protocol-level fact and not an implementation detail:** any client matching a
supplied name against a read catalogue meets it, and the tempting response — offer the first N and
let a human pick — is measured wrong. Listing an arbitrary capped slice of such a family **put the
intended master outside the offered list about a third of the time** (present in 65.6% of lists,
against 100% once families beyond the cap were withheld and counted instead).

> **RULE: where a supplied name reaches a family it does not separate, report the count and withhold
> the list. An arbitrary slice of a family is not a shortlist — it is a wrong answer that looks like
> a shortlist.**

The scope is narrow and matters: the catalogue side is live, and every *source* name in the
measurement is a fabricated mutation of a live name. It measures the rule against real naming
habits, not against real operator input.


### 9.5 Identity after write

**VERIFIED.** `LASTMID` is **0** on successful master creates — unusable for master identity;
Expand Down
Loading