Skip to content

Rectify unqualified folded ledger binding - #331

Merged
lamemustafa merged 117 commits into
masterfrom
tapish-codex/rectify-issue329-20260912
Sep 15, 2026
Merged

lamemustafa merged 117 commits into
masterfrom
tapish-codex/rectify-issue329-20260912

Conversation

@lamemustafa

@lamemustafa lamemustafa commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Closes #329. Rectifies unqualified folded-name binding introduced by #288 and tracked by the P1 reviews on #314 and #316.

Functional summary

A generic catalogue has no product, release, licence, endpoint or operator-approval qualification. It therefore cannot authorize the historical gateway fold. Ledger and stock-item folded spellings now use the existing candidate path; exact names and decisive identifiers retain their behavior. Narrow-index candidates stay visible before the listing cap without outranking stronger identifier candidates or cloning/scanning entire holder vectors during sort.

Current BindingBasis has only Identifier and ExactName; a normalized bound status cannot be constructed or deserialized. There is no core binding-report persistence reader. Historical captured JSON remains evidence, and persisted operator selections use their separate existing catalogue-binding representation with fresh exact revalidation. Import refusals explain operator candidate selection, updating the exact payload spelling, and revalidation before a new build.

The ADR, protocol reference, test corpus and operator text preserve measured licensed Silver directional observations while withdrawing the generic symmetric/canonical inference. The source-draft fixture’s provenance explicitly distinguishes its captured catalogue from authored source XML.

Merge hold: review thread hy1D8 still requires a captured end-to-end source-draft flow. The retained DTO is derived from a captured catalogue plus authored source input; these tests cannot close that P1. Authored synthetic XML is legitimate source-draft input; the outstanding evidence is the actual native load/catalogue command and its returned DTO. Read-only native connection and guarded company selection succeeded after the SSH restart. The Mac is currently locked; the final native load/catalogue capture remains pending on this candidate. #290 has landed. This candidate normally integrates master 06528eeb615b827896b9649ea476034143bbf7f9 and targets master; its entire source tree is byte-identical to the accepted 723a572c4895c025002aac0b3e2769536c9b8ff9 tree. Hosted checks and review must qualify the new head.

Test or reproduction command

Candidate 314ff0a8fa8a053103d2780d3065647034afd7d1:

  • Rust 1.96.0: cargo test --locked --manifest-path src-tauri/Cargo.toml -p bridge-tally-core --lib: 149 passed on the final core runtime/test bytes at dec42831330987bc7330504668b93a74ba2986f7. Core and adapter Clippy passed with warnings denied. These full-suite results are retained in the agent execution transcript; a separate full log was not retained.
  • The byte-cap regression executes the real one_master_match parser/binder/renderer with one narrow-fold name and ten lexically earlier wide-only matches whose combined bytes exceed 8,192. The narrow candidate remains first and visible in the truncated output.
  • The final focused adapter regression for a mixed identifier/missing/near-miss refusal passed. The message requires an operator-reviewed exact payload update and fresh validate_masters before building; no file is written on refusal.
  • Source-draft Vitest: 58 passed; frontend production build passed after the normal Rectify catalogue candidate precision and lookup work #290 prerequisite merge. That UI source is unchanged by the later core/adapter corrections.
  • Earlier focused consumer controls passed through the production parser/binder with a captured catalogue. They remain offline consumer evidence, not a captured source-draft end-to-end run.
  • Root independently reviewed the final runtime delta. All 212 actual-byte pins and unique membership match; supported rehash, seal, repoint and compatibility gate pass with 11 unknown and 0 evidenced claims. Diff checks pass.

Migration compatibility

Current report deserialization intentionally rejects the legacy normalized basis. No core binding-report storage migration or database change is introduced. Newly prepared folded names require operator selection and exact revalidation. Existing saved operator choices retain their independent freshness checks.

Rollback notes

Reverting would restore unqualified automatic folded binding. Keep candidate-only behavior in any replacement. The read-only native connection and company-selection probe is not final source-draft evidence. No financial mutation or import was performed.

Reuse/deletion: reuses the existing candidate matcher and deletes automatic folded-name authority. No unused scope registry, compatibility type, feature flag, dependency or alternative matcher. Without the repair, a downstream exact-spelling check can validate an incorrect target after a caller copies an unqualified bound name.

Security impact

Automatic target selection is narrowed; company/endpoint admission, fresh catalogue checks and exact import authorization remain enforced. No DSC, credential, transport or dispatch change. Local regression evidence does not replace the explicit end-to-end capture hold.

lamemustafa and others added 30 commits September 10, 2026 21:45
Every failure across four document-import engagements was binding a
document's entities to the target book's masters, never reading the
document. Bridge was growing two answers to it: an MCP-private prefix
matcher that named one near-miss candidate as `exact_live_spelling`, and
a desktop screen that correctly ranks nothing but narrows nothing either.

Add `bridge_tally_core::master_binding` as the single contract both
surfaces consume, per ADR 0016. It matches an identifier embedded in a
master name before the name itself, binds only where a rule is unique on
both sides, and never resolves a near-miss: it reports candidates with
the rule that surfaced each, and no score. An empty catalogue is a typed
refusal rather than a report full of "missing".

Deletes `master_match` and `master_key` from agent_import and moves
unicode-normalization down a layer with them. The write gate is
unchanged: build_import_xml and the approved-post recheck still admit
byte-exact names only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…scale

Self-review found a real defect: a catalog name was trimmed at the
boundary, so a bound row reported a spelling the book does not contain.
The write gate compares byte-exact against Tally's own name, so that
would have refused with no explanation. Observed names are now retained
verbatim; only source names are trimmed. Names differing solely in
surrounding whitespace are an ambiguity, not a refused catalog.

Adds a characterization suite over one fabricated 200-master book with
the recorded naming pathologies. The assertion that matters is that no
entity binds to a master a human would not have chosen; the counts are
pinned underneath so loosening a threshold has to move a number.

The mutation sweep was checked against two positive controls rather than
trusted for passing: resolving a near-miss to its first candidate trips
it, and binding a lone candidate does not. Both results are recorded in
the test, so it is read as "no mutation reaches the wrong master" and
not as "no rule change can loosen binding".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rzakhani-94af40

# Conflicts:
#	docs/tally/compatibility/compatibility-matrix.json
#	docs/tally/compatibility/compatibility-surface.json
The preparation screen listed every ledger in the company for every
source entry, so the operator read the whole catalogue once per line.
#288 already returns a per-entry binding; nothing drew it.

Each entry's list now opens with the ledger binding matched, or with the
candidates it could not choose between, followed by the complete
catalogue under its own heading. The full list stays reachable in every
case: this is a shortcut through it, never a filter on it.

Nothing is selected for the operator. A near miss says plainly that
nothing was chosen and reports a truncated candidate list truthfully,
and a capture carrying no bindings renders exactly as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ten review findings, read against the tree rather than taken at face
value, and all reproduced. The severe one: bind_one selected Identifier
before ExactName, so a ledger carrying a number, requested byte-exactly,
returned match_state identifier while the write gate admits exact only.
Every ledger with a phone or account number in its name was permanently
unimportable — the exact population this contract was built for. When
the two signals agree it now reports the byte-level fact.

Also: neither side trims a name any more, so a trailing space cannot
claim byte equality it does not have; digits inside a mixed code are no
longer emitted as a standalone numeric; all admitted eight-digit date
orders are excluded, not just year-first; more identifiers than the
bound is refused rather than truncated, which could hide a conflict; the
source-entity bound now covers what the source parser admits and the
desktop states whether narrowing ran; desktop candidate bytes are capped
in aggregate; a binding refusal keeps the catalogue evidence it already
read; and master_binding.rs is sealed into the compatibility surface,
which needed a deliberate one-file cap raise since it now decides
admission outcomes.

The larger finding came from running the binder over 470 real ledger
names from all 16 synthetic books. Prefix matching offered a median of
40 candidates, 63% of the catalogue, and omitted the right master a
third of the time: a truncated name reaches a whole family, and an
alphabetically capped slice of DN Party 001..120 does not contain
DN Party 057. A prefix matching more masters than the cap is now counted
and deliberately not listed. Re-measured on the same names: where
candidates are listed the right master is present in 403 of 403 rows,
median list length 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The remaining review finding, and the one I had triaged but not fixed.
FallbackBinding::assign took an EntityBinding detached from its report
plus any catalog, so a stock-item binding could be parked against a
ledger catalog and the result carried no provenance for anything
downstream to detect.

Assignment is now a method on BindingReport taking an index into its own
entities, so an entity from another report cannot be named at all, the
catalog class is checked, and the binding carries its class forward.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A fix is a change and generates its own findings; the re-review of the
previous commits raised four, all reproduced.

An unusable ledger name in a parsed draft was skipped while the response
still claimed a complete narrowing pass, so the rows that vanished were
exactly the ones worth looking at; the pass is now reported unavailable.
The reported candidate total took the larger of the suppressed family and
the retained candidates, which under-reports when they are different
masters; it is now their union. An identifier hint reached extraction
without the bound applied to every other name. And the operator workflow
in docs/agent/README.md still told readers to correct only near_miss
rows, which now leaves a bound-but-not-exact row refused at the build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two further findings from the re-review, both reproduced.

`Purchases FY25` and `Sales FY25` both yielded the code identifier
`FY25`, so identifier-first matching bound the source to whichever
existed before it ever compared the names. A fiscal-period label
identifies a period, not a party, and is now excluded by shape; the
minimum code length also rises from four to six, since a four-character
mixed token is weak evidence of identity and the failure mode here is
money against the wrong party. Re-measured against the same 470 live
ledger names: no change to the distribution, so the tightening costs
nothing observed.

Candidate collection recounted every prefix from the start of the name,
making it quadratic in a field the source parser lets reach 4 KiB. It
now carries the character count forward in one pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No book on the instance carried an embedded identifier: across 470 live
ledger names from all 16 loaded companies, zero yielded a numeric
identifier and exactly one a code identifier. The rule that separates
this from fuzzy matching had no live coverage at all.

Ten `MB ` ledgers now exist in BRIDGE CORPUS OPENING, parented to
Suspense A/c so no receivable, payable or ageing measurement moves, and
documented in TEST_CORPUS.md section 9 with the import method and the
company-choice reasoning. BRIDGE PROBE B SANDBOX was rejected as the
target despite the manufacturing precedent: it shares a GUID with a
second loaded company and Bridge's own reads refuse it as
company_identity_ambiguous.

Within minutes the pair sharing one identifier exposed a defect no
fabricated fixture had produced. A byte-exact request for a ledger whose
embedded number is shared with another was refused as
IdentifierConflict, making that ledger permanently unimportable, since
the write gate admits exact only. Byte equality with an observed master
name is now decisive: it names exactly one master, and an ambiguous
identifier does not undermine it. Only a decisive identifier pointing
elsewhere still outranks an exact name, and that stays a reported
conflict.

Re-measured over 485 live names, 2,330 cases: identifier binds 3 -> 11,
every uppercase mutation now binds, and where candidates are listed the
right master is present in 434 of 434 rows at a median list length of 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two findings from the latest re-review, both reproduced.

Enumerating the period shapes that must not become identifiers was a
losing game: FY25 was fixed, then APR2025 and 2025Q1 were still binding
two unrelated ledgers that merely share a period. The rule is now a
shape — every run in the token is a short alphabetic marker or a number
reading as a year or small ordinal, at most three runs — and a code
identifier additionally needs eight alphanumerics, three digits and two
letters. Requiring real length is the part that does not depend on
having thought of every label. Measured against 485 live ledger names,
exactly one yields a code identifier at all, and it still does.

The aggregate candidate budget was applied to the consumer's copy, so
the report's own clones were already allocated by then; capping the copy
bounded only the copy. The budget now lives in bind() and is spent in
entity order, and the desktop's second budget is deleted as redundant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The voucher-presence lane needs the same fold for voucher numbers and
voucher-type names that master names use, and is exposing this function
crate-wide to get it. That is the right call — a second, subtly
different normaliser is the divergence ADR 0016 exists to end, and it
would diverge silently, agreeing on every name tested by hand and
differing on the punctuation nobody thinks to try.

Records that obligation at the function, and its corollary: changing
what this folds changes every consumer's notion of sameness at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…40' into claude/source-draft-narrow-targets

# Conflicts:
#	docs/tally/compatibility/compatibility-matrix.json
#	docs/tally/compatibility/compatibility-surface.json
The narrowing copy predated the state the live measurement made
necessary. For a source name that reaches a family of ledgers and tells
none of them apart, the report now lists no candidates and reports the
count — so the screen would have rendered "0 possible ledgers are listed
first", a count of nothing, over the case that matters most.

That state now says what it means: the line matches N existing ledgers
and separates none of them, so none is listed; use a fuller source name
or pick from the full list. No misleading "Possible" heading appears
over an empty group, and the whole catalogue stays reachable as before.

Also surfaces bindings_state: when the narrowing pass could not run, the
toolbar says so rather than letting every row look merely unnarrowed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A client-supplied REMOTEID is not readable back — Tally overwrites the
attribute with its own value (IMPLEMENTATION_GUIDE.md §3.3a, fourth
property, verified). Nothing here uses it, but the doc on
unresolved_identity said only that the identity is retained "for later
reallocation" without naming the channel, and the obvious wrong choice
fails silently: an amount parked with its identity in a write-only field
is unreallocatable, and nothing about the write says so.

Says narration, and says why. Surfaced by the voucher-presence lane,
which had to correct three claims of its own after reading the
reference's §9.3 heading as the general case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…facts

candidates can be empty because nothing resembles the name, because a
family resembles it and none is separable, or because the list was cut —
and those mean opposite things to whoever decides what to do next. The
disambiguators are reason, candidate_count and candidates_truncated, and
reading the empty vector alone is wrong in two cases out of three.

Recorded here rather than left to each consumer because it has already
been got wrong twice by different lanes: the preparation screen rendered
"0 possible ledgers are listed first" over a family of 120, and the
voucher-presence contract needed a paired test to stop its rule
collapsing into "no candidates means unknown". Same defect class this
ADR was written against — a refusal whose neighbouring value reads as an
answer.

Also records why this is a doc and not a type. An enum of Listed /
Truncated / Withheld / None is the stronger fix and the one P2 asks for,
but it is breaking, a stacked consumer already depends on
candidates_truncated as a predicate and holds the boundary with tests,
and forcing that rework mid-review trades an improvement for a
regression risk. Revisit once both have merged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…isting

Five items in one round, because review reopens once either way and the
marginal cost of the rest once it is reopened is small.

Tally's master-name matching is measured, not guessable:
IMPLEMENTATION_GUIDE.md §3.3b found it case-insensitive AND
separator-insensitive — a hyphen matches a space — and otherwise exact.
The binder was stricter, which is not the safe direction it looks like:
it refused names Tally accepts, and `X - Y` is a common ledger
convention. A separate master_identity_key follows §3.3b and stops where
Tally stops; `AND` for `&`, a missing suffix word and a singular for a
plural still refuse. It is separate from comparison_key rather than a
widening of it, because that one is shared with voucher numbers and
voucher-type names and §3.3b says nothing about those. Measured live: 16
of 16 hyphenated masters now bind from the spelling Tally itself
accepts, where all 16 were near-misses before.

Candidates becomes None | Listed | Truncated | Withheld. An empty vector
was three different facts and a consumer reading is_empty() was wrong in
two of them, a shape already got wrong twice by different lanes. Taken
before merge because the contract has not shipped and this is the
cheapest it will ever be; the consumer who pays for it measured thirty
lines and reported the change improves its code. The MCP result gains an
explicit listing discriminator, since a model is the caller that would
read an empty array as "no such ledger exists"; the desktop DTO stays
flat, where the screen already distinguishes the cases and is tested.

Also: FallbackBinding says reallocate with a Journal and never Alter or
Cancel, which §9.7 measured as duplicating with the target untouched
while reporting success; the ADR records that identifier coverage is
bimodal by client (42%, 0%, 0%, 0%) so the rule is a first-pass check
and never a primary key; and BindingStatus says what a Bound does not
establish — not that the master still exists, not that the requested
name may be written, not that it is right in business terms, and no
authority at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three were the same shape recurring: an identifier built from something
that identifies a period rather than a party.

A token carrying letters now never yields a standalone numeric, whether
or not it qualified as a code — `Part A12345678` was reaching an
unrelated `Bank 12345678` through the one-letter gap the code test
rejects. Period labels are recognised by their numbers rather than their
words, which catches `SEPTEMBER2025` and `2025QUARTER1` that no cap on
the alphabetic run ever would: a month name can be any length, a year
cannot. And a fiscal range is excluded before its digits are fused,
since `2025-2026` strips to an eight-digit run no calendar reading
rejects.

Fallback assignment now checks catalog provenance, not just class: two
ledger catalogs are both Ledger, and a fallback drawn from the one the
report never saw names a master that was never a candidate. Candidate
collection selects by index and clones only what it retains, instead of
cloning every match before the cap and the budget discard most of it.

ADR 0016 quoted thresholds this module stopped using two rounds ago, and
it is the contract two surfaces integrate against. Synced — and a test
now reads the ADR and asserts it quotes the live constants, so the next
drift fails rather than waiting to be noticed. Verified against a
positive control: changing a constant without the document fails it. It
also caught a false positive of its own on first run, which was the
detector being too strict about `(10%)` rather than the ADR being wrong.

Live re-measure over 485 names, 2,330 cases: 434 of 434 listed rows
still contain the right master, median listed length 2, no wrong binds.
Identifier binds 11 -> 8 with bound and unbound totals unchanged: three
mutations that had bound to themselves through a leaked numeric now bind
by name instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ializable

Both reproduced by execution before fixing, and both re-verified against
the same reproduction after.

A token shared across a name family IS the family. Where the catalog is
large enough that the token stays under the common-token threshold — 30
rows among 330 is 9% — the shared-token pass re-offered exactly the rows
the prefix pass had withheld, restoring the arbitrary capped slice that
withholding exists to prevent. Measured: the case reported near_miss
with 25 of 30 listed and the intended master outside the slice; it now
reports no_discriminating_candidate with the family counted and none
listed. Weaker rules now skip the withheld set; decisive rules still
reach a family member on its own evidence, because the identifier or the
whole key is what separates that one from its siblings.

Candidates::Listed was a newtype variant carrying a sequence, and under
Serde's internally tagged representation a tag cannot be merged into a
sequence — so serializing the MOST COMMON unresolved result failed at
runtime while the other three variants succeeded. Now a struct variant.
No test caught it because none had ever serialized an Unresolved, only a
Bound; every variant now round-trips in a test.

Live re-measure unchanged: 434 of 434 listed rows contain the right
master, median listed length 2, no wrong binds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…40' into claude/source-draft-narrow-targets

# Conflicts:
#	docs/tally/compatibility/compatibility-matrix.json
#	docs/tally/compatibility/compatibility-surface.json
…k a name

Both reproduced by execution before fixing, both re-verified after, and
both fixes are smaller than what they replace.

`FY2025-26` canonicalizes to `FY202526`, whose six-digit run reads as no
period at all, so the label became a Code identifier and `Purchases
FY2025-26` bound to a sole `Sales FY2025-26`. The period test now runs
on the raw token and splits on the separators operators actually write,
so `FY2025` and `26` stay legible as what they are. That subsumes
is_year_range, which is deleted: one period test where there were two,
covering ranges the numeric path caught and the code path did not.

Two identifier hints selecting two other masters, with the source name
byte-matching a third, bound the name and silently discarded the
conflict. A byte-exact name survives an identifier that is merely
shared — the ambiguous set still contains the master the name spells —
but not identifiers that all point elsewhere. The predicate is now that
one sentence rather than three conditions, and the report offers every
master the evidence reached, so the operator sees the disagreement
rather than one side of it.

Live re-measure unchanged: 434 of 434 listed rows contain the right
master, median listed length 2, no wrong binds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two wrong binds, both reproduced before fixing and both re-verified
after, and both the same class one level down: an identifier built from
something that is not identity.

The letter guard was ASCII-only, so `पार्टी12345678` read as digits
standing alone and bound a party to an unrelated `Bank 12345678`. The
observed books carry Devanagari, Tamil and Bengali ledger names, so this
was reachable on the corpus this PR already reads. The guard is now
Unicode alphabetic.

`XXXXX1234X` cleared every length and composition test — ten characters,
six letters, four digits, no period — while carrying only a last four
that any number of parties share, so two unrelated ledgers with the same
mask bound to each other. A token whose letters are a single repeated
character is a mask; an identity-bearing code has distinct letters.

TEST_CORPUS.md §9 recorded live counts without the confidence marker
AGENTS.md requires, so the seeding, the coverage counts, the rule's live
behaviour and the general safety claim are now separated into VERIFIED,
VERIFIED, PARTIAL and UNVERIFIED with the scope of each. The strongest
claim in that section was never the one a reader would have taken from
it.

Live re-measure unchanged: 434 of 434 listed rows contain the right
master, no wrong binds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Third finding on the same predicate, so the fix is the structural one
rather than a fourth condition.

The predicate asked whether the *union* of matched masters contains the
byte-exact one. That answers the shared case correctly — one number on
two masters, where the name separates them — and the mixed case wrongly:
`ACME 11111111` with a hint reaching `BETA 22222222` has the exact
master in the union because its own number is one of the identifiers,
while a second identifier plainly disagrees.

Flattening identifier-to-master provenance into one set discarded the
only fact that separates those two, and they need opposite answers. The
match is now kept per identifier, and a byte-exact name is outranked
when any single identifier reached somewhere else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…40' into claude/source-draft-narrow-targets

# Conflicts:
#	docs/tally/compatibility/compatibility-matrix.json
#	docs/tally/compatibility/compatibility-surface.json
`is_masked` guards the code branch, where a mask spelled with letters is
caught by the letter test anyway. A mask spelled with punctuation never
reaches it: in the numeric branch every non-digit is an ordinary
delimiter, so `********12345678` split cleanly and offered its visible
suffix as though it were the whole account, binding two unrelated
ledgers that share it.

A value written with mask punctuation is partial by construction, so
what it exposes is a suffix and not the number. Ordinary punctuation
around a whole number is untouched — `(5550001001)` and `5550001-002`
still bind — because a fix that rejected all punctuation would have been
quietly worse than the bug.

Fourth finding in the same family: an identifier built from something
that is not identity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…40' into claude/source-draft-narrow-targets

# Conflicts:
#	docs/tally/compatibility/compatibility-matrix.json
#	docs/tally/compatibility/compatibility-surface.json
Tokenisation split on "not alphanumeric", and `char::is_alphanumeric` is
false for a Devanagari virama — the halant that joins consonants — and
false for a nukta. So Indic ledger names were cut at exactly the
character that holds a word together: `राय एण्ड सन्स` yielded one usable
token instead of three, `ट्रेडर्स` became a fragment, and a Tamil name
lost its tail. Those names are in the books this binder already reads.

The separator set is now defined positively — whitespace, and ASCII
punctuation — so an ASCII class decides only questions about ASCII
characters and everything outside it is word content, marks and joiners
included. That keeps the rule right for scripts nobody here has tested.

This only ever degraded candidate quality, never caused a wrong bind:
shared tokens surface candidates and do not decide. But it degraded it
precisely for the names an Indian firm would actually use.

Verified against a positive control: with the old predicate the new test
finds no candidates at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…40' into claude/source-draft-narrow-targets

# Conflicts:
#	docs/tally/compatibility/compatibility-matrix.json
#	docs/tally/compatibility/compatibility-surface.json
…rzakhani-94af40

# Conflicts:
#	docs/tally/compatibility/compatibility-matrix.json
#	docs/tally/compatibility/compatibility-surface.json
…40' into claude/source-draft-narrow-targets

# Conflicts:
#	docs/tally/compatibility/compatibility-matrix.json
#	docs/tally/compatibility/compatibility-surface.json
Two more of the same family, both small.

A mask and the digits it hides are often written apart: `**** 12345678`
is the same statement as `********12345678`, and reading tokens
independently lost the relationship, so the attached form was rejected
while the separated one bound. Extraction now carries whether the
preceding token was a mask.

The period boundary split on ASCII `-` and `/` only, while the
comparison key already folds the Unicode dash variants — so `FY2025–26`
fused to `FY202526` and became an identity-bearing code where
`FY2025-26` had failed closed. Both now admit the same set, named once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff9d4664ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/adr/0016-master-binding-authority.md
Comment thread src-tauri/crates/bridge-tally-core/src/master_binding.rs
Comment thread src-tauri/crates/bridge-tally-core/src/master_binding.rs Outdated
Comment thread src-tauri/crates/bridge-tally-core/src/master_binding.rs
Comment thread scripts/fixtures/source-draft-capture-bindings.json

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c023687181

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src-tauri/crates/bridge-tally-core/src/master_binding.rs
Comment thread src-tauri/src/agent_import.rs Outdated
Base automatically changed from claude/source-draft-narrow-targets to master September 12, 2026 21:19
t and others added 2 commits September 13, 2026 02:51
master moved when #320 landed, which stales the pinned digests on this
branch. The only conflicts were the two compatibility manifests; the pin
sets were identical on both sides (212 entries, none added or removed), so
master's sealed manifest was taken and the digests recomputed from the
merged tree rather than hand-edited.

Reseal ran in the documented order: rehash-surface reported 4 changed
entries, matching exactly the four pinned files this branch modifies
(TALLY_PROTOCOL_REFERENCE.md, master_binding.rs, agent_import.rs,
source_draft/catalog.rs). A confirming rehash after seal-surface and
repoint-matrix reports 0 changed entries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

# Conflicts:
#	docs/tally/compatibility/compatibility-matrix.json
#	docs/tally/compatibility/compatibility-surface.json
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@lamemustafa
lamemustafa merged commit 62a5e0b into master Sep 15, 2026
13 checks passed
@lamemustafa
lamemustafa deleted the tapish-codex/rectify-issue329-20260912 branch September 15, 2026 09:09
lamemustafa pushed a commit that referenced this pull request Sep 15, 2026
master narrowed the same three paragraphs of TALLY_PROTOCOL_REFERENCE.md
via #331 that this branch narrowed independently. Both say the measured
slash case is directional and licenses no canonical form; the wordings
differ, so git could not merge them.

Resolution takes master's text for all three, because that wording shipped
with the code change that removed BindingBasis::NormalizedName and because
it withdraws the earlier claim as binding authority rather than erasing it
from the probe history. This branch's operational RULE blockquote is kept
after master's scope paragraph: master states the limit abstractly (does not
generalize across product, tier, object class, direction or caller) while
the blockquote names the concrete failure a reader must avoid (a fold
treating punctuation as a class merges masters the gateway keeps apart).

The branch's other three documents do not overlap with master and are
carried unchanged.

Compatibility surface resealed: rehash reported one changed entry, the
protocol reference, and a confirming pass reports zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lamemustafa pushed a commit that referenced this pull request Sep 15, 2026
The code conflicts this branch was expected to carry (agent_catalog.rs,
agent_import.rs, agent_import_tests.rs) auto-merged once #314 and #331 were
on master; only documentation and the compatibility manifests conflicted.

This branch carried a copy of #314's documentation corrections while #314 was
under review, so both sides had independently reworded the same paragraphs.
Resolution took whichever text states the post-#331 reality explicitly:

- TALLY_PROTOCOL_REFERENCE.md keeps master's wording. Its "Superseded
  interpretation" block withdraws the earlier claim as binding authority
  rather than erasing it, and already states the candidate-only rule.
- PROMPT_PLAYBOOK.md, IMPLEMENTATION_GUIDE.md and IMPROVEMENT_PLAN_2026H2.md
  keep this branch's wording, which names the binder directly: folded names
  remain candidates for operator selection and exact revalidation. Master's
  IMPLEMENTATION_GUIDE line implied a recorded directional comparison could
  still authorize automatic binding, which is no longer true after #331
  removed BindingBasis::NormalizedName.

Checked that taking this branch's PROMPT_PLAYBOOK did not drop #314's merged
content: the staged file differs from master by 12 added and 5 removed lines,
and those 5 are exactly the two reworded hunks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lamemustafa pushed a commit that referenced this pull request Sep 15, 2026
Conflicts were the two compatibility manifests, TALLY_PROTOCOL_REFERENCE.md,
and the bank-statement sanitiser plus its test, which #316 also touched.

- sanitise-bbox-capture.py takes this branch's side: it is a strict superset,
  carrying the same encoding="utf-8" read that master added plus this
  branch's keep-list materialisation and _load_parser call.
- sanitise-bbox-capture.test.py takes this branch's side. _load_parser does
  not exist on master, so master's block is the pre-#332 shape; this branch's
  version adds the EvidenceRefusal handling that matches its own code, plus
  boundary controls for cropped rows, wrong-profile parsing and row-count
  mismatch.
- TALLY_PROTOCOL_REFERENCE.md takes master's wording, consistent with how the
  same three paragraphs were resolved when #314 and #316 landed.

Verified rather than assumed: scripts/sanitise-bbox-capture.test.py runs
green on the merged tree ("all sanitiser contracts hold").

Compatibility surface resealed; a confirming rehash reports zero changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lamemustafa pushed a commit that referenced this pull request Sep 15, 2026
a_party_difference_echoes_the_source_spelling_not_its_catalog_binding
proposed the party as "alpha traders" against a catalogue holding
"Alpha Traders". That bound through BindingBasis::NormalizedName, which
#331 removed from master while this branch was held.

Post-#331 the folded spelling binds to nothing, so PartyOutcome is not
Bound, and book_presence declines to report a difference at all -- by
design: "Only a *bound* party can disagree: an ambiguous one has no single
name to" compare against. The test was asserting pre-#331 binding
semantics, not a property of difference reporting.

The proposal now uses the catalogue's exact name. The test's actual subject
is unchanged: a reported difference echoes the source spelling against the
observed one, rather than echoing whatever the source bound to.

This is the integration cost of a branch held open across a binding-contract
change, and it was only visible in the test suite -- fmt, check and clippy
all passed on the merged tree. 267 pass in bridge-tally-core.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lamemustafa pushed a commit that referenced this pull request Sep 15, 2026
#397 moved the window-completeness gate from `BookWindow::observed` to
`Absent`'s production site, and #294 then synced to master. Both landed in
this branch's base, conflicting in five files.

Resolutions, none of which drop either side's work:

- `book_presence.rs` (5 hunks). Kept this branch's `ObservedWindow`
  constructor, `ColumnEvidence` and `narration_evidence`; kept the base's
  `read` field and `read()` accessor; deleted the `WindowIncomplete`
  refusal the base removed. The withholding order in `decide` is
  preserved: `skipped_evidence`, `ManualNumberNotSupplied`, the party
  outcomes, then the window read, then `Absent`.
- `agent_catalog.rs`. Both sides rewrote the `voucher_presence`
  description. Replayed this branch's two narration-marker sentences onto
  the base's rewritten contract text at anchors present verbatim in both,
  so neither description silently replaced the other.
- `book_presence_tests.rs` (2 hunks). Took the base's renamed and inverted
  partial-window test through this branch's constructor, and set
  `codes.len()` by counting the `PresenceError` variants the test lists
  rather than by arithmetic on the two sides.
- Both compatibility manifests. Took this branch's 217-pin set, which is a
  strict superset of the base's 216, then resealed with `scripts/reseal.sh`
  (36 hashes changed). Verified directly that no path is missing from
  either parent, since `--verify` passes over a dropped pin.

Two fixes the merge itself required:

`a_partial_window_withholds_absent_even_when_nothing_resembles_the_proposal`
auto-merged cleanly against the pre-`ObservedWindow` positional
constructor and would not have compiled. Ported.

`a_party_difference_echoes_the_source_spelling_not_its_catalog_binding`
passed on this branch and failed after the merge, deterministically.
Cause is master, not either PR: "Rectify unqualified folded ledger
binding" (#331) changed a folded name match from resolving a binding to
only suggesting candidates, so the proposal's `alpha traders` no longer
reached `PartyOutcome::Bound` and the party-difference block is guarded on
`Bound`. An exact spelling would have made the test a duplicate of
`a_party_difference_compares_the_observed_party_field_not_every_ledger`
under a name that no longer described it, so the test moves to the basis
that still decides without byte equality: an identifier embedded in the
master name. Driven to failure by removing that identifier, to prove it is
what carries the test.

ADR 0017 named the type `RemoteIdEvidence`, which this branch renamed to
`ColumnEvidence`, in three places. Corrected. The `RemoteIdEvidenceUnavailable`
reason variant is unchanged.

Verified under the pinned 1.96.0 toolchain: fmt clean; src-tauri workspace
1537 passed, 0 failed, 1 ignored (the manual live replay); doctests 2
passed; tools workspace 55 passed; clippy `-D warnings` clean on both
workspaces with no warning or error lines; `reseal.sh --verify` current.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:tally Tally integration type:rectify Regression rectification

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: folded ledger binding lacks qualified scope

1 participant