A202-0002: Obligation shape, due condition, and acceptance rule¶
Status: Fixtures and compatibility. Stage 3 of the five stages in README.md section 3. The static fixtures in section 5 are implemented in the conformance suite; the seven runtime cases in section 5.3 remain recorded for implementation-level verification.
Date: 27 July 2026
Status of this document: Informative in full. The normative text this proposal adopts is carried by agreement/obligation-v0.1.md, which marks its own normative sections.
Adopts: obligation v0.1
1. Problem¶
The canonical model names Obligation in its object inventory, describes it as a "measurable duty, due condition, and acceptance rule", and defines none of the three. The state machine has an obligation.activated event that moves the aggregate from committed to in_performance, and a performance.declared and acceptance.granted pair that move it onward, with no object underneath any of them. The kernel schema has obligation in its object_type enum and obl_ in its prefix table, and no payload definition.
What a counterparty cannot currently do:
- Parse what it owes. A supplier's agent receives a committed agreement and cannot compute, from the record, which discrete duties fall on it, in what quantity, or by when. The agreement carries terms; nothing decomposes them into duties with obligors.
- Know when a duty is due. There is no typed due condition, so an implementation either invents one or carries the due date as prose inside a term. Prose is not something a counterparty's agent can evaluate, and two implementations that parse it differently will disagree about whether a party is late.
- Know what counts as done.
acceptance.grantedmoves the aggregate, and nothing states who may grant it, what it must bind to, or what happens when only part of what was owed is accepted.
The concrete situation where this goes wrong: a buyer commits to a supplier for ten calibration units against a terms_hash. The supplier delivers eight. Today the record can express performance.declared and acceptance.granted, and after that sequence the aggregate reads as if performance was accepted. Nothing in the record says two units are still owed. The party that most needs to prove they were owed has no object to point at.
A second failure is quieter. Because nothing binds an acceptance to the exact assertion it accepts, an acceptance and the performance it purports to accept can drift apart while both remain validly signed. The specification set already refuses that drift for offers, for agreements, and for approvals, and does not refuse it here.
2. Proposal summary¶
Adopt obligation v0.1, which defines:
- The obligation object. Common envelope,
obl_prefix, non-nulltransaction_id. Payload carriesagreement_id,commitment_id,obligorandobligeeaspartyRef,subject,due_condition,quantityasquantityString,unit_code,considerationasmoneyor null, andstate. - Subject by reference.
subjectnames the owed term through the agreement'sterms_hash,profile, and aterm_pathaddressed the same way a mandate constraintpathis. The obligation never restates a term, so a drifted copy becomes a hash mismatch rather than a second valid signature. - Typed due conditions.
due_at_time,due_on_event,due_on_discharge, andall_of. AND composition only, one nesting level, closed registry, cycles refused. An unknown type fails closed withA202-OBLIGATION-CONDITION-UNKNOWN. - Assertion and response as two acts. The obligor asserts with a
performance_eventcarrying at least one evidence reference. The obligee responds with anobligation_responsebound to the assertion'scontent_hash, withresponse_typeofaccept,reject, orwaive. Partial acceptance names a remainder obligation and never mutates the original. A rejection carries a reason code from a closed list and opens the dispute path without changing the obligation's terms. - Nine states and a guarded transition table in the style of the existing state machine, reusing
A202-STATE-TRANSITION-DENIEDrather than adding a parallel code. - Nine refusal codes, listed in section 7 of the adopted document.
3. Alternatives considered¶
Do nothing. Leave Obligation named and undefined. Rejected: every implementation then defines the object itself, and two implementations that both pass the current conformance suite can disagree about what a committed agreement obliges. That disagreement surfaces only after performance, which is the worst possible time to discover it.
Carry the due date as a profile term and define no due condition. Rejected: it moves the problem into every profile, so the same commercial concept gets a different shape per domain, and the kernel loses the ability to say anything about whether an obligation is due. It also violates the market-neutrality rule in the other direction, by putting a market-neutral concept behind a profile boundary.
Make acceptance a version bump on the obligation. Rejected: it makes the accepted state the only state visible in the current version, and reconstructing what was originally owed then depends on holding the whole version chain. It also makes partial acceptance a mutation, which erases the shortfall from the record.
Allow OR and NOT in due conditions. Rejected for v0.1: a disjunction is expressible as two obligations, and writing it out is what keeps the record replayable. Boolean combinators can be added later under a MINOR increment without invalidating anything written under AND only.
Reuse the existing acceptance object type for performance acceptance. Rejected: its payload is closed and bound to offer_id and offer_hash. Overloading it would make one type mean two different things at two different points in the lifecycle, and the disambiguation would have to be inferred from context.
4. Compatibility¶
4.1 What breaks¶
Nothing that is currently valid becomes invalid. No existing field changes meaning, no existing state is removed, no existing transition is narrowed, and no existing fixture is reclassified. Under RELEASES.md section 2 this is a MINOR change: new object types, new payload definitions, new error codes for cases previously undefined.
4.2 Kernel schema changes required¶
| Change | Detail |
|---|---|
Add obligationPayload $def and its conditional |
Keyed on object_type: obligation, id pattern ^obl_, signatures minItems 1 |
Add performanceEventPayload $def and its conditional |
object_type: performance_event is already in the enum. The payload is undefined today |
Add obligation_response to the object_type enum |
New type. Prefix obr_ |
| Register two identifier prefixes | obr_ for obligation response, and prf_ for performance_event, which is in the object_type enum today with no entry in the prefix table of canonical-commercial-model-v0.1.md section 3. See section 4.5 |
Add subjectRef and dueCondition $defs |
Both reuse sha256Hex, quantityString, and the registered profile identifier pattern |
No new value type is introduced. money, percentString, quantityString, partyRef, and sha256Hex are reused unchanged, and the obligation object is refused if it introduces a parallel representation of any of them.
4.3 Existing objects¶
| Object | Effect |
|---|---|
Agreement |
Unchanged. An obligation references agr_ and terms_hash, and the agreement does not reference obligations back |
Commitment |
Unchanged in shape. The relationship model already states that a commitment decomposes into obligations; the obligation now names the commitment it decomposes |
Offer, Acceptance |
Untouched. Nothing in the negotiation path changes |
CommercialMandate |
Unchanged in shape. Three action names are used by this proposal, obligation.waive, performance.declare, and acceptance.grant, and the actions array is already an open list of action names |
Evidence |
Referenced through the evidence reference shape adopted by A202-0004. See section 4.6 |
4.4 Existing states: in_performance and exception_open are the integration points¶
The aggregate state machine is unchanged. This proposal supplies the object beneath transitions that already exist.
| Aggregate transition | What this proposal adds beneath it |
|---|---|
committed to in_performance, on obligation.activated |
The obligation object the event activates, and the guard that its subject.terms_hash equals the agreement's terms_hash |
in_performance to acceptance_pending, on performance.declared |
The assertion object, and the requirement that it carries at least one evidence reference. The existing guard reads "evidence manifest present" and now has an object that satisfies it |
acceptance_pending to settlement_pending, on acceptance.granted |
The obligation_response bound to the assertion hash, signed by the obligee |
acceptance_pending to in_performance, on acceptance.rejected |
The obligation_response carrying a registered reason code. The existing required side effect reads "activate rework obligation if agreed", which is now the named remainder or a new obligation rather than an unspecified one |
any eligible committed state to exception_open, on exception.opened |
An obligation reaching disputed is the object-level fact behind the exception. The dispute itself is adopted by A202-0003 |
exception_open to in_performance, on remediation.accepted |
Remediation lands as a new obligation, so the transition has an object to activate |
One asymmetry is recorded rather than smoothed over. The aggregate carries a single exception_open state while an obligation set may carry several disputed obligations at once. The aggregate leaves exception_open when the events for that transition append, and not automatically when one obligation's dispute resolves. This proposal does not change that, and does not add a per-obligation aggregate state, because doing so would multiply the aggregate state space by the obligation count.
4.5 A prefix table gap found while drafting¶
performance_event and exception are members of the object_type enum in the kernel schema and have no entries in the identifier prefix table of canonical-commercial-model-v0.1.md section 3. The generic identifier pattern ^[a-z]{3}_ admits any three-letter prefix, so two implementations can mint different prefixes for the same object type and both validate.
This proposal registers prf_ for performance_event, because obligation discharge depends on that object. It does not register a prefix for exception, which is out of its scope and is recorded here so it is not lost.
4.6 Ordering against A202-0004¶
The assertion requires at least one evidence reference in the shape defined by evidence-verification-v0.1.md section 3, which A202-0004 adopts. A202-0002 depends on A202-0004 for that shape and on nothing else in it. Either both land in one release or A202-0004 lands first. They cannot land in the reverse order, because the assertion rule would then reference an undefined shape.
4.7 Migration¶
There is nothing to migrate. No obligation object exists under any earlier version of the specification set, because none was defined. An implementation that has been minting an object of its own design under the obl_ prefix is not migrating from a previous version of this specification; it is adopting the specification for the first time.
5. Fixture plan¶
The published suite holds 7 positive and 31 negative fixtures. The ratio is deliberate, and this proposal is weighted the same way: 2 positive and 13 negative static fixtures, plus 7 cases that require runtime state.
5.1 Positive¶
| Fixture | What it exercises |
|---|---|
valid-obligation.json |
A complete obligation: subject by reference against a terms_hash, a due_at_time condition, quantity and unit code, and money consideration |
valid-obligation-response-partial-acceptance.json |
Accept in the allow direction with accepted_quantity below quantity and a named remainder_obligation_id |
5.2 Negative¶
| Fixture | Expected code | What it refuses |
|---|---|---|
negative/obligation-condition-type-unknown.json |
A202-OBLIGATION-CONDITION-UNKNOWN |
An unregistered due condition type. Fails closed at schema and independently at evaluation |
negative/obligation-condition-fields-of-another-type.json |
A202-OBLIGATION-CONDITION-UNKNOWN |
type: due_at_time carrying obligation_id. A registered type wearing another type's fields |
negative/obligation-condition-cycle.json |
A202-OBLIGATION-CONDITION-CYCLIC |
Two obligations each due on the other's discharge. Neither can ever become due, and today nothing says so |
negative/obligation-condition-nested-all-of.json |
A202-OBLIGATION-CONDITION-UNKNOWN |
all_of nested more than one level |
negative/obligation-subject-restates-terms.json |
A202-OBLIGATION-SUBJECT-UNREFERENCED |
subject carrying a copy of the term instead of a term_path and terms_hash |
negative/obligation-subject-terms-hash-mismatch.json |
A202-OBLIGATION-SUBJECT-UNREFERENCED |
subject.terms_hash differing from the referenced agreement's. This is the drift case, caught as a hash mismatch |
negative/obligation-obligee-not-party-to-agreement.json |
A202-OBLIGATION-SUBJECT-UNREFERENCED |
An obligee that is neither the buyer nor the supplier on the agreement |
negative/obligation-assertion-no-evidence.json |
A202-OBLIGATION-ASSERTION-UNEVIDENCED |
An assertion with an empty evidence_refs |
negative/obligation-response-hash-mismatch.json |
A202-OBLIGATION-RESPONSE-HASH-MISMATCH |
A response whose assertion_hash does not match the named assertion. The direct analogue of A202-APPROVAL-HASH-MISMATCH |
negative/obligation-response-signed-by-obligor.json |
A202-OBLIGATION-RESPONSE-UNAUTHORIZED |
The obligor accepting its own performance |
negative/obligation-waiver-signed-by-obligor.json |
A202-OBLIGATION-RESPONSE-UNAUTHORIZED |
The obligor waiving its own obligation. Held separately from the case above because a waiver names no assertion, so a check written only against assertion_id would miss it |
negative/obligation-partial-acceptance-no-remainder.json |
A202-OBLIGATION-REMAINDER-MISSING |
Eight of ten accepted with no remainder obligation. The shortfall would otherwise disappear from the record |
negative/obligation-partial-acceptance-mutates-quantity.json |
A202-OBLIGATION-TERMS-MUTATED |
A response that rewrites the obligation's quantity to the accepted amount |
negative/obligation-rejection-reason-unregistered.json |
A202-OBLIGATION-REJECTION-REASON-UNKNOWN |
A rejection carrying a reason code outside the closed list |
negative/obligation-consideration-negative-money.json |
A202-TERMS-INVALID |
A negative consideration. Confirms the obligation reuses money rather than a parallel type, which is the failure this fixture exists to catch |
negative/obligation-due-business-days-no-calendar.json |
A202-TERMS-INVALID |
A due_at_time expressed in business days with no named calendar |
5.3 Cases requiring runtime state¶
These cannot be expressed as static documents and are verified against a running implementation, in the manner of the existing runtime lists.
- An obligation whose due condition is met moves
pendingtodue, and one whose condition is not met does not. - A rejected obligation accepts a second assertion, and each response binds exactly one assertion hash.
- An advisory determination on a disputed obligation leaves the obligation in
disputed. This is the fail-closed case: an implementation that lets any determination move state passes every static fixture and fails here. - A presumptive determination likewise leaves the obligation in
disputed. - A binding determination moves the obligation only where the target transition would be legal for any other event.
obligation.activatedagainst an agreement whoseterms_hashdiffers is refused, and the aggregate stays incommitted.- A waiver from
pendingand a waiver fromdueboth reachwaived, and a waiver fromdischargedis refused withA202-STATE-TRANSITION-DENIED.
6. Origin¶
Drafted against the object inventory and the state machine of the v0.1 specification set, which name Obligation, obligation.activated, performance.declared, and acceptance.granted without defining the object any of them acts on. The partial-acceptance case in section 1 is the situation that forced the split between an accepted obligation and a remainder obligation rather than a mutation. This is context for reviewers rather than an argument.