Skip to content

docs(adr): ratify the claim expiry contract (ADR-011, S5/APA-26) - #181

Merged
Aparnap2 merged 2 commits into
mainfrom
docs/adr-011-claim-expiry-contract
Oct 21, 2026
Merged

Aparnap2 merged 2 commits into
mainfrom
docs/adr-011-claim-expiry-contract

Conversation

@Aparnap2

Copy link
Copy Markdown
Owner

What changed

Documentation-only, plus two comment-only source edits. No lifecycle behaviour changed.

File Change
docs/adr/011-claim-expiry-contract.md NEW — the ratification that never existed
docs/specs/09_DECISION_LOG.md +19 — the decision-log entry (house pattern)
docs/specs/06_PROGRESS.md +3/−2 — completed bullet; Last updated → 2026-10-21; live Current base pointer → main@254c4b9
docs/AGENT_OPERATING_CONTRACT.md item 5 now lists EXPIRED in the transition chain
docs/KNOWN_LIMITATIONS.md +1 — new row KL-18
apps/api/internal/claims/statemachine.go comment-only — header no longer claims a parity that did not exist
packages/domain/state_machine.py docstring-only — states it is superseded for lifecycle semantics and is not a mirror

Base main@254c4b92dddf59a758a133c1edbf7618dcc011b2 · commit bb6421a · branch docs/adr-011-claim-expiry-contract. ADR-011 avoids the pending ADR-010 in PR #161.

The gap this closes

EXPIRED was implemented, wired, tested and deployed — but no document ever ratified it. Its semantics were attested only by code comments citing a Linear id this repo does not record, while two things made that dangerous:

  • statemachine.go:3-7 described itself as "the Go port of the Python specification in packages/domain/state_machine.py" — a specification with 13 statuses and no EXPIRED. The file claimed parity it did not have.
  • fixtures/golden_cases.json — the cross-language contract designated by ADR-001 — has nine cases and none covers EXPIRED. The one status on which the two implementations disagree was exactly the status the contract did not cover.

What ADR-011 records

EXPIRED is an approved state of the authoritative Go lifecycle (S5/APA-26):

  • Terminal-for-round — no out-edges; terminality is enforced by absence from claims.Transitions.
  • Exactly two inbound edges — HITL → EXPIRED and ACTION_PENDING → EXPIRED (HITL-wait timeout).
  • Timeout expiry can never decide a claim — post-expiry APPROVE/REJECT/ESCALATE/HOLD are illegal, and APPROVE from HITL was already illegal because HITL has no VERIFIED edge.
  • Re-drive is a new round, never an implicit edge.
  • The Go edge is authoritative; the Python artefact is superseded for lifecycle semantics and is not a mirror.

Alternatives considered and rejected in the ADR: resyncing the Python graph to a true 14-status mirror (contradicts ADR-001's demotion and keeps a second lifecycle alive); removing the Go edges (the deployed GCW workflow posts EXPIRE on the HITL-timeout path — that branch would have no business-state outcome); leaving it undocumented (the status quo that caused this).

Evidence the implementation already behaves this way

Nothing in the implementation changed; these already existed and still pass:

  • apps/api/internal/claims/claim.go:117-121 — the status and its stated intent.
  • apps/api/internal/claims/statemachine.go:53-60 — the only two inbound edges.
  • apps/api/internal/handlers/decision.go:95-100 — EXPIRE wired into the HMAC decision endpoint with the same HMAC, version and idempotency controls as every other decision.
  • workflows/claim-investigation.yaml — the deployed workflow already posts EXPIRE (expire / expire_escalated).
  • apps/api/internal/claims/expiry_red_test.go — the S5/APA-26 contract.
  • apps/api/internal/handlers/decision_expiry_test.go — 4 PostgreSQL-gated tests.
  • apps/api/internal/repository/postgres/hitl_expiry_test.go — TestHITL_ExpiredExcludedFromPending.

Verification

gofmt -l .                     -> clean
go vet ./... / go build ./...  -> clean
go test ./... -count=1         -> 41 packages ok, 0 failures   (real PostgreSQL; claimops_app, rolsuper=f, rolbypassrls=f)
go test -race -count=1 ./...   -> clean, no data races
uv run pytest tests/unit -q    -> 67 passed
uv run ruff check .            -> All checks passed!
uv run mypy packages scripts/ci -> Success: no issues found in 11 source files

The expiry behaviour was re-asserted unchanged after the edit: TestExpiry_HITLToExpired_Legal, TestExpiry_ActionPendingToExpired_Legal, TestExpiry_NoOutEdges, TestExpiry_ApproveFromHITL_Illegal, TestDecision_PostExpiryDecisions_Rejected_Live, TestHITL_ExpiredExcludedFromPending — all PASS.

Diff shape proof: git diff on statemachine.go contains only // comment lines; git diff on packages/domain/state_machine.py is a single hunk at line 1 (the docstring). No executable line changed in either file.

Scope notes and deliberate omissions

  • Two disclosures. (1) 06_PROGRESS.md's Current base: was a live pointer to main@dffa160; because the file now carries a fact true as of 254c4b9, I updated it rather than leave the file internally inconsistent. This incidentally resolves one of the three W17 pointers; 09_DECISION_LOG.md:490 and 10_PHASE3_E2E_QUALIFICATION.md:5 remain for that separate pass. (2) packages/domain/state_machine.py is touched — docstring only — because its silence is precisely what caused the misattribution this ADR resolves.
  • Deliberately not bundled: the statemachine.go comment describing golden case 08 as witnessing "stale-version wins over legality" is also inaccurate (case 08's edge is legal, so the case witnesses only that a stale version is rejected). That is a separate focused change.
  • Still open, not closed here: the cross-language fixture has no EXPIRED case; the canonical audit-action vocabulary (Go emits claim.transitioned, the fixture says claim.status.transition); and the Go-vs-Python malformed-claim rule. These are tracked separately and are not resolved by this ADR.
  • docs/KNOWN_LIMITATIONS.md gains KL-18 registering the narrower Python graph rather than silently reconciling it.

Status

Documentation and comments only. Not authorized to merge — opened as a draft for review. No branch-protection change, no deployment. Release status NOT QUALIFIED; this closes no release gate.

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: cfe7f70c-09de-4e6d-afa9-1c675b87e953
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

EXPIRED has been implemented, wired, tested and deployed for a long time,
but no document in this repository ever ratified it. Its semantics were
attested only by code comments citing a Linear id the repo does not
record, while the file calling itself the specification of this lifecycle
did not contain the status at all.

ADR-011 records the approved semantics: EXPIRED is terminal-for-round
(no out-edges; terminality by absence from claims.Transitions), reachable
only as HITL -> EXPIRED or ACTION_PENDING -> EXPIRED (HITL-wait timeout).
Timeout expiry can never approve, reject, close, escalate or otherwise
decide a claim; post-expiry APPROVE/REJECT/ESCALATE/HOLD are illegal and
APPROVE from HITL was already illegal. Re-drive after expiry requires an
explicit new decision/round, never an implicit edge.

Reconciled documentation:
- statemachine.go's header no longer claims the file is 'the Go port of the
  Python specification' - packages/domain/state_machine.py is a demoted
  artifact, not a mirror, and does not define EXPIRED. Comment-only change.
- AGENT_OPERATING_CONTRACT.md item 5 now lists EXPIRED in the chain.
- packages/domain/state_machine.py's docstring states it is superseded for
  lifecycle semantics and is not a mirror. Docstring-only change.
- 09_DECISION_LOG.md and 06_PROGRESS.md carry the decision.
- KNOWN_LIMITATIONS.md gains KL-18 registering the narrower Python graph
  rather than reconciling it silently.

No lifecycle behaviour changed. The implementation already behaved this
way and the existing tests already pin it: claims/expiry_red_test.go,
handlers/decision_expiry_test.go (4 PG-gated),
repository/postgres/hitl_expiry_test.go.

The statemachine.go comment about golden case 08 is also inaccurate but
is deliberately left alone: that correction is a separate focused change.

Co-Authored-By: opencode <no-reply@opencode.ai>
@Aparnap2
Aparnap2 force-pushed the docs/adr-011-claim-expiry-contract branch from bb6421a to 876d9e9 Compare October 21, 2026 06:53
The rebase onto main@7ad446c (which merged #180 and #179) made three
statements in this change false as written:

- The ADR, the decision-log entry and KL-18 all said the cross-language
  fixture 'contains nine cases'. After #179 it contains nineteen. None
  of the nineteen covers EXPIRED, so the substantive claim is unchanged;
  only the count was stale.
- The ADR's Base line still named main@254c4b9; it is now written against
  main@7ad446c.
- The Context said 'no document in this repository ever ratified it'.
  This ADR ratifies it, so the claim needs its temporal qualifier. The
  statemachine.go reference is likewise now stated as 'before this ADR',
  since this change corrects that header.

Also fixes KL-18's position: it was inserted between KL-16 and KL-17
instead of after KL-17, so the register's rows were out of order.

Co-Authored-By: opencode <no-reply@opencode.ai>
@Aparnap2
Aparnap2 marked this pull request as ready for review October 21, 2026 07:03
@Aparnap2
Aparnap2 merged commit c971727 into main Oct 21, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant