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]>
This commit is contained in:
@@ -21,7 +21,12 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
case "${TARGET}" in
|
||||
sandbox) export DOL_WRITE="${SCRIPT_DIR}/dol-write.sh" ;;
|
||||
# sandbox honors a pre-set DOL_WRITE so the offline tests can inject
|
||||
# tests/stub-dol-write.sh (the erp#37 hook the op scripts already honor);
|
||||
# the default is the host-guarded dol-write.sh.
|
||||
sandbox) export DOL_WRITE="${DOL_WRITE:-${SCRIPT_DIR}/dol-write.sh}" ;;
|
||||
# prod NEVER inherits — always the gated dol-prod-write.sh (env-only key
|
||||
# + ARCO_PROMOTE_CONFIRM), so no environment trick can reroute a prod apply.
|
||||
prod) export DOL_WRITE="${SCRIPT_DIR}/dol-prod-write.sh" ;;
|
||||
*) echo "promote-apply.sh: --target must be sandbox|prod" >&2; exit 2 ;;
|
||||
esac
|
||||
@@ -32,7 +37,8 @@ import json, sys, subprocess, os
|
||||
manifest_path, script_dir = sys.argv[1], sys.argv[2]
|
||||
ops = json.load(open(manifest_path))
|
||||
OP_SCRIPT = {"thirdparty": "thirdparty-create.sh", "invoice": "invoice-create.sh",
|
||||
"creditnote": "creditnote-create.sh", "payment": "payment-record.sh"}
|
||||
"creditnote": "creditnote-create.sh", "payment": "payment-record.sh",
|
||||
"thirdparty_update": "thirdparty-update.sh", "contact": "contact-create.sh"}
|
||||
refmap = {}
|
||||
|
||||
import urllib.parse
|
||||
@@ -99,13 +105,28 @@ for i, op in enumerate(ops, 1):
|
||||
sys.stderr.write(r.stdout + r.stderr + "\n")
|
||||
sys.exit("promote-apply: op %d (%s) FAILED" % (i, t))
|
||||
out = r.stdout.strip()
|
||||
parsed = None
|
||||
try:
|
||||
rid = json.loads(out).get("id")
|
||||
parsed = json.loads(out)
|
||||
except Exception:
|
||||
pass
|
||||
if isinstance(parsed, dict):
|
||||
rid = parsed.get("id")
|
||||
else:
|
||||
rid = out if out.isdigit() else None
|
||||
ref = op.get("ref")
|
||||
if ref and rid is not None:
|
||||
refmap[ref] = int(rid) if str(rid).isdigit() else rid
|
||||
print(" [%d/%d] %-11s %-8s -> id=%s" % (i, len(ops), t, ("@" + ref) if ref else "", rid))
|
||||
# Surface the idempotency evidence inline: contact dedupes and
|
||||
# thirdparty-update read-back diffs are the proof a re-apply is a no-op.
|
||||
extra = ""
|
||||
if isinstance(parsed, dict):
|
||||
if parsed.get("deduped"):
|
||||
extra += " deduped=true (already on target — no write)"
|
||||
ch = parsed.get("changed")
|
||||
if isinstance(ch, dict):
|
||||
extra += " changed=%d%s" % (len(ch),
|
||||
(" [%s]" % ", ".join(sorted(ch))) if ch else " (no-op)")
|
||||
print(" [%d/%d] %-17s %-8s -> id=%s%s" % (i, len(ops), t, ("@" + ref) if ref else "", rid, extra))
|
||||
print("OK — promote complete. ref -> id: %s" % json.dumps(refmap))
|
||||
PY
|
||||
|
||||
Reference in New Issue
Block a user