diff --git a/docs/tally/IMPLEMENTATION_GUIDE.md b/docs/tally/IMPLEMENTATION_GUIDE.md index 2b7ecd741..c7f0e58a5 100644 --- a/docs/tally/IMPLEMENTATION_GUIDE.md +++ b/docs/tally/IMPLEMENTATION_GUIDE.md @@ -585,16 +585,17 @@ has always named. It may overwrite, may partially update, or may duplicate. `TAL **Consequences.** -*Positive:* this gives real duplicate prevention without a TDL plugin and without a UDF -fingerprint. For a generate-a-file-the-human-imports design, **re-running the same file is safe.** +*Positive:* the measured byte-identical Journal repeat produced no duplicate without a TDL plugin +or UDF fingerprint. That observation does not qualify a resend after an unknown outcome or an +intervening external edit; the repeated import can alter the existing voucher. *Not the outbox, and not the narration marker.* Both of those were listed here as unnecessary and neither is: - **The durable dispatch intent stays.** `REMOTEID` prevents a duplicate; it does not tell you, - after a crash, *what you sent*. A resend is only safe while the exact key and payload are still - on disk, which is what the `row fsynced before dispatch` invariant and the restart-reconciliation - flow in `docs/agent/README.md` are for. + after a crash, *what you sent*. Preserve the exact key and payload on disk for read-only outcome + reconciliation, as required by `row fsynced before dispatch` and `docs/agent/README.md`. + Retaining them is not permission to resend after an unknown outcome. - **An independent attribution marker stays.** The returned *attribute* does not echo the client key — but the key itself survives in any field Tally does not own. The committed capture `src-tauri/crates/bridge-tally-protocol/tests/fixtures/agent/native-namespaced-journal.utf16le.xml` returns it inside `NARRATION` as @@ -611,7 +612,7 @@ proof-of-post claim must account for that — carry your own marker in a field T or when the payload differs from the original (partial update semantics). Also untested on licensed or standard TallyPrime. -### 3.3b Master-name matching: case- and separator-insensitive, otherwise exact +### 3.3b Master-name matching: directional alternatives, otherwise exact **VERIFIED 2026-07-30**, against a ledger named `BRIDGE-PROBE-LEDGER-A` and one named `ZZ Ram & Sons Pvt Ltd`: @@ -627,7 +628,11 @@ licensed or standard TallyPrime. | `ZZ Ram & Son Pvt Ltd` (singular for plural) | **rejected** | | entirely different name | **rejected** | -So Tally normalises **case and separators** and is otherwise **exact on letters**. +These rows establish only the supplied candidate against the recorded master; +they do **not** establish a symmetric case/separator normalizer or canonical +fold. Automatic binding is exact-codepoint unless the authoritative protocol +records the particular directional comparison. In particular, do not infer a +slash rule from this baseline. > **Promoted to `TALLY_PROTOCOL_REFERENCE.md` §9.4b**, which is where observed gateway behaviour > belongs and which carries the consequences for a writer. This entry stays as the measurement @@ -671,14 +676,53 @@ is no natural idempotency: voucher number is not a key. **What the heading's "narrowed" means.** §3.3a since established that a byte-identical repeat under the same `REMOTEID` is an **upsert** on the qualified Journal path — `CREATED=0, ALTERED=1`, no -duplicate. So on that path a crash-retry of the *same file* is safe on its own, and the sentence -that used to stand here — that the fingerprint plus an embedded key is "the only thing" preventing a -duplicate — is no longer true where §3.3a applies. - -It is still true everywhere §3.3a does not reach, and that is most places: a **different** payload -under the same key is untested (it may overwrite, partially update or duplicate), as is any -non-Journal voucher type, any other SKU, and a retry across a Tally restart or a company boundary. -Name which case you are in before relying on either mechanism. +duplicate. This is the observed outcome of that repeat, not permission to resend after an unknown +outcome: retain the original batch identity and reconcile its outcome first. It does not protect +against an intervening external edit. The sentence that used to stand here — that the fingerprint +plus an embedded key is "the only thing" preventing a duplicate — is no longer true where §3.3a +applies. + +**And it is not true anywhere else either.** An earlier revision of this paragraph — mine — said it +"is still true everywhere §3.3a does not reach", which quietly kept the fingerprint alive as a +duplicate-prevention mechanism in every case §3.3a excludes. §3.4a establishes the opposite: a +`(date, amount, ledger-set, voucher-type)` tuple **cannot** distinguish a retry from a legitimate +identical payment, so it prevents no duplicate anywhere. Two paragraphs of one patch contradicting +each other is how a withdrawn mandate comes back. + +What is actually true outside §3.3a's reach is narrower and less comfortable — with **one** +narrowly qualified exception, and the qualification is tighter than the first correction made it +look. `TALLY_PROTOCOL_REFERENCE.md` §9.8 measured **one thing**: how a **failed `Alter`** behaves +under Manual numbering with `PREVENTDUPLICATES=Yes`. It was cleanly rejected — `CREATED=0, +ALTERED=0, EXCEPTIONS=1` — where automatic numbering silently duplicated. That is the whole result. + +Three limits come with it, and §9.8 states two of them itself: + +- **Request shape.** The observation is about a failed `Alter`. §9.8's own rule says *"Do not apply + the failed-`Alter` observation to a different request identity mechanism."* A crash retry sends a + `Create`, which is a different request shape and is **UNVERIFIED**. +- **SKU.** §9.8 carries no licensed qualification for the numbering path. Its later scope + clarification covers a licensed *Journal* `ACTION="Create"` repeat carrying `REMOTEID` and says + in terms that it does **not** establish voucher-number identity, the configured numbering method, + or other request shapes. +- **Voucher type.** Journal only, as everywhere else in this section. + +So the honest statement is: **for a failed `Alter` on the measured baseline, Manual numbering +converts a silent duplicate into a clean rejection.** It is not a general duplicate-prevention +mechanism, and a `Create` retry is not covered by it. + +Outside §3.3a's `REMOTEID` path and outside that one measured case, **there is no proven +duplicate-prevention mechanism at all.** A **different** payload under the same key is untested (it +may overwrite, partially update or duplicate), as is any non-Journal voucher type, any other SKU, +and a retry across a Tally restart or a company boundary. Name which case you are in, and where it +is neither, stop and involve a human rather than reaching for the tuple. + +The tuple is withdrawn in every case. §9.8 does not rehabilitate the fingerprint — it reports how +one failure mode behaves under one setting, which is a different kind of thing entirely. + +**Why this needed two corrections.** The first revision withdrew an over-broad claim ("no proven +mechanism anywhere") and replaced it with another one ("Manual + `PREVENTDUPLICATES` is a proven +mechanism"), widening §9.8 past both its request shape and its SKU in the act of narrowing +something else. A claim is not made safe by being a correction. ### 3.4a Undefined UDF fields are silently discarded — **the plan's primary idempotency key does not work as written** @@ -773,23 +817,38 @@ fingerprint is **co-primary** rather than secondary. Withdrawn: promoting it doe to do the job. The tuple is identical for a legitimate recurring or same-day repeat payment, so as an automatic dedupe it suppresses real vouchers no matter which tier it is placed in — see §3.4a. -What follows instead is narrower and less comfortable: **there is no proven automatic -write-confirmation mechanism for Phase 4.** `REMOTEID` upsert covers a byte-identical repeat on the -Journal path (§3.3a) and nothing beyond it; a destroyed narration marker leaves a write -unattributable, and the honest response to that is to stop and ask a human, not to substitute a -signal that cannot tell the two cases apart. +What follows instead is narrower and less comfortable: **there is no proven mechanism that lets an +automatic dedupe *decision* be made from the fingerprint tuple.** That is not the same claim as "no +proven duplicate-prevention mechanism outside §3.3a" — it overstates the gap, but only just. One +mechanism is proven: `REMOTEID` upsert on a byte-identical repeat on the Journal path (§3.3a). One +narrower observation sits beside it: under Manual numbering with `PREVENTDUPLICATES=Yes`, a +**failed `Alter`** is cleanly rejected rather than silently duplicated — `CREATED=0, ALTERED=0, +EXCEPTIONS=1` (§3.3; +[`TALLY_PROTOCOL_REFERENCE.md` §9.8](TALLY_PROTOCOL_REFERENCE.md#98-voucher-numbering-method-changes-everything--use-manual)). +That is a failed-`Alter` result on §9.8's own baseline, not a general rejection mechanism: §9.8 +forbids carrying it to a different request identity mechanism, and a crash retry sends a `Create`. +Neither reaches a destroyed narration marker or a differently-numbered duplicate under automatic +numbering; for those cases the honest response is still to stop and ask a human, not to substitute +the fingerprint as an automatic suppressor — it cannot tell a retry from a legitimate second +payment no matter which carrier is missing. ### 3.5 Identity after write -`LASTMID` is **0** on successful master creates — unusable. Read masters back by normalised -name. `LASTVCHID` is populated for vouchers and usable, subject to a foreign-writer -cross-check. It also accepts non-numeric text without error when parsed back, so validate it. +`LASTMID` is **0** on successful master creates — unusable. Read masters back by name — and +**normalised never means NFC/NFD-normalised**: §9.4b measured Tally matching on exact +codepoints, so normalising before comparing resolves an NFD create onto a distinct +pre-existing NFC master and promotes the wrong object. Which name rule applies is the +SCOPE GATE's question (`PROMPT_PLAYBOOK.md` Phase 4 step 4); on an unqualified licensed SKU +it is exact codepoints and nothing else. `LASTVCHID` is populated for vouchers and usable, +subject to a foreign-writer cross-check. It also accepts non-numeric text without error when parsed back, so validate it. ### 3.6 Master re-create is a silent Alter Re-sending an identical ledger `ACTION="Create"` returned `CREATED=0, ALTERED=1` — the -existing master was **overwritten** with the retry payload. Pre-read before creating, and -persist `CREATED` and `ALTERED` as distinct outbox outcomes. +existing master was **overwritten** with the retry payload. Persist `CREATED` and `ALTERED` as +distinct outbox outcomes. A pre-read alone does not authorize creation: use the complete-catalogue +and mutation-time prerequisites in `PROMPT_PLAYBOOK.md` Phase 4 step 3a; an unqualified case stays +unresolved without dispatch. ### 3.7 Company pinning is asymmetric — I2 @@ -1116,7 +1175,7 @@ zero; fail closed or quarantine. | Modal dialog | Gateway blocked until a human clicks | §5.1 | | `ClosingBalance` read as a period figure | Wrong balance, presented as correct | §6.4 | | `ACTION="Alter"` + `REMOTEID` | Creates a duplicate. `Create` upserts a **byte-identical** repeat; a *corrected* payload is UNVERIFIED and may overwrite, partially update or duplicate | §3.3a | -| Master name differing by more than case/separators | Voucher rejected, master NOT auto-created | §3.3b | +| Master name outside an exact or qualified directional comparison | Voucher rejected, master NOT auto-created | §3.3b | | Omitting `BILLALLOCATIONS.LIST` | Allocation becomes `On Account` with no bill identity | §3.3c | | Self-referential `$$NumItems` in a collection | Gateway hangs, empty reply | §5.3b | | `` used for a per-request constant | Per-row work; request exceeds deadline | §2.3a | diff --git a/docs/tally/IMPROVEMENT_PLAN_2026H2.md b/docs/tally/IMPROVEMENT_PLAN_2026H2.md index cfa20791b..78b3bc897 100644 --- a/docs/tally/IMPROVEMENT_PLAN_2026H2.md +++ b/docs/tally/IMPROVEMENT_PLAN_2026H2.md @@ -403,17 +403,27 @@ Re-sending the identical voucher payload, same `VOUCHERNUMBER`, produced `CREATE > **Superseded 2026-09-11 — do not build the fingerprint on the strength of this.** The measurement holds: without a client `REMOTEID`, a repeated payload creates a second voucher. But it is not the *only* thing available, because the vouchers measured here carried no client `REMOTEID` and Tally therefore assigned its own. `IMPLEMENTATION_GUIDE.md` §3.3a measures the controlled case: with a client-supplied `REMOTEID`, a byte-identical re-import returns `CREATED=0, ALTERED=1` and leaves **one** voucher. So a duplicate-prevention scheme does not need a TDL plugin or a UDF fingerprint **on the path that was measured**, which is a byte-identical Journal repeat on one licensed instance. §9.8 states its own exclusions: not other voucher types, and **not restart behaviour**. Recovery code must not read this as a general licence to redispatch — a retry after a Tally restart, or on a voucher type nobody has qualified, is exactly where a categorical reading of `REMOTEID` would recreate the client-data duplication §8.4 exists to prevent. Qualify each context live before retrying in it. See also `TALLY_PROTOCOL_REFERENCE.md` §9.3, whose title carried the same overgeneralisation until it was corrected. > -> **This narrows §3.1.2's fingerprint; it does not remove the outbox.** `REMOTEID` prevents a duplicate. It does not tell you, after a crash, *what you sent* — the returned `REMOTEID` **attribute** is Tally's own value, not yours, so the dispatch intent has to survive locally or the write is neither provable nor safely reconstructible. Be precise about which field: the key itself does survive in anything Tally does not own, and the committed capture returns it inside `NARRATION` as `[BRIDGE:…]`. A categorical "Tally does not return the key" would send recovery work to discard the one attribution channel that works. The `row fsynced before dispatch` invariant in this plan and the restart-reconciliation flow in `docs/agent/README.md` both still stand, and a resend is only safe while the exact key and payload are still on disk. +> **This narrows §3.1.2's fingerprint; it does not remove the outbox.** `REMOTEID` prevents a duplicate. It does not tell you, after a crash, *what you sent* — the returned `REMOTEID` **attribute** is Tally's own value, not yours, so the dispatch intent has to survive locally or the write is neither provable nor safely reconstructible. Be precise about which field: the key itself does survive in anything Tally does not own, and the committed capture returns it inside `NARRATION` as `[BRIDGE:…]`. A categorical "Tally does not return the key" would send recovery work to discard the one attribution channel that works. The `row fsynced before dispatch` invariant in this plan and the restart-reconciliation flow in `docs/agent/README.md` both still stand, and the exact key and payload must remain on disk for read-only reconciliation. They do not authorize a resend after an unknown outcome. ### 8.5 Re-creating an existing master silently becomes an Alter Re-sending the identical ledger `ACTION="Create"` returned `CREATED=0, ALTERED=1` — no error. A retry silently **overwrites** the existing master with the retry payload, including any defaulted fields. -**Adds to §6:** master creates require a pre-existence read before dispatch, and `CREATED` vs `ALTERED` must persist as distinct outbox outcomes. "No duplicate was made" is not the same as "my create succeeded." +**DEVIATION 2026-09-12 — amends §6:** a pre-existence read is necessary but does not +authorize master creation. Require a qualified Complete catalogue for the exact company and +master class, plus a qualified mutation-time condition or proven exclusive-write window. +Concurrent automatic creation remains UNQUALIFIED until those prerequisites hold; another +ordinary pre-read or operator confirmation is insufficient. Apply the full creation/bind/refusal +contract in `PROMPT_PLAYBOOK.md` Phase 4 step 3a, including approved-field equality before +binding an existing name. Retain unresolved proposals and block dependent writes when evidence +is missing. Persist `CREATED` and `ALTERED` as distinct outcomes; avoiding a duplicate does not +establish that the requested create succeeded. ### 8.6 `LASTMID` is 0 on successful master creates; `LASTVCHID` works -Both ledger creates returned `LASTMID=0` despite `CREATED=1`. **Confirms §5.1.4's choice**: masters must be read back by normalized name. `LASTVCHID` is populated for vouchers and usable, still subject to the foreign-writer cross-check. +Both ledger creates returned `LASTMID=0` despite `CREATED=1`. **Confirms §5.1.4's choice**: masters must be read back by name. Which name-matching rule applies is decided by the SCOPE GATE in `PROMPT_PLAYBOOK.md` Phase 4 step 4 and by nothing here: §9.4b's case-folding and separator rows inherit §0's **Edit Log 7.0 Educational** baseline and mark licensed TallyPrime UNVERIFIED, so on a licensed SKU match on **exact codepoints** and let a case or separator difference fail loudly. Widen only where a licensed capture has qualified a **directional write comparison**: §9.4d does so for **ledgers** on licensed 7.1 **Silver**, with `education_mode=false`, by importing vouchers naming folded spellings and reading the day book back, and for no other master type or licence tier. Its slash row is slash-candidate against space-master only; the reverse is not qualified, so slash-bearing cross-spellings remain exact-only. Gold and other unqualified tiers remain exact-only. A compatibility result cannot widen it — that evidence is a live-**read** receipt and `compatibility/README` says it never establishes any write behaviour. `LASTVCHID` is populated for vouchers and usable, still subject to the foreign-writer cross-check. + +DEVIATION 2026-09-12 (`TALLY_PROTOCOL_REFERENCE.md` §9.4b): "normalized name" here never means NFC/NFD normalization, which is WITHDRAWN. §9.4b is MEASURED: Tally matches master names on exact codepoints, so an NFD create read back with NFC folding applied would resolve onto a pre-existing, distinct NFC master and promote the wrong object. Compare on exact codepoints. This deviation removes normalization; it does not decide whether any case or separator fold is permitted on top — that is the SCOPE GATE's question, answered above, and on an unqualified licensed SKU the answer is exact codepoints and nothing else. ### 8.7 AlterID high-water marks move — Drift Sentinel's mechanism is sound @@ -734,8 +744,11 @@ not say so. That is the failure this plan is least able to detect, because nothi no counter is wrong. The correction, with the full table of what each signal does support, is in -[`IMPLEMENTATION_GUIDE.md`](./IMPLEMENTATION_GUIDE.md) §6.2, and -`TALLY_PROTOCOL_REFERENCE.md` §3.3a states the same rule from the other side: +[`IMPLEMENTATION_GUIDE.md`](./IMPLEMENTATION_GUIDE.md) **§3.4a** — §6.2 is the Unicode +round-trip result and carries none of this — and +[`TALLY_PROTOCOL_REFERENCE.md`](./TALLY_PROTOCOL_REFERENCE.md) **§9.3** states the same rule from +the other side. (`§3.3a` is a section of the implementation guide, not of the reference; citing it +under the wrong document sent readers to a section that does not exist.) | Signal | What it supports | | --- | --- | @@ -743,8 +756,15 @@ The correction, with the full table of what each signal does support, is in | narration marker | attribution — which client write produced which voucher | | date/amount/ledger tuple | "these look alike, a human should check" — **not** a dedupe decision | -**Consequence for the qualification work:** the regression in -[`PROMPT_PLAYBOOK.md`](./PROMPT_PLAYBOOK.md) §6 — "duplicate re-dispatch with edited narration is -still caught by the fingerprint check" — is still a *useful* case, but its exit criterion is that -the pair is **flagged for review**, not that the second dispatch is suppressed. A qualification -that passes by suppressing it is qualifying the defect. +**Deviation 2026-09-12 — unknown outcomes remain read-only.** The former +qualification wording below required a second dispatch and treated preventing it +as a defect. That acceptance criterion is withdrawn. After an unknown outcome, +reconcile the original saved batch without sending another write; an edited +narration or an absent fingerprint match cannot authorize replay. Assert that the +dispatch count does not increase and unresolved evidence remains visible for +manual review, as required by `PROMPT_PLAYBOOK.md` Phase 4 recovery and §6. + +The original distinction remains relevant to independently proposed business +events: a similar date/amount/ledger tuple is a review flag and cannot by itself +suppress a separately approved legitimate payment. It does not turn recovery of +one unknown attempt into a new business event. diff --git a/docs/tally/PROMPT_PLAYBOOK.md b/docs/tally/PROMPT_PLAYBOOK.md index 2211d14b8..736062416 100644 --- a/docs/tally/PROMPT_PLAYBOOK.md +++ b/docs/tally/PROMPT_PLAYBOOK.md @@ -49,7 +49,8 @@ STILL NON-NEGOTIABLE (unchanged): - Exact decimals only; never floating point for amounts. - A failed/partial/cancelled run never advances a verified checkpoint. - "Posted" is never claimed from counters alone; only from readback. -- No automatic retry of writes without an idempotency probe first. +- No automatic resend after an unknown write outcome. Retain the original batch + identity and use read-only outcome reconciliation; an inconclusive result stays held. - Deletion tombstones only from complete, verified scans. - Only synthetic test data. Never commit/log raw books data, GSTINs, PANs, narrations, credentials, usernames, or machine paths. @@ -298,9 +299,41 @@ Implement: fix-it list. 5. Encoding/normalization hardening: UTF-8/UTF-16LE/BOM fixtures; non-English (Devanagari, Gujarati, Tamil) company/ledger/narration - fixtures in the simulator corpus; NFC normalization + case-insensitive - collation for name keys (Tally name uniqueness is effectively - case-insensitive). + fixtures in the simulator corpus. Scope name-key resolution by SKU and + entity class, using the shared ADR 0016 binder: + - On §0's Edit Log 7.0 Educational baseline, §9.4b's + `accepts(candidate, tally_name)` permits only the measured directional + transformations; do not infer their reverse. + - For LEDGERS on licensed 7.1 Silver (`education_mode=false`), use only + §9.4d's individually measured **directional** comparisons; do not turn + them into a canonical fold. In particular, its slash row supplied a + slash candidate against a space-bearing master. The reverse was not + measured, so slash-bearing cross-spellings remain exact-codepoint only + until both directions are qualified. Preserve every other codepoint, + including NFC/NFD, en dash and underscore distinctions. + - Other licensed scopes remain exact-codepoint unless a capture + qualifies their particular rule. Compatibility live-READ receipts + do not establish write behaviour (`compatibility/README`). + Name keys are exact at storage. Apply any permitted fold only while + resolving a request against retained rows; if a fold reaches more than + one row, surface ambiguity. Storing folded keys can erase a distinct + `Alpha-Beta`/`Alpha Beta` or case-differing row before ambiguity handling + can run. + Resolution still requires unique identity evidence. Never coalesce + distinct catalogue or mirror identities merely because a fold agrees. + A read is not a safe place to be wrong about this — the mirror rows + built here are what later binding decisions resolve against, so a fold + that merges two masters here merges them everywhere downstream. + DEVIATION 2026-09-12 (TALLY_PROTOCOL_REFERENCE.md §9.4b): NFC + normalization of name keys is WITHDRAWN. §9.4b is MEASURED, not + inferred: an NFD spelling of a UI-created NFC ledger was rejected + (`EXCEPTIONS=1`, ledger does not exist) while the NFC spelling created + it — Tally matches on exact codepoints. Normalizing before comparing + resolves a name onto a master Tally itself keeps apart. Name keys + used to resolve identity compare on exact codepoints; do not + NFC/NFD-normalize either side of that decision. A broader comparison + may suggest unresolved candidates or refuse a suspected collision, as + ADR 0016 permits; it must not merge mirror identities or authorize a write. 6. Migration: versioned mirror schema evolution for the new fields (voucher lines, bill allocations, inventory lines, tax lines) with rollback notes. @@ -345,9 +378,16 @@ Hunt specifically for: 3. Amount fidelity: any new tax/inventory line parsed through anything but ExactDecimal; sign conventions (IsDeemedPositive) mishandled on new line types; Dr/Cr balance invariant not re-checked with lines present. -4. Identity/normalization traps: NFC normalization applied on read but not - on the keys used for diffing (same ledger counted twice); case-collation - asymmetry between mirror and reconciliation. +4. Identity/normalization traps: NFC/NFD normalization used to resolve + identity, merge mirror rows, or accept readback is a finding even when + applied consistently to both sides (§9.4b preserves those codepoint + distinctions). This does not prohibit the broader, non-deciding + candidate comparison in ADR 0016 or the conservative refusal detector + in Phase 4 step 3a. Those paths may suggest or refuse; they may not + resolve identity or authorize a write. Likewise, a symmetric case fold + used to resolve identity needs its own qualified scope: §9.4b's + `accepts(candidate, tally_name)` measures only the stated direction; + use §9.4d only within its separately observed scope. 5. Bounded-resource regressions: new list explosions (AllInventoryEntries on huge vouchers) versus the 32 MiB response cap — is there a paging or windowing story? Does a capped response get honestly labeled Partial? @@ -548,7 +588,7 @@ is simpler), then vouchers (payment/receipt/journal/contra). Implement — write core (masters): 1. Outbox state machine in the mirror DB: PENDING → DISPATCHING → {CONFIRMED | CONFIRMED_WITH_DIVERGENCE | REJECTED | OUTCOME_UNKNOWN} - OUTCOME_UNKNOWN → probe → {CONFIRMED | CONFIRMED_WITH_DIVERGENCE | PENDING | MANUAL} + OUTCOME_UNKNOWN → read-only probe → {CONFIRMED | CONFIRMED_WITH_DIVERGENCE | OUTCOME_UNKNOWN | MANUAL} `CONFIRMED_WITH_DIVERGENCE` is the terminal state when readback (step 4) proves the write landed but Tally normalized/dropped a field vs intent; it is a distinct persisted state, never collapsed into `CONFIRMED`, and it @@ -562,8 +602,116 @@ Implement — write core (masters): MAX_LEDGER_WRITE_BATCH. 3. Single-writer actor owns the import surface; reads gated during dispatch→readback windows; queue depth visible. +3a. MASTER-CREATION GUARD — **BEFORE DISPATCH, NOT IN READBACK.** + This runs on the intent, before any import is sent. Placing it in + step 4 was a defect: step 4 begins "after counters accept", and by + then `ACTION="Create"` against a name Tally considers matching has + already returned `CREATED=0, ALTERED=1` and **silently overwritten + the existing master with the retry payload** (IMPLEMENTATION_GUIDE + §3.6, plan §8.5). A refusal raised during readback protects nothing; + the client's master is already gone. Pre-read, then decide, then + dispatch. + The harm is an OVERWRITE, not a duplicate. An earlier revision of + this guard said a near-collision "makes a SECOND master differing + only by case". That is not what was measured: the existing master is + altered in place, so its group, its opening balance and its GST + registration are replaced by whatever the new payload carried. A + duplicate is visible in a ledger list; an overwrite is not. + This pre-read is a necessary check, not a mutation-time guarantee. + Require a catalogue whose source completeness is qualified for the + exact company and master class. A capped, truncated, Partial, failed, + or unqualified read cannot authorize creation, even if it is nonempty + and contains no collision. Never infer completeness from a row count + below the transport cap or from two agreeing bounded reads. + Bridge's actor serializes only Bridge: another operator or importer + can create the name after this read, before dispatch. Readback cannot + recover the previous master after an overwrite. The dispatch counters + must therefore remain an independent race detector: a create must report + `CREATED=1`; `ALTERED=1` is an overwrite alarm and manual halt, never a + promotion to CONFIRMED. A create has no pre-image of a foreign master + introduced after the pre-read, so Bridge cannot restore its prior content. + Retain the affected name, exact dispatched payload and dispatch time, and + tell the operator that the unobserved prior content requires reconstruction + from their own records or a Tally backup. This detects the race but does not + prevent it. Concurrent automatic master creation therefore remains + UNQUALIFIED. Do not enable it until + a qualified mutation-time condition or proven exclusive-write window + covers that interval; a confirmation or another ordinary pre-read + does not establish either. The quiet-company Journal preview in + issue #239 is not evidence qualifying this master-create flow. + Three outcomes, never two: + **bind** to an exact-codepoint match without creating or altering it + only after fresh same-company readback also matches every relevant + approved master field (including parent/group, GST configuration and + opening balance). Missing/unqualified field evidence or any difference + is a visible intent conflict: retain the unresolved proposal and block + dependent voucher dispatch for manual resolution. A name match alone + must never silently discard approved fields; + **create** only after the completeness and mutation-time prerequisites + above are qualified AND no existing master collides under the detector + below; + otherwise **REFUSE and retain the unresolved proposal.** + Regression acceptance includes an exact-name master with differing + approved fields and an unreadable required field: both remain visibly + unresolved and neither creates, alters, nor enables a dependent write. + THE DETECTOR IS NOT THE BINDER AND MUST BE WIDER THAN IT. + §9.4b's `accepts()` is DIRECTIONAL — for a requested `FOO` against an + existing `foo`, `accepts(FOO, foo)` is false — so reusing it as the + detector misses exactly the collision it exists to catch. The + detector folds SYMMETRICALLY and deliberately over-wide: + case-insensitive both ways; slash, hyphen and space interchangeable + in both directions and in composition; leading and trailing whitespace + ignored; internal whitespace + runs collapsed; **and NFC/NFD canonical equivalents treated as + colliding.** That last row matters most and is the one most easily + left out: §9.4b's exact-codepoint result came from an **EDU** + instance, so a licensed SKU that folds canonical equivalence is not + excluded — and an NFD request beside an existing NFC master would + otherwise pass exact lookup *and* the detector, and overwrite it. + Detecting NFC/NFD collision does NOT reintroduce NFC normalisation + into matching: the binder still compares exact codepoints. One folds + to refuse, the other folds to write, and only the second needs + evidence. + Several detector rows are UNVERIFIED as *matching* behaviour, which + is why they belong here: **an unverified equivalence cannot justify a + write, but it is ample reason to stop and ask.** A detector that + misses a collision overwrites a master in a client's book; a detector + that over-fires costs one question to a human. Fail toward the + question. 4. Readback verification: after counters accept, re-export the object - (masters by normalized name; vouchers by LASTVCHID) and + (masters matched by name under the SCOPE GATE below — never by a + broader rule stated anywhere else in this step; vouchers by + LASTVCHID) and + SCOPE GATE (§9.4b, §9.4d, §0) — THE ONLY NAME-MATCHING RULE IN THIS + STEP. §9.4b's rows use §0's **Edit Log 7.0 Educational** baseline + except the separately captured NFC/NFD row. That capture is not licensed — + `src-tauri/crates/bridge-tally-protocol/tests/fixtures/encoding/` + `PROVENANCE.md` records a TallyPrime EDU instance and date only. Its + product family and release are **UNVERIFIED** in that evidence, so it + qualifies neither the Edit Log 7.0 baseline nor a licensed SKU. + **§9.4d is the licensed qualification, and it is qualification of a + WRITE.** It re-ran §9.4b's method on **TallyPrime 7.1, licence tier + silver, `education_mode=false`** by importing vouchers naming folded + spellings and reading the **day book** back to see which master each + posted against. That is observed write behaviour on the SKU this + project writes to, for **ledgers**. + So: for **ledgers on licensed 7.1 Silver (`education_mode=false`)**, + match only under §9.4d's individually measured directional rows. Its + slash row does not qualify the reverse, so a slash-bearing cross-spelling + is **exact-codepoint only** and fails loudly until both directions are + captured. Gold, other tiers and unqualified versions remain exact-codepoint + only. For **every other master type** — stock items, groups, voucher types + — §9.4d measured nothing, so match on **exact codepoints** and let a case + or separator difference fail loudly. + **A compatibility result cannot widen this.** `compatibility/README` + defines a cell's evidence as a live-**read** receipt and says it + "never establishes ... any write behavior". An earlier revision of + this gate offered the connected instance's compatibility result as a + widening route; it is withdrawn. Only a direct write measurement + like §9.4d qualifies a write predicate. + Exact-only matching is not automatically the safe answer — see the + MASTER-CREATION GUARD at step 3a, which is where the creating case is + decided, and which runs before dispatch rather than here. ALWAYS cross-check the fetched object against the idempotency key and the (date, amount, ledger-set, voucher-type) fingerprint before promoting to CONFIRMED — LASTVCHID can be clobbered by a foreign @@ -571,20 +719,27 @@ Implement — write core (masters): else OUTCOME_UNKNOWN. Persist the BridgeID ↔ GUID/MasterID binding. Field-diff readback vs intent; divergence → CONFIRMED_WITH_DIVERGENCE, surfaced in the Gap Map, never silent. -5. OutcomeUnknown recovery: on restart, DISPATCHING rows → probe by key + - fingerprint. A probe MATCH is not itself a confirmation: run the SAME - full field-level readback diff as the normal dispatch path (step 4) and - resolve to `CONFIRMED` or `CONFIRMED_WITH_DIVERGENCE` — never promote to - `CONFIRMED` on key+fingerprint alone (Tally can retain both identifiers - while normalizing/dropping other fields, which would report a divergent - write as clean). Re-dispatch ONLY on an unambiguous ABSENCE PROOF that - cannot be confused with an edited prior write: because a crash can be - followed by a foreign edit that changes the narration and a fingerprint - field (so a real prior write matches neither probe), a mere "not found by - probe" is inconclusive → stay `OUTCOME_UNKNOWN` or escalate to `MANUAL`, - never re-dispatch. Alter with foreign AlterID bump → `MANUAL`. Bounded - retries (3, backoff) only from a proven-absent state, then `MANUAL` with - evidence. + DEVIATION 2026-09-12 (TALLY_PROTOCOL_REFERENCE.md §9.4b): "matched by + name" never means NFC/NFD-normalized. An NFD create read back against + a pre-existing NFC master would resolve as a match and promote the + wrong object to CONFIRMED — §9.4b measured Tally keeping the two + apart. Never normalize either side first. This deviation removes + normalization; it does not widen what remains — whether any case or + separator fold is permitted on top of exact codepoints is decided by + the SCOPE GATE above and by nothing in this sentence. On an + unqualified licensed SKU that leaves exact codepoints and nothing else. +5. OutcomeUnknown recovery: on restart, retain the original batch identity, + intent and payload for every DISPATCHING row. Reconciliation is read-only; + it must never dispatch the original write again. A probe MATCH is not + itself a confirmation: run the SAME full field-level readback diff as + the normal path (step 4), and resolve to `CONFIRMED` or + `CONFIRMED_WITH_DIVERGENCE` only on that evidence. A missing or ambiguous + match stays `OUTCOME_UNKNOWN` or goes to `MANUAL`; an edited prior write + can match neither key nor fingerprint. A foreign AlterID change goes + to `MANUAL`. Bound the read-only reconciliation attempts, not write + retries. DEVIATION 2026-09-12: the former absence-proof re-dispatch and + three-write-retry branches are withdrawn. No mutation-time absence + authority was qualified for them; a probe must not manufacture one. Implement — voucher writes (after masters CONFIRMED-path is soak-tested): 6. Voucher Create for payment/receipt/journal/contra with full lines, @@ -593,13 +748,15 @@ Implement — voucher writes (after masters CONFIRMED-path is soak-tested): fallback — WHICH of the two is authoritative is a per-version compatibility claim qualified on the licensed lab. The fingerprint check is mandatory secondary dedupe regardless (narration is user- - editable; never trust the embedded key alone on re-dispatch). + editable; neither signal alone authorizes recovery or a resend). DEVIATION 2026-09-11 (IMPROVEMENT_PLAN_2026H2 §8.19): "dedupe" here means RAISE A FLAG FOR A HUMAN, never suppress automatically. The tuple cannot tell a retry from a legitimate second payment — a standing instruction, or two invoices settled to one supplier on one day, produce the identical tuple — so automatic suppression is a - silent under-write. See IMPLEMENTATION_GUIDE.md §6.2. + silent under-write. See IMPLEMENTATION_GUIDE.md §3.4a — the signal + table and its automatic-suppression constraints. (§6.2 is the Unicode + round-trip result and says nothing about fingerprints.) 7. Cancel qualified as the compensation primitive (ACTION=Cancel by REMOTEID/GUID). Alter-by-GUID qualified per version; where flaky, the fallback is a Cancel+Create saga bound in one outbox transaction with @@ -613,20 +770,23 @@ Implement — voucher writes (after masters CONFIRMED-path is soak-tested): education-mode and never Verified. Tests (the non-negotiable five, plus unit coverage): -- crash mid-dispatch → restart → recovery resolves to exactly-once (probe - finds the voucher → CONFIRMED; or absent → re-dispatch), proven by final - Tally state in the simulator AND on the licensed lab. Use an OS-agnostic +- crash mid-dispatch → restart → read-only reconciliation retains the + original batch. A matching full readback resolves the outcome; absent + or ambiguous evidence remains held for manual review. Assert that no + second write dispatch occurs, and verify the final Tally state in the + simulator AND on the licensed lab. Use an OS-agnostic crashpoint: a test-only injected panic/abort at the point between "outbox row committed" and "response parsed" is the primary mechanism (runs on the Windows matrix targets). Where an external process kill is used, it must be cross-platform — `taskkill /F /PID` on Windows, `kill -9` on POSIX — and the licensed-lab evidence must record the Windows result specifically, since the compatibility matrix targets Windows; -- duplicate re-dispatch with edited narration (key destroyed) is still - caught by the fingerprint check — "caught" meaning SURFACED FOR REVIEW, - not suppressed (deviation 2026-09-11, IMPROVEMENT_PLAN_2026H2 §8.19); a - run that passes this case by suppressing the second dispatch is - qualifying the defect; +- after an unknown outcome, an edited narration cannot trigger a second + write: reconciliation stays held if the original cannot be established. + Separately, a new legitimate payment with the same fingerprint is + SURFACED FOR REVIEW, never automatically suppressed (deviation + 2026-09-11, IMPROVEMENT_PLAN_2026H2 §8.19); exercise both cases so a + blanket dedupe rule cannot masquerade as safe recovery; - foreign writer interleaves between import and readback → LASTVCHID cross-check catches it (no false CONFIRM); - alter with concurrent foreign edit → MANUAL, never blind retry; @@ -700,8 +860,8 @@ Must still hold: cannot reach the import surface (the ReadOnlyProfile boundary in bridge-tally-read-transport is intact). 3. Allowlist default remains OFF; no migration flips existing companies on. -4. No automatic write retry beyond the bounded OutcomeUnknown probe path; - REJECTED (semantic) errors never auto-retry. +4. OutcomeUnknown probes are read-only and never authorize an automatic + resend. REJECTED (semantic) errors likewise never auto-retry. 5. Checkpoint/proof semantics: a write updates the mirror only through readback-confirmed state, never by assuming intent; snapshots and incremental scans reconcile Bridge-originated writes without double diff --git a/docs/tally/TALLY_PROTOCOL_REFERENCE.md b/docs/tally/TALLY_PROTOCOL_REFERENCE.md index bde016da7..29b66f31a 100644 --- a/docs/tally/TALLY_PROTOCOL_REFERENCE.md +++ b/docs/tally/TALLY_PROTOCOL_REFERENCE.md @@ -867,8 +867,13 @@ Read that precisely, because the obvious paraphrase — "Tally does not dedupe o number" — is false in two directions. Under **automatic** numbering the supplied number is *discarded* (§9.8), so the two sends never shared a stored voucher number and nothing could have deduped on it. Under **Manual + `PREVENTDUPLICATES=Yes`**, §9.8 records that a repeated number is -**cleanly rejected** — a qualified duplicate-rejection mechanism that a reader of this sentence -would otherwise never look for. +**cleanly rejected** — a qualified rejection that a reader of this sentence would otherwise never +look for. Qualified narrowly, though: §9.8 measured a **failed `Alter`**, and its own rule forbids +carrying that observation to a different request identity mechanism. A crash retry sends a +`Create`, which is **UNVERIFIED** here, as is the behaviour on any licensed SKU — §9.8's scope +clarification covers a licensed Journal `REMOTEID` repeat and says in terms that it establishes +neither voucher-number identity nor the configured numbering method. Do not read this sentence as +promising a crash-retry is safe under Manual numbering. So: on the numbering method measured here, a crash-retry duplicates client data unless the integrator prevents it. @@ -903,7 +908,9 @@ voucher by reusing a key. **Not the outbox, though.** `REMOTEID` prevents a duplicate; it does not tell you, after a crash, *what you sent*. The `REMOTEID` **attribute** does not echo the client key on readback (below), so -a resend is only safe while the exact key and payload are still on disk. Be precise about the +the exact key and payload must remain on disk for read-only reconciliation. They do not +authorize a resend after an unknown outcome; restart behaviour remains unqualified. See +`IMPLEMENTATION_GUIDE.md` §§3.3–3.5 and the playbook's held recovery flow. Be precise about the field: the key itself does survive anywhere Tally does not own — a narration marker comes back — and a categorical "Tally does not return the key" would send recovery work to discard the one attribution channel that works. The durable dispatch intent stays — see the @@ -1035,8 +1042,14 @@ assigned. **VERIFIED.** Re-sending an identical ledger `ACTION="Create"` returned `CREATED=0, ALTERED=1` with no error — the existing master was **overwritten** with the retry payload. -"No duplicate was made" is not the same as "my create succeeded." Pre-read before creating, -and persist `CREATED` and `ALTERED` as distinct outcomes. +The observed counters distinguish an alteration of an existing master from creation +of a new one. This experiment did not establish protection against a concurrent +foreign writer or recovery of an unobserved prior master. + +The required implementation workflow is maintained in +[Implementation Guide §3.6](IMPLEMENTATION_GUIDE.md#36-master-re-create-is-a-silent-alter) +and `PROMPT_PLAYBOOK.md` Phase 4 step 3a. This section records the gateway observation; +it does not grant dispatch authority from a pre-read. ### 9.4a A partial ledger `Alter` preserves the omitted Party GSTIN @@ -1120,7 +1133,10 @@ accepts(candidate, tally_name): # `tally_name` is the spelling Tally holds. Each line is one measured # result. Do not compose them; do not add a line without a capture. return candidate == tally_name # exact — VERIFIED - or ascii_lower(candidate) == ascii_lower(tally_name) # ASCII case — VERIFIED, see below + or candidate == ascii_lower(tally_name) # candidate is the master lowercased — VERIFIED + # NOT included: ascii_lower(candidate) == ascii_lower(tally_name). + # That also accepts an UPPERCASE candidate against a lowercase master, + # a direction never sent. See the third note below. or drop_one_trailing_space(candidate) == tally_name # ONE trailing space — VERIFIED or candidate == tally_name.replace("-", " ") # space for Tally's hyphen — VERIFIED ``` @@ -1133,10 +1149,13 @@ Three things this spelling is careful about, each of which was wrong in an earli - **The separator substitution is applied to `tally_name` only.** `tally_name="A-B"` accepts `candidate="A B"`; `tally_name="A B"` does **not** accept `candidate="A-B"`. That asymmetry is the entire point of the clause and is what a canonical form cannot express. -- **The case clause folds both sides, and that is broader than the capture.** The measurement sent - a lowercase name against a master carrying uppercase; the reverse was not sent. It is written - symmetrically because "Tally folds ASCII case" is the claim the capture supports, but a consumer - relying on the *uppercase-candidate* direction is relying on an inference. Qualify it before +- **The case clause is directional, because the capture was.** The measurement sent a **lowercase** + candidate against a master carrying uppercase. `ascii_lower(candidate) == ascii_lower(tally_name)` + also accepts an **uppercase** candidate against a lowercase master, which was never sent — so the + symmetric form asserts a second experiment, exactly as a canonical form does for the separator. + An earlier draft admitted that in this note and left the symmetric clause in the predicate + anyway; a qualification in the prose does not qualify the code beside it. Written as + `candidate == ascii_lower(tally_name)`, the predicate now says only what was sent. Qualify it before building on it. If a further direction is later measured, one clause is added and the table row changes. Until @@ -1154,14 +1173,14 @@ 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 here** — the reverse direction was never sent on this SKU. Measured **matched** on licensed 7.1, §9.4d | +| 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 Silver, §9.4d | | one trailing space ignored | **VERIFIED** | | **two or more** trailing spaces ignored | **UNVERIFIED** — only one was sent | -| *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 | +| *leading* whitespace ignored | **UNVERIFIED here**. Measured **matched** on licensed 7.1 Silver, §9.4d | +| runs of internal whitespace collapsed to one | **UNVERIFIED here** — only a single space was tested. Measured **matched** on licensed 7.1 Silver, §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 here**, and §9.4d splits it on licensed 7.1: `/` **matched**, underscore and en dash **rejected**. Not one row — do not fold them together | +| any other separator (underscore, en dash, `/`) treated as a space | **UNVERIFIED here**, and §9.4d splits it on licensed 7.1 Silver: `/` **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 @@ -1171,12 +1190,17 @@ result into them would silently widen the scope of a measurement nobody repeated **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. -`tally-matches-master-names-by-exact-codepoint` recorded it on 2026-08-19, TallyPrime 7.1, port -9001: a voucher naming a UI-created NFC ledger in its **canonically equivalent NFD** spelling was +`tally-matches-master-names-by-exact-codepoint` recorded the observation on +2026-08-19: a voucher naming a UI-created NFC ledger in its **canonically equivalent +NFD** spelling was rejected — `EXCEPTIONS=1`, `LINEERROR` saying the ledger does not exist — while the NFC spelling created it. A create with a programmatically-constructed NFD name returned `CREATED=1` and read -back with identical NFD codepoints, so storage is verbatim too. **Tally matches on exact -codepoints.** A fold that normalises before comparing therefore resolves a name onto a master Tally +back with identical NFD codepoints, so storage is verbatim too. **The observed +instance matched these spellings by exact codepoints.** The checked-in encoding +provenance records only a TallyPrime EDU instance and date; it does not establish +release, port, or standard-versus-Edit-Log product identity for this observation. +Those classifications remain **UNVERIFIED**. This row therefore qualifies neither +§0's Edit Log 7.0 baseline nor any licensed SKU. A fold that normalises before comparing therefore resolves a name onto a master Tally itself keeps apart — the precise failure this section exists to prevent. **Why it needs saying twice.** This bug shipped, and the fold was then audited against this section @@ -1287,7 +1311,7 @@ voucher each, then the **day book was read back** to record which master each vo 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 | +| Supplied against a live master | Licensed 7.1 Silver | §9.4b on Educational | | --- | --- | --- | | exact | **matched** | matched | | ASCII lowercase | **matched** | matched | @@ -1319,7 +1343,7 @@ ledger present in every company: | `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 +So §9.4b's abbreviation findings hold on licensed 7.1 Silver 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.** @@ -1354,10 +1378,15 @@ hyphen and whitespace cases, and the rejected en dash, underscore, abbreviation, NFD cases are observations of this one operation. They do not generalize across product, tier, object class, direction, or caller. +> **RULE: use only the recorded directional alternatives; do not fold separators into a canonical +> form.** The slash result is candidate `/` against master space, not the reverse. An en dash and +> an underscore were rejected in their recorded directions, 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. +a reader would fold too much, it was that "normalises separators" hides both the particular +substitutions and their directions. Nothing about a separator's appearance predicts which comparison +the gateway accepts. **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 @@ -1401,10 +1430,12 @@ habits, not against real operator input. ### 9.5 Identity after write -**VERIFIED.** `LASTMID` is **0** on successful master creates — unusable for master identity; -read masters back by normalised name. `LASTVCHID` is populated for vouchers and usable, -subject to a foreign-writer cross-check. `LASTVCHID` also accepts non-numeric text without -error when parsed back, so validate it. +**VERIFIED.** `LASTMID` is **0** on successful master creates, so this counter does not +identify the created master. `LASTVCHID` is populated for vouchers. Non-numeric +`LASTVCHID` text is also accepted without error when parsed back. + +For the implementation's readback identity policy, see `IMPLEMENTATION_GUIDE.md` §3.5 +and `PROMPT_PLAYBOOK.md` Phase 4 step 4. Their prescriptions are separate from this observation. --- @@ -1420,7 +1451,11 @@ voucher number survives and how a failed Alter behaves. Two consequences, both significant: -1. **Voucher-number-based idempotency only works with Manual numbering.** Under automatic +1. **What was measured is the FAILED-ALTER column, and the consequences below are about that + column.** §9.8 sent a failed `Alter`; it did not test a `Create` retry, a restart, or another + voucher type, and its own rule below forbids carrying the observation to a different request + identity mechanism. Read "idempotency" here as "this failure mode, under this setting". + Voucher-number-based idempotency in that sense only works with Manual numbering. Under automatic numbering the client-supplied number is thrown away, so any dedupe key built on it is silently ineffective. This was not obvious — the create returned `CREATED=1, ERRORS=0` and looked entirely successful. diff --git a/docs/tally/compatibility/compatibility-matrix.json b/docs/tally/compatibility/compatibility-matrix.json index 835c55ac2..999e3068f 100644 --- a/docs/tally/compatibility/compatibility-matrix.json +++ b/docs/tally/compatibility/compatibility-matrix.json @@ -1,7 +1,7 @@ { "schema_version": 1, "bridge_commit_sha": "be1c20cc3fd66fa1ece196505c69f26e555e4b8e", - "compatibility_surface_sha256": "f0e1a2c88e5905abe06daccf580611d011b11dc3f126fe799b74ea779f7fcd05", + "compatibility_surface_sha256": "7e5b43aab5656f08d3a477073c408857f3e9147687e9355342347e99e1f76f4d", "claims": [ { "claim_id": "erp9-6-6-3-windows-education-xml-one-company", diff --git a/docs/tally/compatibility/compatibility-surface.json b/docs/tally/compatibility/compatibility-surface.json index c8da4d7bb..b4d8405b9 100644 --- a/docs/tally/compatibility/compatibility-surface.json +++ b/docs/tally/compatibility/compatibility-surface.json @@ -35,7 +35,7 @@ }, { "path": "docs/tally/TALLY_PROTOCOL_REFERENCE.md", - "sha256": "2a115ee06c8f26caae7f8fb703e709599f3ca01dc87c6484bb0ec88b1ff870dd" + "sha256": "095c325b1daf6cb5ec888394e35bb4c5ba9d7d2563d0f417a7173ca5854c441d" }, { "path": "docs/tally/compatibility/README.md", @@ -850,5 +850,5 @@ "sha256": "a8ac2714fecf51947f2822c8c46d7ce2e8602c732780ff60566a7771f0836f9a" } ], - "manifest_sha256": "f0e1a2c88e5905abe06daccf580611d011b11dc3f126fe799b74ea779f7fcd05" + "manifest_sha256": "7e5b43aab5656f08d3a477073c408857f3e9147687e9355342347e99e1f76f4d" } \ No newline at end of file