feat(write-skill): idempotency keys — manifest replay must be a no-op #44

Closed
opened 2026-07-11 18:17:29 +02:00 by arcodange · 1 comment
Owner

Part of the AI back-office PRDchallenge C2 + atom-contract idempotency_key.

Context. Learning #4 of the 2026-07-11 rehearsal: manifest B failed mid-run (varchar(50), fixed by erp#37) and could NOT be safely re-applied — op 1 (invoice create) had already run; a replay would have duplicated the DARNIS invoice. A partial failure currently forces manual manifest surgery.

Deliverables

  • invoice-create.sh: pre-POST dedupe lookup — supplier kind by (socid, ref_supplier); customer kind by (socid, date, total_ttc, line fingerprint). Existing match → return the existing id with "deduped": true instead of creating.
  • payment-record.sh: dedupe by (invoice, amount, normalized transaction_id) against the invoice's payment list.
  • thirdparty-create.sh: dedupe by exact name (abort on ambiguous, per the promote lookup semantics).
  • Replay test: apply a manifest twice on a sandbox checkpoint → second run reports all-deduped, zero new rows.

Acceptance: the replay test passes; promote-apply output distinguishes created vs deduped ops.


Execution — Blocked by: none · Blocks: #47, #52 · Human gates: none.
Start: claude --worktree idempotency-keys from the erp repo root; read the three write scripts + the erp#37 tx-id normalization (merged) they must compose with; replay fixtures available in the preserved replay pack.
Done means: evidence comment here (double-apply output: run 1 created / run 2 all-deduped) → PR merged with Closes #44.

Part of the **AI back-office PRD** — [challenge C2](https://gitea.arcodange.lab/arcodange-org/factory/src/branch/main/vibe/PRD/ai-back-office/challenges.md) + atom-contract `idempotency_key`. **Context.** Learning #4 of the 2026-07-11 rehearsal: manifest B failed mid-run (varchar(50), fixed by erp#37) and could NOT be safely re-applied — op 1 (`invoice` create) had already run; a replay would have duplicated the DARNIS invoice. A partial failure currently forces manual manifest surgery. **Deliverables** - `invoice-create.sh`: pre-POST dedupe lookup — supplier kind by (socid, `ref_supplier`); customer kind by (socid, date, total_ttc, line fingerprint). Existing match → return the existing id with `"deduped": true` instead of creating. - `payment-record.sh`: dedupe by (invoice, amount, normalized transaction_id) against the invoice's payment list. - `thirdparty-create.sh`: dedupe by exact name (abort on ambiguous, per the promote lookup semantics). - Replay test: apply a manifest twice on a sandbox checkpoint → second run reports all-deduped, zero new rows. **Acceptance:** the replay test passes; `promote-apply` output distinguishes created vs deduped ops. --- **Execution** — Blocked by: none · Blocks: #47, #52 · Human gates: none. **Start:** `claude --worktree idempotency-keys` from the erp repo root; read the three write scripts + the erp#37 tx-id normalization (merged) they must compose with; replay fixtures available in the preserved replay pack. **Done means:** evidence comment here (double-apply output: run 1 created / run 2 all-deduped) → PR merged with `Closes #44`.
arcodange added this to the P1 — Flagship pipeline milestone 2026-07-11 18:17:29 +02:00
Author
Owner

Evidence — live double-apply on the sandbox (2026-07-18), acceptance PASSED.

Checkpoint status checked first (ARMED, ai_agent_sandbox id=4); no checkpoint refresh run. tests/replay-idempotency.sh (committed) builds a self-contained manifest (thirdparty + validated supplier invoice + Qonto-style-tx payment) and applies it twice:

-- RUN 1: expect 3 created --
  [1/3] thirdparty  @tp  -> id=13  created
  [2/3] invoice     @inv -> id=30  created
  [3/3] payment          -> id=17  created
OK — promote complete (3 created). ref -> id: {"tp": 13, "inv": 30}
row counts after run 1: thirdparties=1 invoices=1 payments=1
-- RUN 2 (same manifest): expect 3 deduped, zero new rows --
  [1/3] thirdparty  @tp  -> id=13  deduped=true (already on target — no write)
  [2/3] invoice     @inv -> id=30  deduped=true (already on target — no write)
  [3/3] payment          -> id=-   deduped=true (already on target — no write)
OK — promote complete (3 deduped). ref -> id: {"tp": 13, "inv": 30}
row counts after run 2: thirdparties=1 invoices=1 payments=1
PASS — stored payment num verified in erp#37 short form

Closing proof: the actual 2026-07-11 manifest-B-suppliers.json — the manifest Learning #4 declared unreplayable — now replays on the sandbox as 5/5 deduped, zero new rows (DARNIS invoice → id 29, all four payments matched by normalized tx). The runbook's Learning #4 carries a dated resolution addendum.

Offline suite: 11/11 green (dedupe hits POST nothing; conflicts/ambiguity/role-mismatch abort pre-POST; long-form historical tx nums dedupe; draft convergence validates).

Dedupe keys shipped: thirdparty = exact name (ambiguous aborts) · supplier invoice = (socid, ref_supplier), conflict on total aborts · customer invoice = (socid, date, total_ttc ±0.02, line fingerprint) · payment = (invoice, amount, normalized transaction_id). Known holes documented in SKILL.md: payments without tx id have no dedupe key (warned, plan flags tx=MISSING); creditnote-create.sh out of scope — follow-up issue candidate.

Sandbox fixtures left behind (tp 13 / inv 30 / pay 17 + the manifest-B replay rows) — disposable, reclaimed by the next human-run checkpoint refresh.

Shipped in PR #70. Built by a Claude subagent lane; reviewed + merged by the session orchestrator under the operator's in-session delegation.

**Evidence — live double-apply on the sandbox (2026-07-18), acceptance PASSED.** Checkpoint status checked first (ARMED, `ai_agent_sandbox` id=4); no `checkpoint refresh` run. `tests/replay-idempotency.sh` (committed) builds a self-contained manifest (thirdparty + validated supplier invoice + Qonto-style-tx payment) and applies it twice: ``` -- RUN 1: expect 3 created -- [1/3] thirdparty @tp -> id=13 created [2/3] invoice @inv -> id=30 created [3/3] payment -> id=17 created OK — promote complete (3 created). ref -> id: {"tp": 13, "inv": 30} row counts after run 1: thirdparties=1 invoices=1 payments=1 -- RUN 2 (same manifest): expect 3 deduped, zero new rows -- [1/3] thirdparty @tp -> id=13 deduped=true (already on target — no write) [2/3] invoice @inv -> id=30 deduped=true (already on target — no write) [3/3] payment -> id=- deduped=true (already on target — no write) OK — promote complete (3 deduped). ref -> id: {"tp": 13, "inv": 30} row counts after run 2: thirdparties=1 invoices=1 payments=1 PASS — stored payment num verified in erp#37 short form ``` **Closing proof:** the actual 2026-07-11 `manifest-B-suppliers.json` — the manifest Learning #4 declared unreplayable — now replays on the sandbox as **5/5 deduped, zero new rows** (DARNIS invoice → id 29, all four payments matched by normalized tx). The runbook's Learning #4 carries a dated resolution addendum. Offline suite: 11/11 green (dedupe hits POST nothing; conflicts/ambiguity/role-mismatch abort pre-POST; long-form historical tx nums dedupe; draft convergence validates). Dedupe keys shipped: thirdparty = exact name (ambiguous aborts) · supplier invoice = (socid, ref_supplier), conflict on total aborts · customer invoice = (socid, date, total_ttc ±0.02, line fingerprint) · payment = (invoice, amount, normalized transaction_id). Known holes documented in SKILL.md: payments without tx id have no dedupe key (warned, plan flags `tx=MISSING`); `creditnote-create.sh` out of scope — follow-up issue candidate. Sandbox fixtures left behind (tp 13 / inv 30 / pay 17 + the manifest-B replay rows) — disposable, reclaimed by the next human-run checkpoint refresh. Shipped in PR #70. Built by a Claude subagent lane; reviewed + merged by the session orchestrator under the operator's in-session delegation.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: arcodange-org/erp#44