Commit Graph
12 Commits
Author SHA1 Message Date
arcodangeandClaude Fable 5 3840e74dcd feat(write-skill): idempotency keys — manifest replay is a no-op (erp#44)
Learning #4 of the 2026-07-11 rehearsal: manifest B failed mid-run and could
not be re-applied — op 1 (the DARNIS invoice) had already run and a replay
would have duplicated it. Every write op now dedupes BEFORE any POST:

- thirdparty-create.sh: by exact name (promote '#thirdparty:name=' semantics);
  ambiguous (2+) aborts; an existing fiche missing the requested role aborts
  (refuse-never-repair). Emits {"id", "deduped"} instead of a bare id.
- invoice-create.sh: supplier kind by (socid, ref_supplier) — same key with a
  different total aborts as a conflict; customer kind (or supplier without
  ref_supplier) by (socid, date, total_ttc ±0.02, line fingerprint) with descs
  HTML-unescaped. Credit notes are never candidates. A deduped DRAFT with
  validate:true is validated on replay, so an interrupted run converges.
- payment-record.sh: by (invoice, amount, normalized transaction_id), composing
  with the erp#37 varchar(50) normalization on BOTH sides so historical
  long-form nums still match; same tx + different amount aborts; without a tx
  id there is no dedupe key (warned). Dedupe answers id:null (the payments list
  exposes no paiement rowid) + the existing bank line.
- All three refuse to POST blind when the dedupe lookup fails with anything but
  the documented empty-list 404 (the voir_tous trap would otherwise mint dupes).
- promote-apply.sh: marks each op created / deduped=true inline and totals them
  in the summary — an all-deduped second run is visible proof of a no-op.
- promote-plan.sh: advertises each op's dedupe key (and flags tx=MISSING as
  'a replay WILL double-pay').

Proof:
- tests/run-tests.sh: 5 new offline cases (11 total) — dedupe hits POST
  nothing, conflicts/ambiguity abort pre-POST, long-form history dedupes,
  draft convergence validates; stub extended to serve the new lookups with the
  live-observed empty behaviors ([] for invoices/payments, 404 for tiers).
- tests/replay-idempotency.sh (new, live): double-applies a self-contained
  manifest on the sandbox — run 1 '3 created' (rows 1/1/1), run 2 '3 deduped'
  with row counts unchanged and the stored num in erp#37 short form.
- The historic manifest-B now replays on the sandbox as 5/5 deduped, zero new
  rows — the exact replay Learning #4 declared impossible.

SKILL.md updated in the same change (per-op dedupe keys, replay-safety section,
gotchas); the 2026-07-11 runbook's Learning #4 carries a dated resolution
addendum.

Closes erp#44.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-07-18 23:46:05 +02:00
arcodangeandClaude Fable 5 35b227eb6d feat(write-skill): client-dossier ops — thirdparty update (allowlisted) + idempotent contacts
Part of erp#65 (phase 1). Ledger grammar "thirdparty complete" gets its
op: allowlisted non-ledger fields, per-field diff read-back. Contacts
are born idempotent (dedupe by email then name). Promote ops wired both
targets, offline stub tests, SKILL.md workflows, KM dossier manifest
(unsigned-contract truth fix + EIN-to-collect note).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-15 19:49:13 +02:00
arcodange 0d66d6a6dc Merge pull request 'fix(txid): normalize bank tx ids to fit Dolibarr's num_payment varchar(50)' (#37) from arcodange/txid-varchar50-normalize into main
Reviewed-on: #37
2026-07-11 18:05:55 +02:00
arcodangeandClaude Fable 5 f9d83037b6 fix(txid): normalize bank tx ids to fit Dolibarr's num_payment varchar(50)
Qonto transaction ids run ~67 chars (<org>-<n>-<n>-transaction-<uuid>) but
Dolibarr stores num_payment in varchar(50) (llx_paiement.num_paiement,
llx_paiementfourn.num_paiement) — POSTing a payment with the raw id fails
HTTP 400 "value too long for type character varying(50)". Parade proven live
on the sandbox (2026-07-11): store the UUID suffix (globally unique, ~37
chars). Wise ids (short numerics) are unaffected.

Writer side — payment-record.sh strips everything through "transaction-"
before POST, announces the normalization on stderr, REFUSES (never truncates)
ids still >50 chars after normalization, and emits the normalized num in the
output JSON.

Reader side — bank-match.sh PASS 0 (exact tx-id, erp#28) now compares BOTH
sides in raw AND canonical short form: Qonto feed ids are carried long+short,
payment nums are normalized on compare — so nums stored short (the varchar(50)
form) and historical long-form nums both keep matching. Wise ids untouched.

Proven offline (no credentials, no network, no sandbox/prod writes):
- arcodange-bank-reco/tests/run-tests.sh — new bank-match --fixtures offline
  mode: long feed id ↔ short num, long ↔ long (back-compat), Wise numeric,
  each Δ+19d outside the ±7d window so only PASS 0 can pair them (exit 0,
  3×[tx-id]); plus the empty-num negative (exit 1, 0 matched).
- dolibarr-sandbox-write/tests/run-tests.sh — payment-record via a stubbed
  dol-write.sh (DOL_WRITE hook): long→short in POST body + output JSON, Wise
  untouched, >50-after-normalization refused BEFORE any POST, citing
  varchar(50).

Both SKILL.md document the canonical short form + the varchar(50) constraint.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-11 17:48:35 +02:00
arcodangeandClaude Fable 5 0240c519b1 fix(provision): grant user lire (251) to ai_agent_sandbox so the armed probe passes
The checkpoint status/relink-env armed probe calls GET /users/info, which
requires Dolibarr right 251 (user->user->lire). WRITE_IDS didn't include it,
so a freshly provisioned agent answered 403 on the probe — reported NOT armed
— while its key actually authenticates (GET /thirdparties -> 200). Right 251
was granted live in SQL on the sandbox (fk_user=4) today; this persists it in
WRITE_IDS so every future provision grants it.

Also teach both probes to tell the failure modes apart instead of one opaque
message: 401 = key rejected (stale/instance-encrypted -> re-provision),
403 = key OK but right 251 missing (-> grant it / re-provision), 200 = armed.
Docs updated accordingly (checkpoint SKILL.md probe outcomes, sandbox-write
SKILL.md gotcha, test/README.md rights table synced to WRITE_IDS incl. 262/111).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-11 17:38:30 +02:00
arcodangeandClaude Opus 4.7 b945c8de47 feat(payment): first-class transaction_id when recording a règlement
Make the originating bank transaction id a first-class input on payment-record.sh
so every règlement is tied to the real bank movement at write time.

- `transaction_id` is the canonical field (the Qonto/Wise feed tx id); `num` stays
  as a back-compat alias. It's stored on the payment's bank line (llx_bank.num_chq),
  the reconciliation key.
- Recording WITHOUT a transaction_id prints a stderr warning (still posts, but won't
  auto-reconcile) — nudges the agent to always carry it.
- Output normalises to {id, bank_transaction_id, transaction_id}.
- Promote: manifests' payment ops carry transaction_id; promote-plan shows it
  (tx=… or tx=MISSING).

Proven live: customer + supplier record with transaction_id; the `num` alias maps
to the same field; the no-tx warning fires; promote plan/apply carry it through.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-30 00:34:10 +02:00
arcodangeandClaude Opus 4.7 e748efd8f0 feat(payment): return the bank transaction id on règlements (reconciliation link)
A payment only returned its paiement id, which isn't what bank reconciliation
keys on. payment-record.sh now emits {id, bank_transaction_id, num}:

- bank_transaction_id = the Dolibarr bank line (llx_bank.fk_bank_line) the payment
  created, resolved via GET /{invoices|supplierinvoices}/{id}/payments (correlated
  by num, else the most recent line). Works for customer and supplier.
- num stores the originating bank tx id (Qonto/Wise) and lands on that bank line's
  num_chq — so arcodange-bank-reco can match a règlement to a statement line by id
  instead of fuzzy amount/date. Both ends captured at write time.

Proven live: customer {id:13,bank_transaction_id:35,num:QONTO-TX-1234},
supplier {id:16,bank_transaction_id:36,num:WISE-TX-5678}; llx_bank rows 35/36
carry the refs in num_chq. promote-apply still extracts .id unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-30 00:17:41 +02:00
arcodangeandClaude Opus 4.7 64d2cb4237 feat(skills,cli): supplier avoirs + banque lire (bank-account discovery)
The two V9 follow-ups, both proven live on the sandbox.

- creditnote-create.sh: `kind:"supplier"` makes an avoir fournisseur on
  /supplierinvoices (type=2 + fk_facture_source, carries ref_supplier); default
  customer path unchanged. Proven: customer AVC002 (-240) + supplier AVF2026001
  (-144, ref_supplier carried, linked to source, validated).
- bank-accounts.sh + `arcodange sandbox accounts`: list bank accounts (id/label/
  bank) so a payment can pick its account_id. Needs `banque lire` (rights 111),
  now added to the provisioner's WRITE_IDS so fresh runs include it; the existing
  ai_agent_sandbox user was granted it live. GET /bankaccounts now returns the 3
  accounts (QONTO, WISE EURO, Compte Courant Asso).
- SKILL.md: supplier-avoir example + accounts helper + updated banque-lire note.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-30 00:07:48 +02:00
arcodangeandClaude Opus 4.7 04985fe15c feat(promote): resolve pre-existing entities by business key (#entity:field=value)
Closes the last promote gap: a manifest can now reference records it does NOT
create. A value like "#thirdparty:name=KissMetrics" (or :code=CL0007) is looked
up on the TARGET at apply time and resolved to that target's id — so the same
manifest is portable (sandbox id on --target sandbox, prod id on --target prod).

promote-apply.sh: resolve() gains a "#" branch + a lookup() helper that queries
the target via the GET wrapper with sqlfilters. Supports thirdparty
(name/code/supplier_code) and invoice/supplierinvoice (ref/ref_supplier). A
lookup matching nothing OR more than one record ABORTS the run — it never
guesses, so it cannot write to the wrong entity.

Proven live: "#thirdparty:name=ACME Conseil" resolved to the existing client and
invoiced it; a not-found code and an ambiguous (2-match) name both aborted with
exit 1. Combined with @refs, arbitrary self-contained-or-referential change-sets
now replay cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 23:57:49 +02:00
arcodangeandClaude Opus 4.7 00d86b47a3 feat(skills,cli): promote-to-prod replay (ADR-0003 capstone) + supplier payment fix
The human-gated path that carries a reviewed sandbox change to prod.

- promote-plan.sh: render a manifest (JSON array of write ops with symbolic @refs
  instead of ids — portable sandbox->prod) as a human-readable change-set.
- promote-apply.sh <manifest> --target sandbox|prod: replay it, resolving each
  @ref to the id actually created during the run (dependent ops wire up). sandbox
  rehearses via dol-write.sh; prod via dol-prod-write.sh.
- dol-prod-write.sh: the ONLY prod-write path. Prod key read from the ENVIRONMENT
  only (DOLIBARR_PROD_WRITE_KEY, never a stored .env); every write refused unless
  ARCO_PROMOTE_CONFIRM=I-UNDERSTAND-THIS-WRITES-PROD.
- create scripts take a DOL_WRITE override so promote-apply reuses them per target.
- bin/arcodange: `promote {plan|apply}` group + example manifest.
- payment-record.sh: fixed supplier payments (payment_mode_id + closepaidinvoices).

Proven live: plan renders; apply --target sandbox replays a 3-op chain with refs
resolved (@tp1->id, invoice socid=@tp1, payment invoice=@inv1); --target prod
without the confirm flag is REFUSED before sending. Supplier payment now works
end-to-end via the script.

Limitation (documented): manifests reference entities they create (@ref);
pre-existing prod entities need business-key resolution (follow-up).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 23:48:47 +02:00
arcodangeandClaude Opus 4.7 79286650d7 feat(skills,cli): sandbox avoir (credit note) + arcodange sandbox CLI group
- dolibarr-sandbox-write/scripts/creditnote-create.sh: create a customer avoir
  (credit note) — a customer invoice type=2 referencing source_invoice
  (fk_facture_source); amounts negative, validates to an AVC… ref. Proven live.
- bin/arcodange: new `sandbox` command group wiring the write scripts —
  `arcodange sandbox {thirdparty|invoice|payment|creditnote|write}` (JSON on
  stdin). Header + usage updated to note the CLI now does host-guarded sandbox
  writes (still read-only on prod).
- SKILL.md: avoir workflow + CLI notes.

Verified end-to-end through the CLI: thirdparty -> invoice (FAC…) -> avoir
(AVC…, total_ttc -240, fk_facture_source set); host-guard intact via the CLI.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 21:04:49 +02:00
arcodangeandClaude Opus 4.7 d2e8b3a3a4 feat(skills): dolibarr-sandbox-write — host-guarded write skill (V9)
The write-capable companion to the read-only dolibarr* skills, scoped to the
erp-sandbox. Lets an AI agent rehearse bookkeeping writes against a copy of prod
(ADR-0003) before a human promotes the reviewed change to prod.

- scripts/dol-write.sh: write wrapper that REFUSES any host that is not
  erp-sandbox.arcodange.lab (the structural prod-safety guarantee) using the
  ai_agent_sandbox key from a gitignored .env.
- scripts/thirdparty-create.sh: create client/supplier fiches; codes auto-assign
  via the elephant mask (code="-1").
- scripts/invoice-create.sh: customer (/invoices) or supplier (/supplierinvoices)
  invoices with product/service lines + ref_supplier, optional validate.
- scripts/payment-record.sh: record a règlement (VIR/CB/CHQ/LIQ); customer pays
  full + marks paid, supplier needs an amount.
- SKILL.md (safety model + workflows + the human-gated promote flow), .env.example,
  example input.

Proven end-to-end live against the sandbox: client -> invoice (service+product
lines, HT 1100 / TTC 1320) -> validate -> payment (paid); supplier -> supplier
invoice (ref_supplier carried) -> validate. Host guard verified to refuse a prod
URL before sending.

Avoirs (credit notes) and bin/arcodange CLI wiring are planned follow-ups.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-06-29 20:49:31 +02:00