Skip to content

Correct a Bridge-built import batch in place, under compare-and-swap - #439

Merged
lamemustafa merged 3 commits into
masterfrom
feat/import-amend-cas
Sep 16, 2026
Merged

lamemustafa merged 3 commits into
masterfrom
feat/import-amend-cas

Conversation

@lamemustafa

Copy link
Copy Markdown
Owner

Why

Every build_import_xml build draws a fresh random batch identity, so rebuilding a corrected voucher and importing it creates a duplicate. That is the one thing that stops a bank-statement import from moving onto Bridge end to end: the manual workflow corrected a mis-booked row by re-importing under a stable REMOTEID, and Bridge had no equivalent.

Bridge deliberately does not let a caller's transaction label become a Tally upsert key, and that stays true. This PR adds an explicit correction path that cannot be entered by accident.

What

build_import_xml accepts an optional amends_batch_id, which must name a batch this Bridge built. The new build re-renders the named transactions under that batch's identity: the same REMOTEID and the same [BRIDGE:…] narration marker. A file import then alters those vouchers in place instead of creating new ones.

Tally's upsert is blind. It replaces whatever the voucher holds, including an edit someone made in Tally since. So an amendment is admitted only under a compare-and-swap:

  • Lineage admission (agent_import_amend.rs). The named batch resolves to its original, and every build sharing that identity is loaded (ledger::read_lineage). The amendment is refused if any build in that lineage was posted natively: post_import used a random private REMOTEID beside the marker, so importing over it would create a duplicate. It is also refused on a different company GUID, a different endpoint origin, or a legacy identity scheme.
  • Proposal admission. Every bridge_txn_id must be one the lineage built, with the same voucher_type and voucher_number. Amounts, entries, date, narration and reference may change. A type change is refused even though Tally was observed applying one (see below).
  • Book check. The preflight read's window is widened to cover each voucher's recorded dates as well as its new date. Each named voucher must be found by its marker, must not be cancelled or optional, and must match some build in the lineage exactly: date, type, entries (amounts compared in canonical form) and narration. A Journal's REFERENCE is not compared, because the verification read does not fetch it. The warning says so. One divergent voucher refuses the whole amendment, and nothing is written.
  • Native posting of an amendment is refused (import_post_amendment_requires_file_import).
  • The ledger record carries amends_batch_id, and attribution_tag and verification use the original identity, so verify_import on the amendment batch reads back the altered vouchers.

Limit, stated in the tool output: the compare-and-swap covers the book as read during the build. An edit made in Tally between the build and the hand import is overwritten. The warning tells the operator to import promptly and to expect "altered", not "created".

Measured before building on it

These measurements were made over the XML gateway on licensed TallyPrime 7.1 Silver, into a synthetic company only, with hand-built XML copying Bridge's envelope and voucher shape. A same-REMOTEID import with changed content returned CREATED=0 ALTERED=1 and kept the voucher's GUID in every one of these cases:

  • Journal and Payment: amounts changed, then an entry removed (bridge#429)
  • Journal: an entry added back
  • Receipt: amount changed, then counterparty changed
  • Contra: amount changed
  • Payment: date moved a day
  • Payment with a bare lowercase UUID REMOTEID: amount and date changed
  • Payment re-sent as a Receipt: it became a Receipt, which is why this PR refuses type changes

Before each write, a canary proved that an import naming unknown ledgers fails closed. Every probe used only ledgers absent from the client book that was also loaded, and a count-only read of that book stayed at zero. The table is recorded as a scoped correction in TALLY_PROTOCOL_REFERENCE.md §9.3.

Not measured: importing through Tally's own Import menu (which is how an operator imports these files), other releases or tiers, bill allocations, inventory or tax lines, and cancelled or optional vouchers.

Compatibility surface

agent_import_amend.rs is pinned, raising MAX_SURFACE_FILES from 238 to 239 with a named reason. The pin also has a guard test, because the gate cannot see a pin dropped by a merge.

This branch is rebased onto #438 (232→238), as agreed with that branch. Both pin lists and both rationale paragraphs are kept, the cap is 239, and the surface is resealed. The pin set is master's 238 plus this one file, with none dropped.

Tests

The new tests in agent_import_amend_tests.rs cover the lineage, dispatch refusal, company, endpoint and scheme refusals, proposal rules, the date window, the compare-and-swap (admitted, diverged, redated, missing, cancelled, one-of-two diverged), native-post refusal, schema and parsing, refusal before any Tally read, the pin guard, and two simulator end-to-end builds:

  • An amendment against an unchanged book reuses the original REMOTEID and records amends_batch_id.
  • An amendment against a book edited in Tally writes no file and no ledger record.

Controls proven by mutation: each of the dispatch check, CAS diff, narration comparison, cancelled check, type check, endpoint check, native-post refusal, tag identity, lineage dispatch fold, window, and the three build wirings (render identity, CAS call, recorded field) was disabled in turn, and at least one test failed each time.

Gates run locally: cargo fmt --check, cargo test --locked (src-tauri), cargo clippy --workspace --all-targets -D warnings -A clippy::pedantic, tools workspace tests including real_tree_has_complete_migration_and_report_surface_coverage, and scripts/reseal.sh --verify.

Review

Two Sonnet reviews ran, one on correctness and one on prose accuracy. Fixed from them:

  • Narration edits made in Tally were not part of the compare-and-swap, so an amendment would have overwritten them. It is now compared, with a test and a mutation check.
  • The date and voucher-type replacement was stated generally but measured on a Payment only. That is now scoped in the docs and code comments.
  • The measurement docs did not say the XML was hand-built rather than sent through Bridge's binary. They now do.
  • The schema description's "one it built" now names the lineage.

Not in this PR

  • voucher_presence derives the marker from the batch_id a caller supplies. For an amended voucher, pass the original batch id.
  • An older Bridge binary reading a ledger that contains an amendment ignores the field. Its verify_import on the amendment batch would misattribute (read-only). Its post_import should refuse: it re-renders the file under the amendment's own id, so the hash no longer matches the saved one (import_batch_changed). That comes from reading master's admission code; it was not run against an older binary.
  • No bank-statement glue. That comes next, on top of this.

🤖 Generated with Claude Code

@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 069d641 into master Sep 16, 2026
13 checks passed
@lamemustafa
lamemustafa deleted the feat/import-amend-cas branch September 16, 2026 10:05
lamemustafa pushed a commit that referenced this pull request Sep 16, 2026
…admit amendments through tools/call

build_import_xml takes proposals_id and proposals_sha256 in place of inline
vouchers. The resolver opens only a file parse_bank_statement published, with
a well-formed id under bank-statements/, through the hardened local-file open.
It requires the declared schema and id and refuses a file whose bytes no longer
hash to the digest the parse returned. It then hands the file's vouchers to the
unchanged admission path. Through tools/call against the simulator, a proposals
build consumes the same 44 requests as the inline build, and the import XML is
identical apart from batch identity.

The catalog validator did not know the pattern amends_batch_id publishes, so
every tools/call naming amends_batch_id was refused
argument_invalid:amends_batch_id before the handler ran. The amendment path
(#439) was reachable only by calling the handler directly, as its tests do. The
validator now checks that exact batch-id shape, and one constant serves both
the schema and the validator.

An ignored end-to-end test parses the synthetic encrypted HDFC statement
through parse_bank_statement, builds it by proposals_id, and gets six vouchers
with the wrapped UPI reference intact. CI runs it with the pinned PDFium.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lamemustafa pushed a commit that referenced this pull request Sep 16, 2026
…admit amendments through tools/call

build_import_xml takes proposals_id and proposals_sha256 in place of inline
vouchers. The resolver opens only a file parse_bank_statement published, with
a well-formed id under bank-statements/, through the hardened local-file open.
It requires the declared schema and id and refuses a file whose bytes no longer
hash to the digest the parse returned. It then hands the file's vouchers to the
unchanged admission path. Through tools/call against the simulator, a proposals
build consumes the same 44 requests as the inline build, and the import XML is
identical apart from batch identity.

The catalog validator did not know the pattern amends_batch_id publishes, so
every tools/call naming amends_batch_id was refused
argument_invalid:amends_batch_id before the handler ran. The amendment path
(#439) was reachable only by calling the handler directly, as its tests do. The
validator now checks that exact batch-id shape, and one constant serves both
the schema and the validator.

An ignored end-to-end test parses the synthetic encrypted HDFC statement
through parse_bank_statement, builds it by proposals_id, and gets six vouchers
with the wrapped UPI reference intact. CI runs it with the pinned PDFium.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lamemustafa pushed a commit that referenced this pull request Sep 16, 2026
build_import_xml publishes amends_batch_id with a bridge-UUIDv4 pattern that
argument admission did not recognize, and an unrecognized pattern refuses every
value. So every amendment sent over MCP was refused as
argument_invalid:amends_batch_id before the build ran; #439's tests called the
builder directly and never crossed that admission. Found by a live end-to-end
amendment against the licensed lab.

Admission now applies valid_batch_id for that pattern. The matcher lookup is
split out so a test can assert every pattern admission reads is recognized,
and the amendment refusal test now also goes through call_tool_response.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lamemustafa pushed a commit that referenced this pull request Sep 16, 2026
build_import_xml publishes amends_batch_id with a bridge-UUIDv4 pattern that
argument admission did not recognize, and an unrecognized pattern refuses every
value. So every amendment sent over MCP was refused as
argument_invalid:amends_batch_id before the build ran; #439's tests called the
builder directly and never crossed that admission. Found by a live end-to-end
amendment against the licensed lab.

Admission now applies valid_batch_id for that pattern. The matcher lookup is
split out so a test can assert every pattern admission reads is recognized,
and the amendment refusal test now also goes through call_tool_response.

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant