Skip to content

feat(contracts/org): versioned canonical encoder and digest - #246

Merged
itsHabib merged 4 commits into
mainfrom
feat/contracts-org-canon
Aug 23, 2026
Merged

feat(contracts/org): versioned canonical encoder and digest#246
itsHabib merged 4 commits into
mainfrom
feat/contracts-org-canon

Conversation

@itsHabib

Copy link
Copy Markdown
Owner

First code of the org substrate. Deliberately first: a chain's digests outlive every refactor that follows them, so the bytes that produce a hash must be specified before any record type exists. Unfixable after the first chain is written.

Closes task p1-t0-canonical-encoder. Design: docs/features/org/spec.md §4.10 (PR #245, not yet locked — this piece is settled independently of the open questions there).

Why not json.Marshal

All four bakeoff kernels this package draws from — branchroom, mandate, obligation, proofline — canonicalize with json.Marshal. That ties the byte sequence to Go declaration order, HTML-escapes &, <, >, and carries no scheme version. The digests are stable Go-to-Go, reproducible nowhere else, and a field reorder during a refactor silently invalidates every historical digest.

Here a type states its canonical shape by building a Value, so the encoding is a specification a second implementation can follow rather than an artifact of this one.

The encoding

No insignificant whitespace. Map fields in sorted-name order — build order cannot reach the bytes. Shortest-decimal integers. Exactly one escape rule: JSON's two required escapes plus C0 controls; everything else literal UTF-8, including &, <, >, DEL, and U+2028/9.

No floats. Their decimal representation is the single richest source of cross-language digest disagreement, and nothing in an org record needs one — sequences are integers, deadlines are RFC 3339 strings, sizes are integers.

Ambiguous input is refused rather than resolved: duplicate field names, empty names, invalid UTF-8. "Last wins" would be a rule a second implementation has to reproduce exactly; refusal needs no agreement.

The scheme version is hashed in, not recorded beside the digest — a reader that ignores a beside-the-digest field would silently accept bytes produced under different rules; hashing it in makes that an ordinary mismatch.

Tests

The authoritative tests are hand-written expected bytes, not round-trips — a round-trip passes for any self-consistent encoder, including a wrong one.

Golden digests are reproducible with standard tools, which is the portability claim as an executable check rather than an assertion:

printf 'canon/v1\x00{}' | shasum -a 256

Verified to agree with this implementation. TestEncodingDiffersFromEncodingJSON pins the divergence so nobody "simplifies" this back into the thing it exists to replace.

97.7% coverage, -race clean, go list -deps confirms it imports nothing in the module.

🤖 Generated with Claude Code

itsHabib and others added 2 commits August 21, 2026 22:55
The first code of the org substrate, and deliberately the first: a chain's
digests outlive every refactor that follows them, so the bytes that produce
a hash must be specified before any record type exists.

Not reflection over structs. All four bakeoff kernels this package draws
from canonicalize with json.Marshal, which ties the byte sequence to Go
declaration order, HTML-escapes & < >, and carries no scheme version -
digests stable Go-to-Go and reproducible nowhere else, invalidated silently
by a field reorder. Here a type states its shape by building a Value, so
the encoding is a specification rather than an artifact of this
implementation.

Encoding: no insignificant whitespace, map fields in sorted-name order,
shortest-decimal integers, one escape rule (JSON's two required escapes
plus C0 controls), everything else literal UTF-8. No floats - their decimal
form is the richest source of cross-language disagreement and no org record
needs one. Ambiguous input is refused rather than resolved: duplicate field
names, empty names, invalid UTF-8.

The scheme version is hashed in, not recorded beside the digest, so a
reader that ignores it gets an ordinary mismatch instead of silently
accepting foreign bytes.

Golden digests are reproducible with printf | shasum -a 256, verified
against this implementation - the portability claim as an executable test
rather than an assertion.

97.7% coverage, race clean, imports nothing in the module.

Design: docs/features/org/spec.md 4.10. Closes task `p1-t0-canonical-encoder`

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@itsHabib

Copy link
Copy Markdown
Owner Author

@codex review

@itsHabib

Copy link
Copy Markdown
Owner Author

@claude review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@claude

claude Bot commented Aug 23, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 2m 25s —— View job


Code Review — PR #246

  • Read changed files
  • Analyze correctness, design, and encoding spec
  • Post complete review

This is a well-designed, well-motivated piece of foundational infrastructure. The encoding decisions are sound, the test strategy is exactly right (hand-written expected bytes rather than round-trips), and the reasoning is documented at the right level of detail. The notes below are mostly minor polish.


Correctness

appendString byte-by-byte iteration (canon.go:257–265) — Safe and correct. The loop walks bytes, not runes; multi-byte UTF-8 sequences are preserved intact because all continuation bytes (0x80–0xBF) and leading bytes (0xC2–0xFF) are ≥ 0x20 and are neither " (0x22) nor \ (0x5C), so they pass through unmodified one byte at a time. utf8.ValidString at the top of the function guarantees there are no ill-formed sequences. This is correct even though it looks unusual.

sortFields validation order (canon.go:224–237) — The empty-name check fires before the duplicate check (both empty names sort to positions 0 and 1; the check at i=0 catches the first one). This means two empty field names return ErrEmptyFieldName rather than ErrDuplicateField. Not wrong — both are hard refusals — but worth knowing.

Kind zero value (canon.go:48–55) — The deliberate gap at 0 means Value{} refuses at encode time. This is tested. Good defensive design.

Scheme separator (digest.go:37–39) — Using \x00 as a separator between the scheme string and the canonical bytes is correct defensive practice. Since Scheme = "canon/v1" is a fixed constant (no null bytes possible), the separator is technically redundant today but protects against a future scheme that is a prefix of another.


Minor Issues

1. Garbled comment on hexDigits (canon.go:240–241)

// hexDigits is the lowercase alphabet for \u escapes. Case is part of the
// encoding:  and  are different bytes.

Something was elided after encoding: — the two examples that were supposed to appear there (likely A and a, or "A" and "a") are missing. The comment reads as a sentence fragment. Fix this →

2. Duplicate comment block above TestGoldenDigests (canon_test.go:195–201)

// Golden digests. These pin the byte-to-digest mapping including the scheme
// version, which is hashed in rather than merely recorded beside the digest.
// A change to the encoder, the scheme string, or the hash construction breaks
// these — which is the intent: they are the chain's compatibility contract.
// Golden digests, pinning the byte-to-digest mapping including the scheme
// version that is hashed in rather than recorded beside it. A change to the
// encoder, the scheme string, or the hash construction breaks these — the
// intent, since they are the chain's compatibility contract.

The comment block says the same thing twice, slightly rephrased — looks like an edit artifact where a revised version was appended without removing the original. Fix this →

3. Redundant intermediate variable in Encode (canon.go:154–161)

func Encode(v Value) ([]byte, error) {
    var buf []byte
    buf, err := appendValue(buf, v)
    if err != nil {
        return nil, err
    }
    return buf, nil
}

This allocates buf, then immediately passes it to appendValue where the same nil slice would have been the correct starting point. The simplest form:

func Encode(v Value) ([]byte, error) {
    return appendValue(nil, v)
}

is idiomatic, eliminates the intermediate variable, and behaves identically since appendValue uses append internally. Fix this →

4. List aliases its input slice (canon.go:105–107)

func List(items ...Value) Value {
    return Value{kind: KindList, list: items}
}

The variadic items slice is stored directly. A caller holding a reference to the underlying array can mutate the list's contents after construction (before encoding). Map's pairs field has the same issue, but sortFields defensively copies before sorting, so the original is never mutated. List has no such protection. This is unlikely to matter in practice since Values are typically built and immediately encoded, but it is an asymmetry worth noting.


Design Observations (no action required)

  • The scheme-inside-hash design (DigestBytes prepends Scheme + \x00 before hashing) is exactly right. It makes a scheme mismatch manifest as a digest mismatch, which every consumer already handles, rather than requiring a parallel field-check.
  • Refusing invalid inputs (ErrDuplicateField, ErrEmptyFieldName, ErrInvalidUTF8) rather than resolving them is the only defensible choice for a durable hash substrate — "last wins" is a protocol two independent implementations have to agree on, while "refuse" is not.
  • The test TestEncodingDiffersFromEncodingJSON pinning the HTML-escape divergence is important defensive infrastructure. If someone replaces this encoder with json.Marshal for "simplicity," the test breaks loudly.
  • DigestBytes is exposed publicly. The docstring says "Prefer Digest" but doesn't stop a caller from hashing non-canonical bytes and getting a digest for them. Whether to export it is a judgment call; the test helper TestSchemeIsInsideTheHash uses it legitimately. Fine as-is.

Summary: Correctness is solid. The three fixable items are a garbled comment, a duplicate comment block (edit artifact), and a one-line simplification to Encode. The List aliasing note is a heads-up, not a blocker. Ready to merge after the comment cleanup.

@github-actions

Copy link
Copy Markdown

Reviewer: claude
Reviewed commit: 7c4988250cf1dbd42363512d98d9a02df92a81f3

Four findings from the panel on #246:

- hexDigits' comment lost its examples to an elision and read as a
  fragment. It now names the two byte sequences the case rule is about.
- A revised comment above TestGoldenDigests was appended without removing
  the original, so the block said the same thing twice.
- Encode allocated a nil slice only to pass it along; appendValue(nil, v)
  is the same behavior in one line.
- List stored its variadic slice directly. A variadic call cannot alias,
  but List(slice...) can, and a caller mutating that slice afterwards
  would change what a later Encode produced — a digest that depends on
  when it was taken. It clones now, matching sortFields' existing posture.

Goldens unchanged, which is the point: none of this alters a byte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
itsHabib added a commit that referenced this pull request Aug 23, 2026
Builds the continuity contract on top of the canon/v1 encoder: the
fixed-shape record spine, per-kind admission law, and the pure fold that
makes "holding the tip is being the role" a property of code rather than a
runtime convention.

Written against vision.md §3.9 — the state machine, not the earlier spec
draft. Chartered -> Held(inc) -> Active(inc, w) -> Retired, with each law
stated as something the system cannot represent:

  L1  no incarnation without owned work — a charter's scope must name at
      least one work reference
  L2  hold many, act on exactly one — which is what lets an effect stamp
      DERIVE its work_ref from state instead of taking it from the caller
  L3  stopping produces a terminal record — a takeover mid-claim leaves a
      dangling claim the successor must discharge before it may claim
  L4  an effect needs Active plus an open-intent bound of one

Three decisions worth review:

- Structural parameters live on the SPINE, not in the body. The fold has to
  survive body erasure, and a claim whose work URI sat in an erasable blob
  would make ownership depend on a blob somebody is entitled to delete.
- Incarnation ids are the digest of the record that minted them, so charter,
  attach and takeover carry none (a record cannot contain its own digest).
  Digests rather than branchroom's parent+n epochs: a guessable id is one a
  re-reading writer copies by accident.
- Identity is checked BEFORE chain position. The stale writer that matters
  re-read the tip and presents a perfectly correct prev; a position-first
  reducer calls that chain healthy and misdiagnoses the one failure the law
  exists to catch. TestCrashTakeoverAndTheStaleWriter pins the ordering.

Also here: kind classes on the wire (unknown structural refuses, unknown
advisory is skipped — an old reader that skips a takeover concludes it still
holds the role); refusals as values with a frozen 37-identifier vocabulary;
annul restricted to the tip, because verifying an arbitrary digest would
make the fold grow with the chain.

Three reconciliations of vision.md are recorded in record.go's file comment
and belong in the doc before P1's gate: claim/yield/complete added as
structural kinds, abandon settled as a claim terminal, handoff dropped in
favour of release-then-attach.

94.6% coverage, race clean, lint clean, leaf-clean. Property tests cover
prefix-plus-remainder equivalence and total-refusal over arbitrary chains.

Stacked on #246 (canon/v1 encoder).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@itsHabib

Copy link
Copy Markdown
Owner Author

@claude review

@claude

claude Bot commented Aug 23, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 3m 49s —— View job


Code Review — PR #246

  • Read changed files (contracts/org/canon.go, canon_test.go, digest.go)
  • Analyze correctness, design, and encoding spec
  • Post complete review

Reviewed at HEAD c7834f7 (round-1 fix commit). Round-1 addressed four of the prior findings — Encode simplification, List aliasing, the duplicate comment above TestGoldenDigests, and the garbled hexDigits comment. Two issues remain.


Issue 1 — hexDigits comment still effectively garbled (canon.go:240–241)

The round-1 fix inserted raw bytes into the comment source to serve as the "examples," but the bytes are C0 control characters (0x1F, the unit separator) that render as invisible in GitHub, most editors, and cat. Inspecting the file with grep … | cat -v shows ^_ at both example positions — the same character twice, which does not illustrate the case distinction the comment is trying to make.

The intent is to show that lowercase hex matters for the encoding. A readable version:

// hexDigits is the lowercase alphabet for \u escapes. Case is part of the
// encoding: the escape for 0x1F is ``, not ``.
const hexDigits = "0123456789abcdef"

Fix this →


Issue 2 — Map has the same aliasing gap List just fixed (canon.go:117)

List was fixed to use slices.Clone and now documents why:

A variadic call cannot alias, but List(slice...) can, and a caller that mutated its slice after building the Value would change what a later Encode produced — a digest that depends on when it was taken.

Map has the identical exposure and got no equivalent fix:

func Map(fields ...Field) Value {
    return Value{kind: KindMap, pairs: fields}  // aliases the caller's backing array
}

sortFields makes a defensive copy before sorting, so the encoder itself never mutates pairs — but a caller who holds the original []Field and mutates an element between two Encode calls gets different digests from the same Value. The List docstring and fix both argue this is unacceptable ("a digest that depends on when it was taken"); Map should be consistent:

func Map(fields ...Field) Value {
    return Value{kind: KindMap, pairs: slices.Clone(fields)}
}

Fix this →


Round-1 fixes confirmed

Finding Status
Encode simplification to return appendValue(nil, v) ✅ applied
List aliasing → slices.Clone ✅ applied
Duplicate comment above TestGoldenDigests ✅ removed
Garbled hexDigits comment ⚠️ attempted but still broken (see Issue 1)

Design notes (no action required)

  • The byte-by-byte appendString loop is correct: multi-byte UTF-8 continuation bytes (0x80–0xBF) and leading bytes (0xC2–0xFF) are all ≥ 0x20, not " or \, so they pass through intact. utf8.ValidString at the top ensures no ill-formed sequences reach the loop.
  • TestDistinctShapesDigestDistinctly is excellent: the collision check across ten structurally different values (list-of-two vs concatenated string, int-1 vs string-"1", etc.) catches the framing accidents that golden-digest tests alone would miss.
  • TestEncodingDiffersFromEncodingJSON is load-bearing defensive infrastructure. If this package gets "simplified" back to json.Marshal, the test breaks loudly — exactly right for foundational hash substrate.
  • The scheme-inside-hash construction in DigestBytes is correct. Hashing Scheme + \x00 + canonical means a scheme mismatch presents as a digest mismatch, which every consumer already handles, rather than requiring a parallel field check.

Summary: Two issues remain — the hexDigits comment still has invisible control characters where its examples should be, and Map has the same aliasing exposure List was just fixed for. Both are one-liners. Ready to merge after those.

@github-actions

Copy link
Copy Markdown

Reviewer: claude
Reviewed commit: c7834f7ea99d34d2433038026cf1601b48606696

…iasing

Two findings, both one-liners.

The hexDigits comment was reported fixed in round 1 and was not. The
round-1 edit was a string replace whose pattern did not match, so it
silently changed nothing — the line still held two literal 0x1F bytes
where its examples belonged, which is why it kept rendering as a
fragment. Written out properly now, and a sweep confirms no control byte
survives anywhere in the package.

Map stored its variadic slice directly, the same exposure List was fixed
for in round 1. Map(slice...) handed the caller's backing array a live
reference inside the Value, so mutating it between two Encode calls
produced two digests from one Value — the thing the List fix argued was
unacceptable. sortFields already cloned defensively before sorting, so
nothing downstream changes; this closes the constructor.

Goldens unchanged. Round 2 of the two-round cap; anything further goes to
the judge as residual.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
itsHabib added a commit that referenced this pull request Aug 23, 2026
Builds the continuity contract on top of the canon/v1 encoder: the
fixed-shape record spine, per-kind admission law, and the pure fold that
makes "holding the tip is being the role" a property of code rather than a
runtime convention.

Written against vision.md §3.9 — the state machine, not the earlier spec
draft. Chartered -> Held(inc) -> Active(inc, w) -> Retired, with each law
stated as something the system cannot represent:

  L1  no incarnation without owned work — a charter's scope must name at
      least one work reference
  L2  hold many, act on exactly one — which is what lets an effect stamp
      DERIVE its work_ref from state instead of taking it from the caller
  L3  stopping produces a terminal record — a takeover mid-claim leaves a
      dangling claim the successor must discharge before it may claim
  L4  an effect needs Active plus an open-intent bound of one

Three decisions worth review:

- Structural parameters live on the SPINE, not in the body. The fold has to
  survive body erasure, and a claim whose work URI sat in an erasable blob
  would make ownership depend on a blob somebody is entitled to delete.
- Incarnation ids are the digest of the record that minted them, so charter,
  attach and takeover carry none (a record cannot contain its own digest).
  Digests rather than branchroom's parent+n epochs: a guessable id is one a
  re-reading writer copies by accident.
- Identity is checked BEFORE chain position. The stale writer that matters
  re-read the tip and presents a perfectly correct prev; a position-first
  reducer calls that chain healthy and misdiagnoses the one failure the law
  exists to catch. TestCrashTakeoverAndTheStaleWriter pins the ordering.

Also here: kind classes on the wire (unknown structural refuses, unknown
advisory is skipped — an old reader that skips a takeover concludes it still
holds the role); refusals as values with a frozen 37-identifier vocabulary;
annul restricted to the tip, because verifying an arbitrary digest would
make the fold grow with the chain.

Three reconciliations of vision.md are recorded in record.go's file comment
and belong in the doc before P1's gate: claim/yield/complete added as
structural kinds, abandon settled as a claim terminal, handoff dropped in
favour of release-then-attach.

94.6% coverage, race clean, lint clean, leaf-clean. Property tests cover
prefix-plus-remainder equivalence and total-refusal over arbitrary chains.

Stacked on #246 (canon/v1 encoder).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@itsHabib

Copy link
Copy Markdown
Owner Author

@claude review

@claude

claude Bot commented Aug 23, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 1m 42s —— View job


Code Review — PR #246 (Round 3)

  • Read changed files (contracts/org/canon.go, canon_test.go, digest.go)
  • Analyze round-2 fixes at HEAD 98ce4af
  • Post complete review

Reviewed at HEAD 98ce4af. Both round-2 findings are addressed.

Round-2 fixes confirmed

Finding Status
hexDigits comment — invisible 0x1F bytes where examples should be ✅ fixed: now reads the escape "" and the escape "" are different byte sequences for the same rune
Map aliasing — pairs: fields stores caller's backing array directly ✅ fixed: pairs: slices.Clone(fields)

A sweep confirms no C0 control bytes survive anywhere in the package. The comment is now instructive and readable; Map and List are now symmetric on the aliasing guarantee.

Cumulative findings across all rounds

Round Finding Status
1 Encode simplification → return appendValue(nil, v)
1 List aliasing → slices.Clone
1 Duplicate comment above TestGoldenDigests
1→2 Garbled hexDigits comment ✅ (required a second attempt)
2 Map aliasing → slices.Clone

Design observations (no action required)

  • The byte-by-byte appendString loop is correct. Multi-byte UTF-8 continuation bytes (0x80–0xBF) and leading bytes (0xC2–0xFF) are all ≥ 0x20 and neither " nor \, so they pass through intact. utf8.ValidString at the entry point ensures no ill-formed sequences reach the loop.
  • The scheme-inside-hash construction (Scheme + \x00 + canonical before hashing) is correct. A scheme mismatch surfaces as a digest mismatch — which every consumer already handles — rather than requiring a parallel field check.
  • TestEncodingDiffersFromEncodingJSON and TestDistinctShapesDigestDistinctly are load-bearing defensive tests. The first breaks loudly if this encoder is ever "simplified" back to json.Marshal; the second catches framing accidents that golden-digest tests alone miss.
  • sortFields makes its own defensive copy before sorting (make + copy), so Map's slices.Clone and sortFields's internal copy are two separate layers — neither is redundant. sortFields is an internal helper that a future caller might pass a slice it still holds; the clone in Map closes the public constructor, sortFields's own copy closes the helper.

Ready to merge. All verified findings from rounds 1 and 2 are addressed; the code is correct, the tests are authoritative, and the encoding decisions are sound.

@github-actions

Copy link
Copy Markdown

Reviewer: claude
Reviewed commit: 98ce4af793379356928fcbaa0b7133b2b024397f

@itsHabib
itsHabib merged commit 823d1de into main Aug 23, 2026
3 checks passed
itsHabib added a commit that referenced this pull request Aug 23, 2026
Builds the continuity contract on top of the canon/v1 encoder: the
fixed-shape record spine, per-kind admission law, and the pure fold that
makes "holding the tip is being the role" a property of code rather than a
runtime convention.

Written against vision.md §3.9 — the state machine, not the earlier spec
draft. Chartered -> Held(inc) -> Active(inc, w) -> Retired, with each law
stated as something the system cannot represent:

  L1  no incarnation without owned work — a charter's scope must name at
      least one work reference
  L2  hold many, act on exactly one — which is what lets an effect stamp
      DERIVE its work_ref from state instead of taking it from the caller
  L3  stopping produces a terminal record — a takeover mid-claim leaves a
      dangling claim the successor must discharge before it may claim
  L4  an effect needs Active plus an open-intent bound of one

Three decisions worth review:

- Structural parameters live on the SPINE, not in the body. The fold has to
  survive body erasure, and a claim whose work URI sat in an erasable blob
  would make ownership depend on a blob somebody is entitled to delete.
- Incarnation ids are the digest of the record that minted them, so charter,
  attach and takeover carry none (a record cannot contain its own digest).
  Digests rather than branchroom's parent+n epochs: a guessable id is one a
  re-reading writer copies by accident.
- Identity is checked BEFORE chain position. The stale writer that matters
  re-read the tip and presents a perfectly correct prev; a position-first
  reducer calls that chain healthy and misdiagnoses the one failure the law
  exists to catch. TestCrashTakeoverAndTheStaleWriter pins the ordering.

Also here: kind classes on the wire (unknown structural refuses, unknown
advisory is skipped — an old reader that skips a takeover concludes it still
holds the role); refusals as values with a frozen 37-identifier vocabulary;
annul restricted to the tip, because verifying an arbitrary digest would
make the fold grow with the chain.

Three reconciliations of vision.md are recorded in record.go's file comment
and belong in the doc before P1's gate: claim/yield/complete added as
structural kinds, abandon settled as a claim terminal, handoff dropped in
favour of release-then-attach.

94.6% coverage, race clean, lint clean, leaf-clean. Property tests cover
prefix-plus-remainder equivalence and total-refusal over arbitrary chains.

Stacked on #246 (canon/v1 encoder).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
itsHabib added a commit that referenced this pull request Aug 23, 2026
…ld (#248)

* feat(contracts/org): record spine, contract law, and the ownership fold

Builds the continuity contract on top of the canon/v1 encoder: the
fixed-shape record spine, per-kind admission law, and the pure fold that
makes "holding the tip is being the role" a property of code rather than a
runtime convention.

Written against vision.md §3.9 — the state machine, not the earlier spec
draft. Chartered -> Held(inc) -> Active(inc, w) -> Retired, with each law
stated as something the system cannot represent:

  L1  no incarnation without owned work — a charter's scope must name at
      least one work reference
  L2  hold many, act on exactly one — which is what lets an effect stamp
      DERIVE its work_ref from state instead of taking it from the caller
  L3  stopping produces a terminal record — a takeover mid-claim leaves a
      dangling claim the successor must discharge before it may claim
  L4  an effect needs Active plus an open-intent bound of one

Three decisions worth review:

- Structural parameters live on the SPINE, not in the body. The fold has to
  survive body erasure, and a claim whose work URI sat in an erasable blob
  would make ownership depend on a blob somebody is entitled to delete.
- Incarnation ids are the digest of the record that minted them, so charter,
  attach and takeover carry none (a record cannot contain its own digest).
  Digests rather than branchroom's parent+n epochs: a guessable id is one a
  re-reading writer copies by accident.
- Identity is checked BEFORE chain position. The stale writer that matters
  re-read the tip and presents a perfectly correct prev; a position-first
  reducer calls that chain healthy and misdiagnoses the one failure the law
  exists to catch. TestCrashTakeoverAndTheStaleWriter pins the ordering.

Also here: kind classes on the wire (unknown structural refuses, unknown
advisory is skipped — an old reader that skips a takeover concludes it still
holds the role); refusals as values with a frozen 37-identifier vocabulary;
annul restricted to the tip, because verifying an arbitrary digest would
make the fold grow with the chain.

Three reconciliations of vision.md are recorded in record.go's file comment
and belong in the doc before P1's gate: claim/yield/complete added as
structural kinds, abandon settled as a claim terminal, handoff dropped in
favour of release-then-attach.

94.6% coverage, race clean, lint clean, leaf-clean. Property tests cover
prefix-plus-remainder equivalence and total-refusal over arbitrary chains.

Stacked on #246 (canon/v1 encoder).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(contracts/org): close the gaps mutation testing found

`gremlins unleash --timeout-coefficient 30 ./contracts/org/` on the
previous commit: 130 killed, 6 lived, 2 not covered — 95.6% efficacy
against 94.6% line coverage. Every surviving line was "covered": executed
by a test that never asserted on what it did. That gap is the argument
for running mutants instead of trusting a coverage number.

Two survivors were real:

- The min_reader monotonicity check had no EQUALITY case. Only a decrease
  was ever tested, so mutating `<` to `<=` — which refuses a recharter
  that leaves min_reader alone, the common case — survived untouched.
- Nothing asserted on NextDue at all, so inverting the guard that sets it
  survived. Derived liveness is computed against that field; a deadline
  that silently stops advancing is a role that reads as alive forever.

Two were untested branches worth covering: Refusal.Error's seq-0 form,
and short()'s exact boundary.

Now 135 killed, 2 lived, 1 not covered — 98.5%. The three survivors are
equivalent mutants and are recorded in record_test.go with the argument
for each, so a future survivor reads as a missing test rather than as
one more thing that was always like that. The most interesting one is
structural: a mutation of prose inside a refusal's detail string cannot
be killed, because every assertion here matches on Reason and never on
the sentence. That is what makes the vocabulary safe to reword.

Coverage 94.6% -> 96.3%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(contracts/org): teardown may not strand an obligation

An exhaustive walk of the reachable state space found two ways to lose an
obligation permanently, and both survived 96% line coverage, a 98.5%
mutation score, and the rapid property tests.

  charter attach assign claim takeover unassign retire
  charter attach assign claim intent-ref abandon merge

Both reach PhaseRetired with something still open — a dangling claim in
the first, an effect intent with no outcome in the second. Nothing may
extend a retired chain, so in both cases the obligation is gone for good.
That is the silent disappearance L3 says must not be representable, and
for the intent it is worse: T2's whole claim is that a replacement can
always determine committed-versus-absent, and a retired role deletes the
place that determination would be written.

Neither sequence is individually suspicious. checkTeardown asked only
whether the HELD SET was empty, and unassign empties it while leaving the
obligation behind — so each record is locally correct and the three
together are not. That is the failure mode a hand-written table cannot
find: you write cases for combinations you thought of.

checkTeardown now refuses on any outstanding obligation — dangling claim,
open intent, or unanswered escalation. The escalation arm is the same
argument one step further out: a human answers the ROLE, which is what
makes their latency free, and an answer cannot append to a terminal
chain. New refusal `open_escalation`; the frozen vocabulary goes 37 -> 38.

The walk itself ships as statespace_test.go: every reachable state, every
transition out of it, asserting totality (every refusal is a named
reason), eight state invariants, and a pinned reachable-state count. The
count is the interesting artifact — it went 97 -> 91 -> 86 as each fix
made a family of illegal states unreachable, which is the fix being
visible as a shrinking frontier rather than as a passing test.

One correction to the walk itself: its first version checked
dischargeability one record ahead and reported a wedge at {chartered,
dangling}. That state is correct — revoke kills the credential, the
obligation survives its holder, and the successor picks it up after an
attach. The machine was right and the property was wrong; it is a
reachability search now.

Coverage 96.3% -> 98.1%. Mutation 135/2/1 -> 140/2/1 (98.6% efficacy).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(contracts/org): review round 2 — assert the vocabulary, compare Terms

Four accepted findings and one rejected.

- The frozen-vocabulary test computed a digest and only LOGGED it, pinning
  the count alone. A rename that preserves count breaks every consumer
  doing errors.Is(err, &Refusal{Reason: ReasonXxx}) and passed CI
  silently — the exact thing the frozen vocabulary exists to prevent. The
  digest is the assertion now; the count stays as a second signal.
- sameState skipped Terms, so the prefix-plus-remainder property could not
  have caught a fold that dropped or mangled an inherited charter. It
  compares Terms now, and the property chain carries a recharter so the
  comparison has something to disagree about.
- checkBody used an exact emptiness check in one guard and a
  whitespace-aware one in the other. Both behaved correctly; the asymmetry
  read like a fix applied in one place and forgotten in the other.
- RoleState.Dangling now documents that it may outlive its Held entry.
  Unassign blocks only the ACTIVE claim, so a successor can drop the work
  and still owe the close — correct, because the obligation is to record
  how the claim ended, and that survives the work leaving the plate.

REJECTED: that `json:"subject,omitzero"` is a json/v2 tag stdlib ignores,
leaving `"subject":{}` on every record. omitzero landed in stdlib
encoding/json in Go 1.24 and go.mod requires 1.26, so the tag is honored
and there is no divergence between a canonical round-trip and a JSON
marshal. Verified against the toolchain rather than argued, and pinned as
TestWireFormatElidesEmptyOptionals — which also fails loudly if go.mod is
ever moved below 1.24, since that would silently change the wire format of
every record.

Mutation score holds at 98.6% (140 killed, 2 lived, 1 not covered).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
itsHabib added a commit that referenced this pull request Aug 26, 2026
…cisions to fold

Independent reconciliation of the whole ownership-continuity lineage at
2026-08-26 heads: what #245 designed, what #246/#248/#262/#263 shipped
through the governed path, what #265/#266 measure, what hooks#42/#43 and
drive#47 add, what cc-skills#29 proves in fixture, and what remains
hypothesis. Every major claim classified on the honest rung ladder;
duplications and missing joins named; D1-D10 written to be accepted,
amended, or struck in place. Indexed from vision.md's document table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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