PR S: Machine-form obligation layer (terms-as-data) #23

Merged
nsatoshi merged 1 commits from devin/1776883227-pr-s-obligations into main 2026-04-22 20:30:34 +00:00
Owner

Closes gap-analysis v2 §4.1 partial (Legal / Obligation Layer).

Today a Plan only carries a templateHash — a hash reference to an off-chain document. That satisfies tamper-evidence but is not machine-enforceable: the orchestrator can't tell whether an execution context satisfies the terms without a human reading the underlying PDF.

This PR lifts the governing-terms object into first-class structured data so commit/abort/unwind can be evaluated automatically.

What

  • services/obligations/types.tsObligationTerms schema: Consideration, validIssuance[], validPayment[], commit[], abort[], unwind[], AuthorizedParticipant[] (role set matches PR M's SoD: coordinator / approver / releaser / validator / exception_manager / operator), GoverningDocument[] (keyed by templateRef + SHA-256 templateHash, reusing the convention from existing InstrumentTerms).
  • services/obligations/evaluator.ts — closed-operator condition engine (eq, neq, gt, gte, lt, lte, in, not_in, exists, matches, length_gte, length_lte) + all/any/not combinators + dotted-and-indexed path resolution (e.g. plan.steps[1].type). No eval, no code execution, deterministic.
  • services/obligations/index.ts — public surface:
    • canonicalize() — lexicographic key-sort JSON encoding.
    • hashObligationTerms() — SHA-256 of canonical JSON, hex, no 0x prefix.
    • validateObligationTerms() — cheap structural check (ISO-4217 currency, hex hash, required fields, non-empty governingDocuments).
    • evaluateClauses() / evaluateCommit() / evaluateAbort() — run clause arrays against a context, return per-clause results without short-circuiting.
    • buildIssueInstrumentObligation() — derives a sensible default obligation from an issueInstrument step's InstrumentTerms. Bakes in the UCP 600 / URDG 758 rule that MT760 is irrevocable, so unwind applies only when payment failed AFTER instrument dispatch (amendment H / §4.1).

Why a self-contained evaluator

PR P (Rules Engine) ships a more general DSL that this evaluator is a subset of. Keeping a local copy lets PR S be merged independently of PR P — the Condition shape is deliberately identical so clauses authored for one run on the other. Post-merge cleanup can consolidate the two on whichever order they land.

Verification

  • npx tsc --noEmit clean.
  • Full jest suite: 8 suites, 100/100 passing (obligation tests contribute 20).
  • Coverage spans: canonicalize invariance, SHA-256 stability, validation (ISO-4217, hex hash, required fields), evaluator primitives, combinators, path resolution, commit success + failure attribution, abort firing on active exception, buildIssueInstrumentObligation binding template hash + governing law, non-throwing error surfacing for bad regex.
Closes gap-analysis v2 §4.1 partial (Legal / Obligation Layer). Today a Plan only carries a `templateHash` — a hash reference to an off-chain document. That satisfies tamper-evidence but is not machine-enforceable: the orchestrator can't tell whether an execution context *satisfies* the terms without a human reading the underlying PDF. This PR lifts the governing-terms object into first-class structured data so commit/abort/unwind can be evaluated automatically. ## What - `services/obligations/types.ts` — `ObligationTerms` schema: `Consideration`, `validIssuance[]`, `validPayment[]`, `commit[]`, `abort[]`, `unwind[]`, `AuthorizedParticipant[]` (role set matches PR M's SoD: coordinator / approver / releaser / validator / exception_manager / operator), `GoverningDocument[]` (keyed by `templateRef` + SHA-256 `templateHash`, reusing the convention from existing `InstrumentTerms`). - `services/obligations/evaluator.ts` — closed-operator condition engine (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `not_in`, `exists`, `matches`, `length_gte`, `length_lte`) + `all`/`any`/`not` combinators + dotted-and-indexed path resolution (e.g. `plan.steps[1].type`). No `eval`, no code execution, deterministic. - `services/obligations/index.ts` — public surface: - `canonicalize()` — lexicographic key-sort JSON encoding. - `hashObligationTerms()` — SHA-256 of canonical JSON, hex, no `0x` prefix. - `validateObligationTerms()` — cheap structural check (ISO-4217 currency, hex hash, required fields, non-empty `governingDocuments`). - `evaluateClauses()` / `evaluateCommit()` / `evaluateAbort()` — run clause arrays against a context, return per-clause results without short-circuiting. - `buildIssueInstrumentObligation()` — derives a sensible default obligation from an `issueInstrument` step's `InstrumentTerms`. Bakes in the UCP 600 / URDG 758 rule that **MT760 is irrevocable, so unwind applies only when payment failed AFTER instrument dispatch** (amendment H / §4.1). ## Why a self-contained evaluator PR P (Rules Engine) ships a more general DSL that this evaluator is a subset of. Keeping a local copy lets PR S be merged independently of PR P — the `Condition` shape is deliberately identical so clauses authored for one run on the other. Post-merge cleanup can consolidate the two on whichever order they land. ## Verification - `npx tsc --noEmit` clean. - Full jest suite: **8 suites, 100/100 passing** (obligation tests contribute 20). - Coverage spans: canonicalize invariance, SHA-256 stability, validation (ISO-4217, hex hash, required fields), evaluator primitives, combinators, path resolution, commit success + failure attribution, abort firing on active exception, `buildIssueInstrumentObligation` binding template hash + governing law, non-throwing error surfacing for bad regex.
nsatoshi added 1 commit 2026-04-22 18:44:37 +00:00
Machine-form obligation layer (terms-as-data)
Some checks failed
CI / Frontend Lint (pull_request) Failing after 7s
CI / Frontend Type Check (pull_request) Failing after 5s
CI / Frontend Build (pull_request) Failing after 7s
CI / Frontend E2E Tests (pull_request) Failing after 7s
CI / Orchestrator Build (pull_request) Failing after 7s
CI / Contracts Compile (pull_request) Failing after 6s
CI / Contracts Test (pull_request) Failing after 5s
Code Quality / SonarQube Analysis (pull_request) Failing after 19s
Code Quality / Code Quality Checks (pull_request) Failing after 4s
Security Scan / Dependency Vulnerability Scan (pull_request) Failing after 4s
Security Scan / OWASP ZAP Scan (pull_request) Failing after 4s
cd36ff6b38
Closes gap-analysis v2 §4.1 partial (Legal / Obligation Layer) —
today a Plan only carries a templateHash (hash-reference to an
off-chain document). This PR lifts the governing-terms object into
structured, machine-enforceable data.

- services/obligations/types.ts — ObligationTerms schema:
  Consideration, validIssuance[], validPayment[], commit[], abort[],
  unwind[], AuthorizedParticipant[] (role set matches PR M SoD),
  GoverningDocument[] (keyed by templateRef + SHA-256 templateHash
  that reuses the existing InstrumentTerms convention).
- services/obligations/evaluator.ts — closed-operator condition
  engine (eq, neq, gt, gte, lt, lte, in, not_in, exists, matches,
  length_gte, length_lte) + all/any/not combinators, dotted +
  indexed path resolution (e.g. plan.steps[1].type). No eval, no
  code execution, deterministic.
- services/obligations/index.ts — public surface:
    canonicalize(), hashObligationTerms(), validateObligationTerms(),
    evaluateClauses(), evaluateCommit(), evaluateAbort(),
    buildIssueInstrumentObligation() (derives a sensible default
    obligation from an issueInstrument step's InstrumentTerms —
    binds commit, abort, unwind, validIssuance, validPayment clauses
    that reflect UCP 600 / URDG 758 semantics, including the
    "MT760 is irrevocable so unwind only applies when payment
    failed AFTER instrument dispatch" rule from amendment H/§4.1).
- tests/unit/obligations.test.ts — 20 tests covering:
    * canonicalize() key-sorting invariance + array preservation
    * SHA-256 hash stability and sensitivity to mutation
    * validateObligationTerms() (shape, ISO-4217 currency, hex hash,
      authorizedParticipants role required, non-empty docs)
    * evaluator primitives (eq/gt/lt/in/matches/length_*)
    * all/any/not combinators
    * dotted + indexed path resolution
    * evaluateCommit ok-true + failure attribution
    * evaluateAbort firing on an active exception
    * buildIssueInstrumentObligation binding the template hash +
      governingLaw into governingDocuments
    * non-throwing error surfacing on bad regex
- Full suite: 8 suites, 100/100 passing. tsc --noEmit clean.
nsatoshi merged commit b77ebce497 into main 2026-04-22 20:30:34 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: d-bis/CurrenciCombo#23