Part of #65 (phase 1 — phase 2 products/contracts remains, so no Closes).
Deliverables
scripts/thirdparty-update.sh <socid> <json-file-or-inline> — PUT /thirdparties/{id} through the host-guarded dol-write.sh. Allowlist (identity/address/typology/notes/idprof1-6/tva_intra — the ledger grammar's "thirdparty complete"); any other field is refused pre-PUT, exit non-zero, offender named. Read-back contract: GET before → PUT → GET after, stdout = per-field diff of the requested fields that changed; a requested field that didn't take ⇒ non-zero (no silent partial apply). Also accepts the promote stdin form {"socid":N,"fields":{…}}.
scripts/contact-create.sh — POST /contacts, born idempotent (the #44 pattern): dedupes by case-insensitive email, then (lastname, firstname) → {"id": existing, "deduped": true} without POSTing. Tolerates exactly the Dolibarr empty-list HTTP 404; any other listing failure aborts rather than minting duplicates. Refuses unknown fields — including the WIP payloads' soc2 (→ poste).
Promote opsthirdparty_update + contact wired into promote-plan.sh (one-line summaries) and promote-apply.sh (both targets; per-op output now surfaces changed=N […] / deduped=true so idempotency is visible in the apply log). Sandbox target honors a pre-set DOL_WRITE for the offline stub hook; prod never inherits — always the gated dol-prod-write.sh.
Offline tests (tests/run-tests.sh, stub extended per-endpoint): allowlist rejection pre-PUT, contact dedupe (case-insensitive, no POST) + soc2 refusal, happy path of both ops through promote-apply --target sandbox applied twice (run 2 = changed=0 (no-op) + deduped=true). Zero credentials, zero network.
KM manifest packreplay-packs/2026-07-15-km-dossier/ — socid 1 dossier from the operator payloads with the truth fix (contract unsigned: effect-dated 2026-04-23, eIDAS signature in progress; USD invoicing; 4 % window = 6 months post-launch; « US EIN : à collecter (→ idprof1) ») + contact Hendrik Rootering (COO). README documents provenance + the human-gated prod step.
SKILL.md — workflows 5 (dossier update) + 6 (contact), promote op-kind list, three new gotchas; removed the stale "SKIP for credit notes" frontmatter clause (workflow 4 shipped earlier).
Offline test results
OK: payment-record normalization tests passed (long→short, wise untouched, >50 refused pre-POST)
OK: thirdparty-update allowlist — code_client refused pre-PUT, offender named
OK: contact-create dedupe — email match returns existing id, no POST; soc2 refused → poste
OK: promote-apply happy path — run 1 applies (changed=2, contact id 88), run 2 is a no-op (changed=0, deduped)
OK: all offline tests passed
(The sandbox fiche already carried the WIP payload, so the one-field diff is the truth fix.)
Run 2 (idempotency)
>>> promote-apply target=sandbox (writes via dol-write.sh)
[1/2] thirdparty_update @km -> id=1 changed=0 (no-op)
[2/2] contact @hendrik -> id=3 deduped=true (already on target — no write)
OK — promote complete. ref -> id: {"km": 1, "hendrik": 3}
Read-back: all 13 dossier fields correct; note_public carries « SIGNATURE eIDAS EN COURS », « US EIN : à collecter », « FACTURÉ EN USD »; contact 3 = Rootering Hendrik / COO / [email protected] (poste set).
Prod promote is NOT part of this PR — orchestrator's human-gated replay (env-only key + ARCO_PROMOTE_CONFIRM), then dolibarr-thirdparty-completeness re-run on socid 1 should show only the EIN gap.
Part of #65 (phase 1 — phase 2 products/contracts remains, so no `Closes`).
## Deliverables
- **`scripts/thirdparty-update.sh <socid> <json-file-or-inline>`** — PUT `/thirdparties/{id}` through the host-guarded `dol-write.sh`. **Allowlist** (identity/address/typology/notes/idprof1-6/tva_intra — the ledger grammar's "thirdparty complete"); any other field is refused pre-PUT, exit non-zero, offender named. Read-back contract: GET before → PUT → GET after, stdout = per-field diff of the requested fields that changed; a requested field that didn't take ⇒ non-zero (no silent partial apply). Also accepts the promote stdin form `{"socid":N,"fields":{…}}`.
- **`scripts/contact-create.sh`** — POST `/contacts`, **born idempotent** (the #44 pattern): dedupes by case-insensitive email, then (lastname, firstname) → `{"id": existing, "deduped": true}` without POSTing. Tolerates exactly the Dolibarr empty-list HTTP 404; any other listing failure aborts rather than minting duplicates. Refuses unknown fields — including the WIP payloads' `soc2` (→ `poste`).
- **Promote ops** `thirdparty_update` + `contact` wired into `promote-plan.sh` (one-line summaries) and `promote-apply.sh` (both targets; per-op output now surfaces `changed=N […]` / `deduped=true` so idempotency is visible in the apply log). Sandbox target honors a pre-set `DOL_WRITE` for the offline stub hook; **prod never inherits** — always the gated `dol-prod-write.sh`.
- **Offline tests** (`tests/run-tests.sh`, stub extended per-endpoint): allowlist rejection pre-PUT, contact dedupe (case-insensitive, no POST) + `soc2` refusal, happy path of both ops through `promote-apply --target sandbox` applied **twice** (run 2 = `changed=0 (no-op)` + `deduped=true`). Zero credentials, zero network.
- **KM manifest pack** `replay-packs/2026-07-15-km-dossier/` — socid 1 dossier from the operator payloads with the **truth fix** (contract *unsigned*: effect-dated 2026-04-23, eIDAS signature in progress; USD invoicing; 4 % window = 6 months post-launch; « US EIN : à collecter (→ idprof1) ») + contact Hendrik Rootering (COO). README documents provenance + the human-gated prod step.
- **SKILL.md** — workflows 5 (dossier update) + 6 (contact), promote op-kind list, three new gotchas; removed the stale "SKIP for credit notes" frontmatter clause (workflow 4 shipped earlier).
## Offline test results
```
OK: payment-record normalization tests passed (long→short, wise untouched, >50 refused pre-POST)
OK: thirdparty-update allowlist — code_client refused pre-PUT, offender named
OK: contact-create dedupe — email match returns existing id, no POST; soc2 refused → poste
OK: promote-apply happy path — run 1 applies (changed=2, contact id 88), run 2 is a no-op (changed=0, deduped)
OK: all offline tests passed
```
## Sandbox rehearsal (LIVE, 2026-07-15)
`promote-plan` renders both ops; then:
**Run 1**
```
>>> promote-apply target=sandbox (writes via dol-write.sh)
[1/2] thirdparty_update @km -> id=1 changed=1 [note_public]
[2/2] contact @hendrik -> id=3
OK — promote complete. ref -> id: {"km": 1, "hendrik": 3}
```
(The sandbox fiche already carried the WIP payload, so the one-field diff **is** the truth fix.)
**Run 2 (idempotency)**
```
>>> promote-apply target=sandbox (writes via dol-write.sh)
[1/2] thirdparty_update @km -> id=1 changed=0 (no-op)
[2/2] contact @hendrik -> id=3 deduped=true (already on target — no write)
OK — promote complete. ref -> id: {"km": 1, "hendrik": 3}
```
Read-back: all 13 dossier fields correct; `note_public` carries « SIGNATURE eIDAS EN COURS », « US EIN : à collecter », « FACTURÉ EN USD »; contact 3 = Rootering Hendrik / COO / [email protected] (`poste` set).
**Prod promote is NOT part of this PR** — orchestrator's human-gated replay (env-only key + `ARCO_PROMOTE_CONFIRM`), then `dolibarr-thirdparty-completeness` re-run on socid 1 should show only the EIN gap.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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]>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of #65 (phase 1 — phase 2 products/contracts remains, so no
Closes).Deliverables
scripts/thirdparty-update.sh <socid> <json-file-or-inline>— PUT/thirdparties/{id}through the host-guardeddol-write.sh. Allowlist (identity/address/typology/notes/idprof1-6/tva_intra — the ledger grammar's "thirdparty complete"); any other field is refused pre-PUT, exit non-zero, offender named. Read-back contract: GET before → PUT → GET after, stdout = per-field diff of the requested fields that changed; a requested field that didn't take ⇒ non-zero (no silent partial apply). Also accepts the promote stdin form{"socid":N,"fields":{…}}.scripts/contact-create.sh— POST/contacts, born idempotent (the #44 pattern): dedupes by case-insensitive email, then (lastname, firstname) →{"id": existing, "deduped": true}without POSTing. Tolerates exactly the Dolibarr empty-list HTTP 404; any other listing failure aborts rather than minting duplicates. Refuses unknown fields — including the WIP payloads'soc2(→poste).thirdparty_update+contactwired intopromote-plan.sh(one-line summaries) andpromote-apply.sh(both targets; per-op output now surfaceschanged=N […]/deduped=trueso idempotency is visible in the apply log). Sandbox target honors a pre-setDOL_WRITEfor the offline stub hook; prod never inherits — always the gateddol-prod-write.sh.tests/run-tests.sh, stub extended per-endpoint): allowlist rejection pre-PUT, contact dedupe (case-insensitive, no POST) +soc2refusal, happy path of both ops throughpromote-apply --target sandboxapplied twice (run 2 =changed=0 (no-op)+deduped=true). Zero credentials, zero network.replay-packs/2026-07-15-km-dossier/— socid 1 dossier from the operator payloads with the truth fix (contract unsigned: effect-dated 2026-04-23, eIDAS signature in progress; USD invoicing; 4 % window = 6 months post-launch; « US EIN : à collecter (→ idprof1) ») + contact Hendrik Rootering (COO). README documents provenance + the human-gated prod step.Offline test results
Sandbox rehearsal (LIVE, 2026-07-15)
promote-planrenders both ops; then:Run 1
(The sandbox fiche already carried the WIP payload, so the one-field diff is the truth fix.)
Run 2 (idempotency)
Read-back: all 13 dossier fields correct;
note_publiccarries « SIGNATURE eIDAS EN COURS », « US EIN : à collecter », « FACTURÉ EN USD »; contact 3 = Rootering Hendrik / COO / [email protected] (posteset).Prod promote is NOT part of this PR — orchestrator's human-gated replay (env-only key +
ARCO_PROMOTE_CONFIRM), thendolibarr-thirdparty-completenessre-run on socid 1 should show only the EIN gap.🤖 Generated with Claude Code