# External review The -00 announcement on the SCITT mailing list asked readers to run the pinned commit and try to break it. This file records what came back and what changed as a result. Findings are listed whether or not they were bypasses. ## Round 1 — commit `e681e24`, 2026-08-25 Two independent readers ran the pinned commit from a clean clone. Both reproduced the published result: TypeScript silent, the suite passing, `npm run audit` exit 0, and all four advertised bypass demos failing as designed. ### 1. A signed extract was never the subject of the audit — fixed Reported by Iman Schrock (EMILIA). `audit()` verified `input.extract` but reconciled a separate `input.settlements` array. A validly signed extract could therefore carry an off-book settlement while the caller passed `settlements: []`; with empty receipts and checkpoints the report returned `ok: true`, `guarantee: "unconditional"`, `audit: balanced`. That is a full bypass of the completeness claim, reached without forging anything. Fix: when an extract is supplied it is the subject of the audit. Reconciliation runs over `extract.body.settlements`. A caller-supplied array that disagrees with the extract is reported as `extract-settlement-mismatch` rather than silently preferred. ### 2. The extract carried its own trust root — fixed Reported by Iman Schrock (EMILIA). `verifyRailExtract()` checks the signature against the `publicKeyPem` carried inside the extract. An attacker-generated key therefore self-verifies and produced an unconditional guarantee. The signature proved internal consistency, not that the named rail produced the extract. Fix: `audit()` accepts a verifier-supplied `trust` pin — the rail public key, and optionally the expected account, rail, and window. Without a pin the report stays `conditional` and states why. With a pin, a key that does not match is `extract-key-mismatch` and an out-of-scope extract is `extract-scope-mismatch`; both fail closed. ### 3. A repeated ref hid the amount that was unaccounted for — fixed Reported by Pablo Etcheverry, with a written repro in [issue #1](https://github.com/dogrucanemek-alt/cedulon/issues/1). A settlement injected under an existing receipt's ref sent both entries down the duplicate-ref path, which skipped them from the ref-keyed match. The audit still failed, so this was not a bypass, but the surviving finding said only that a ref appeared twice — the specific gap and its amount were never named. Fix: refs that dedup are no longer skipped outright. Per currency, the aggregate settled amount is compared against the aggregate receipted amount for that ref, and a shortfall is reported as `settlement-without-receipt` naming the amount that is unaccounted for. ## Coverage Red-then-green cases for all three are in `tests/extract-binding.test.ts` (cases 18–21). Case 13 in `tests/audit.test.ts` was updated: it previously asserted that a signed but unpinned extract yields an unconditional guarantee, which encoded the defect described in finding 2. ## Round 1b — self-review of the repair, same day Before replying to the list, the repair was attacked on its own terms. Four defects in it were found and closed; cases 22–25 cover them. ### 4. A pinned key was only checked when the signature already verified The trust check sat on the `else` branch of the signature check, so an extract whose signature did not verify skipped pin and scope comparison entirely and returned `ok: true` with a warning. The worse input took the softer path. Once a verifier states a pin, every way of failing to meet it is now a finding, including a signature that does not verify. ### 5. A doubted extract still called itself unconditional `guarantee` was derived from warnings alone, and `extract-key-mismatch` is a finding. A report could therefore name a key mismatch and describe its own guarantee as unconditional in the same breath. Findings that doubt the extract now force `conditional`. ### 6. The operator-facing output hid the guarantee `formatAudit` printed `audit: balanced / receipts=N / findings=0` and nothing else, so a conditional pass looked identical to a pinned one in the only output most people read. This is the same defect class as finding 3, shipped one layer further out. `formatAudit` now prints the guarantee and every warning on both the passing and the failing path. ### 7. An unreadable amount crashed the audit A non-integer amount such as `"1.5"` threw out of `BigInt()` and took the whole report down. An amount the audit cannot read is now reported as `malformed-amount`. ## What is still true, and deliberately so Without a pin, `ok` is still `true` and the summary still reads `audit: balanced`; only `guarantee` and the warnings carry the difference. That follows `MUST-T10-7` in -00, which makes an unauthenticated extract conditional rather than failing. The defect was that nothing surfaced it; that is now fixed. Making an unpinned audit fail outright would be a normative change and is not one this implementation should make on its own. Two further observations were named here as open. Both reporters agreed they were the right next controls, and both are now implemented; cases 26–28 cover them. ### 8. The pin was compared as PEM text — fixed Text comparison tolerated whitespace differences but not envelope differences: the same key published as bare base64 SPKI rather than PEM compared unequal, so an honest rail could be reported as a mismatch. Keys are now compared as SPKI DER bytes, and PEM or bare base64 are both accepted for the pin. The same change separated two failures that used to look identical. A pin the audit cannot read at all is now `trust-key-unreadable` rather than `extract-key-mismatch`, so a broken local configuration is distinguishable from an extract signed by the wrong key. ### 9. Rows outside the declared window were never checked — fixed An extract declares a window and carries settlement rows. Nothing compared the two, so an extract could claim to cover a period while carrying rows from outside it. Each row outside the declared window is now `extract-scope-mismatch`, named by its `ref`. This runs whether or not a key is pinned, because it is a question about the extract's internal consistency rather than about trust. ## Round 1 verification, by the reporters Both reporters re-ran the repair independently rather than take the claim. Iman Schrock verified commit `bdddc4c` from a clean worktree: TypeScript passed, all 97 tests passed, and the audit exposed the conditional guarantee and its warning as described. Iman declined the offer to add the attached test file, on the grounds that cases 18–25 already cover both findings. Pablo Etcheverry re-ran the original repro against the same commit before filing issue #1, so the issue records the behaviour before and after the fix rather than the original report alone. The output now names the gap: `ref x402-real settled 8 USD against 1 USD receipted; 7 USD unaccounted`. ## What -00 already required Finding 1 is not a gap in the draft. `MUST-T10-7` and the Rail Extract Profile in -00 already say a verifier checks completeness against the rail extract and not against the issuer's own receipts alone. The reference implementation contradicted the document it shipped with, and 81 passing tests did not notice, because every test happened to pass a settlement list that agreed with the extract. The correction there was to the code, not to the specification. ## Pending for the next revision The published -00 is frozen; these are queued for -01. One clarifies text that is already there, two are new requirements: - Clarification. -00 says a production verifier obtains the extract from the rail or from a signature the rail published, but the verification algorithm never says which key the signature is checked against. -01 states it: the rail key is obtained out of band, and an extract key that is absent or does not match cannot yield an unconditional guarantee. - New. A verifier checks that the extract covers the expected account, rail, and window, and fails closed when it does not. -00 defines extract scope but the algorithm has no step that compares it against what the verifier expected. - New. A `ref` that repeats is reconciled by aggregate amount per currency, so the unaccounted amount is named. -00 keys the match on each unique `ref`, which is what allowed a repeated ref to drop out of the comparison. - New. The pinned key is compared as SPKI DER rather than as encoded text, so the same key in a different envelope is the same key. - New. Every settlement row outside the extract's declared window is a named finding, so a declared window and the rows it carries cannot disagree silently. The implementation also now emits finding codes that -00 does not define: `extract-key-mismatch`, `extract-scope-mismatch`, `extract-settlement-mismatch`, `trust-key-unreadable`, and `malformed-amount`. The finding code table moves to -01 with them. ## Round 4 — draft-04 archive bytes, 2026-08-30 Tiago Pinto ran the Appendix A vectors against the exact IETF archive bytes (Python 3.14.4, cbor2 6.1.4, cryptography 50.0.1) before reading for failure points: both Ed25519 signatures verify, the SPKI-derived kid matches, and deterministic re-encoding reproduces the protected headers and payloads byte for byte. He then filed a first-failure list - the point where an independent implementation could no longer be built from the text - rather than a prose review: eight failures, one question, three mechanical points. Every repair below landed with its guard red before the fix; the branch history is the evidence. ### 1. Transparency receipt path vs RFC 9942 - fixed Section 11.3 described a hash comparison while citing RFC 9942 verification mechanics, and the evidence bundle never carried the candidate-entry bytes Section 5.2.1 consumes. Split into two named tiers: the witness receipt (a co-signature over the statement hash) and log membership (candidate bytes + inclusion proof, verified per 9942 §5.2.1 against a witness-signed tree head, exact receipt match required). The in-process witness became a Merkle tree issuing inclusion proofs, and absence of tier-2 inputs is reported as not exercised. Code: `c263834`. Text: the -05 witness-section rewrite, MUST-T11-18/19. ### 2. Key resolution in the unpinned branch - fixed Step 4 said "reject" and "report issuer-key-mismatch" in the same breath. Measured cell by cell (`a65a40a`), then decided: membership in the attested set follows verification under the pinned root, read out in one table, each cell a named condition plus a membership decision. Measuring the cells surfaced a sibling of break 6: the carried PEM is an unsigned surface, and swapping it off an honest receipt used to drop the receipt. Now `carried-key-mismatch`, a warning, and the receipt stays attested (`c263834`). Text: -05 step 4 table + issuer-root rule. ### 3. Rail Extract construction - fixed "The member names of that body are the rail's to define" contradicted Table 8. One runtime schema now sits behind signing, verification and the refusal channel (`00a791b`); Table 8 names are normative, added members are free, and -05 carries the full signed body shape in JSON terms, taken from the implementation. ### 4. Issuer order, chainHeadHash, zero checkpoints - fixed "Issuer order" defined as the prevReceiptHash chain order; the verifier rebuilds the chain from links and a shuffled presentation yields a byte-identical finding set (`e5ebe22`). `chainHeadHash` binds to the chain's last in-window link. Zero-checkpoint and open-epoch behaviour measured (fail-closed window-coverage findings) and written into -05 as measured; the false "nothing else feeds another step" sentence replaced by the maintained dependency list. ### 5. Window boundary vs two honest clocks - fixed A design gap, not a text gap: both honest verifiers reached the same false finding at the boundary. Probes locked the failure (`5735c62`), then the decision landed (`c263834`): ref binding governs membership first, and an unmatched item within the extract-declared `clockSkewMs` of the edge is `boundary-deferred`, resolving against the adjacent window and hardening when that window arrives without it. MUST-T10-17. ### 6. Appendable countersignature fails an honest audit - fixed The sharpest point on the list, and the mirror of MUST-T8-9's own lesson. Attack named red (`e6c76f6`), then fixed (`b630e83`): an unattributable countersignature is discarded as approval evidence with a warning, `countersign-missing` stays open under a pin, and the verdict on the untouched issuer receipt cannot be moved by anything appended beside it. Measured from both directions in the gate: the pre-repair tree flipped ok=true to false on a junk blob; the repaired tree holds the finding set byte-identical. ### 7. Counterparty binding - fixed A manifest by A, a receipt paying B, and a matching extract row all passed. Probes locked it (`5735c62`), then two optional bindings and one scope statement landed (`c263834`): manifest `payee` compared under MUST-T8-9's two branches, settlement-record `beneficiary` compared against the receipt payee, and `counterparty-unbound` reported when neither is present - a scope record, not an accusation. ### 8. Delivery hash bound to nothing - fixed Optional `deliveredHash` (claim -70402) in the countersignature payload binds the delivered bytes to the exact issuer receipt bytes under the payee key; the acceptanceCriteriaHash comparison is signed-to-signed (`delivery-mismatch`, MAY-T8-11), and the Introduction's delivery question is conditioned on that evidence (`c263834`; -05 text). ### Question: whose assertion is "allowed by policy" The Receipt Issuer's signed assertion; the Decision Token is consumed at the gate and the audit never sees it. -05 states the boundary in those terms and names a receipt-to-token binding as a possible extension rather than smuggling it in. ### Mechanical Table 3's hash grammar (64 lowercase hex) moved into signers and validators, named per claim (`cbcee11`, `2601b34`); the -04 Appendix A receipt vector that violated it is registered as the counted split V-T4-appendix-a-policy-hash and -05 regenerates the vector with a computed digest. MAY-T8-9 renumbered MAY-T8-10. The lone-surrogate escape permission removed: encoder refuses by name, verify surfaces report rather than throw (`d58594b`, `09959af`), and a guard scans the tree's vectors and fixtures for lone surrogates before the rule can move again. ## Round 5 - accounting rules from a neighbouring draft, 2026-09-01 Not a reading of this code by an outside reviewer. The rules are the outside part: draft-abak-agent-control-delivery-evidence-00 defines per-instruction dispositions, a selection requirement, two population-conservation identities, and conditions on any aggregate result. Its author asked for a mapping from this profile's finding codes onto that vocabulary. Writing the mapping ran his rules over this reconciler, and two of them did not pass. The mapping and the checks are one runnable file. It is pinned here rather than copied into this tree, so the bytes he holds and the bytes described here cannot drift apart: `cedulon-abak-population-probe.mjs`, SHA-256 `84763271fafe050daf6277d398885685cb75216b6cf4d0ac65afc67d52e4c083`. It resolves `@cedulon/*@0.8.0` from npm and needs no clone. ### 1. One exclusion is published and the other is not - closed, 0.11.0 Section 6.3 requires that records excluded before population construction be reported with the exclusion rule and count, or the completeness claim is not reproducible. Two rows can leave a window's accounting here. A settlement left unmatched inside the opening clock-skew boundary is reported as `boundary-deferred`, and the warning names both the row and the rule, so the receiver-record side stays reconstructible. A receipt left unmatched inside the closing boundary, whose ref appears in `nextExtract`, is dropped with no finding and no warning (the `nextRefs` branch in `packages/audit/src/index.ts`), and the summary is `audit: balanced`. A reader holding that report cannot tell whether the issuer population held one instruction or none. The behaviour is right in both cases: the row belongs to the neighbouring window, and charging it here would be a false positive. The probe runs a control for each - move the settlement away from the boundary, or give the receipt a next window that does not name its ref, and both harden into findings - so neither is a row that was never counted in the first place. What is wrong is only that the exclusion on the instruction side is unreportable, and that is the side the completeness claim is about. ### 2. A receipt that positively did not settle receives no class - closed, 0.11.0 An `aborted` receipt is correct to have no row on the extract, and the audit returns `balanced` with no finding. It is also absent from the report altogether, so nothing distinguishes a window holding one refused spend from a window holding none. Section 6.1's EXPLICIT_FAILURE exists to keep exactly that visible. ### What would close both The report publishes findings and an aggregate; it does not publish class counts. Section 6.4's last requirement is the one it does not meet, and the two findings above are that single gap seen from two sides. Closing it means `AuditReport` carrying counts it already computes - how many rows were matched, deferred, carried into the next window, unmatched, and refused - rather than any new check. ### What closed both, 2026-09-03 `AuditReport.counts`, published as 0.11.0 on 2 September 2026 (23:28 UTC). On the receipt side: submitted, attested, in scope, aborted, settled, and of the settled ones matched, deferred, carried, unmatched, repeated, unreconciled. On the settlement side: rows, matched, deferred, unmatched, repeated, unreconciled. Every settled receipt and every row lands in exactly one class, `matched` is the same number on each side, and the identities are asserted by `tests/audit-counts.test.ts` on every report it builds, including the one where the pinned rail key refuses the extract and every row is `unreconciled`. Finding 1's dropped receipt is `carried`; finding 2's aborted receipt is `aborted`. The member is on the finding object (schema updated, object version unchanged), the printed report (two `counts` lines), the `cedulon_audit` result and the ledger export. No exclusion rule changed; the exclusions are now reported. A review of the first cut by two independent readers, the same day, found two defects the identities could not see: a refused extract still sieved the receipts with the window it declared, so a forged document could empty the population and leave every count at zero with the identities intact; and the issuer-chain walk tracked receipts by object identity, so one receipt object presented twice counted as one. Both are fixed in the same tree, each with a test that failed first. The mapping's precedence question in the paragraph below is not addressed by this and stays open on its own terms. ### What the mapping showed about the codes themselves Of the 49 codes `@cedulon/audit` exports, 18 speak to an instruction, 5 to a record, and 1 names an exclusion. The remaining 25 are not dispositions at all: they say the declared population does not stand, or the evidence does not, or they belong to the transparency or terms layer. The codes are also many-to-one against instructions - one malformed receipt emits seven of them, one twice, across five layers - so a disposition mapping needs a precedence rule and a record of what it discarded, which is what Section 6.2 already asks for. ## Round 6 - receipt binding, self-review after the CCF -04 Last Call, 2026-09-03 The CCF receipts profile Last Call comment (Sent/101) said a receipt verified without `assert(proof.leaf.data-hash == HASH(candidate_bytes))` establishes inclusion, not coverage. The same question was asked of this tree on 3 September (`receipt-binding-audit` @ `772fd24`). Inventory: | Path | Verdict | |---|---| | `verifyInclusionReceipt` | Unbound. Takes the receipt only; reads the leaf from the envelope. | | `MemoryTransparencyService.verifyInclusion` | Unbound at verify time. Compared the log's stored leaf, not caller bytes. | | `audit()` layer-2 | Bound. `HASH(candidate)` against `statementHash`, proof root, index, pin. | | `verifyReceipt` / `verifyCoseSign1` / MCP `cedulon_verify_receipt` | No inclusion path. Signature (and claims) only. | | MCP `cedulon_audit` | No inclusion path. Does not take `layer2` or `inclusionReceipts`. | Finding: the public function whose name said "inclusion" did the envelope check and stopped. A valid receipt for statement A verified while the caller held only B. The audit library already implemented MUST-T11-18 on its layer-2 input; the named verifier did not. Fix in this tree (0.12.0 prepared, not published): `verifyInclusionReceipt` is removed. `verifyInclusionEnvelope` is the old envelope check. `verifyInclusion` takes the candidate bytes and the witness key and applies the seven decisions in `MUST-T11-18` order. Tests: `tests/inclusion-shape.test.ts` (six cases plus the in-memory log's (a)(b)). A second reader over that cut, before merge, found what the seven decisions did not cover: the hex under them. `Buffer.from(hex, "hex")` keeps whatever it decoded before the first bad character, so `aazz` as a candidate, a `coseHex` with a trailing suffix, and an empty statement at `register` all passed or were accepted; and a proof was applied before its shape was looked at, so an empty path at index 1 handed back the leaf unchanged and matched a witness-signed envelope whose tree head was that leaf, while a `null` path threw out of the public verifier. Both are closed in the same prepared release: one strict hex reader for the package and the audit's layer-2 input, and a named proof shape whose index must resolve to the root. Tests: `tests/inclusion-shape.test.ts` ("hex and proof shape"), the layer-2 cases in `tests/tiago04-k-round.test.ts`. The same pass found that `release.yml` would have run its npm publish steps off a manual dispatch with the tag guard skipped, and that its release notes stopped at the first blank line; the workflow now answers to tags only and the notes come from `scripts/release-notes.ts`, with `tests/release-workflow.test.ts` and `tests/release-notes.test.ts` holding the shape. A third reader, over the tree after those closures, found that the shape check still read the siblings with `Array.prototype.every`, which skips holes: a sparse array with one filled slot passed and the public verifier threw where the audit had reported a finding, the exact divergence the round was meant to close. It walks by index now, and the two hex readers the package still had outside `strictHexBytes` (`decodeInclusionPayload`, and the leaf and sibling hashes under `applyInclusionProof`) refuse by name. Nothing else in that reading reproduced as a defect; the notes it left are in the tree as tests (the workflow accepts no trigger but a tag push) and as sentences in UPGRADING. Published as 0.12.0 on 3 September 2026 (13:06 UTC), eight packages from the tag with provenance. Known, not closed: - MCP `cedulon_audit` still does not carry a layer-2 input (`packages/mcp-server/src/session.ts` `audit()`, `src/index.ts` tool schema). A caller on that surface cannot present candidate bytes and a proof. - JSON `receiptHash` still hashes `canonical({claims, signature})` (`packages/receipts/src/index.ts` `receiptHash`) rather than the signed COSE octets spec `{#hash-inputs}` names. New receipts MUST use COSE; the JSON path is legacy. ## Round 7 - decision profile, 2026-09-04 Origin. The 1 September population probe (`interop/abak-00`) applied draft-abak-agent-control-delivery-evidence-00 Section 6 to the spend reconciler and found two holes that `counts` later named (Round 5). The draft-abak -01 review, 3 September, recorded that an absent extract is a FAIL from absence. The same triangle — issuer record, independent counterparty, declared population — is the one a decision needs: who decided, what was effected, over which window. What this tree now carries (0.13.0 prepared, not published): - a `ReconciliationProfile` seam; spend behaviour held by `tests/spend-golden.test.ts` (byte for byte) - `DecisionRecordClaims` / `decisionRecordHash` (COSE Sign1 bytes) and `@cedulon/effect-extract` - `DECISION_PROFILE` and four codes (`decision-without-effect`, `effect-without-decision`, `effect-against-refusal`, `effect-mismatch`) - eighteen conformance cases (`tests/decision-profile.test.ts`) - an offline IG koba (`interop/mizan-ig`) over proposed JSONL Known, not closed: - FAIL from absence when no extract is presented (same as spend; draft-abak -01 review, 3 September) - counter names remain spend dialect (`receipts`, `settlements`, `aborted`, `settled`) - effect-signer independence is a pin the verifier states; Meta does not sign. Until B is an independent process, `guarantee` is conditional - IG / WhatsApp-bridge log field names were not measured on Hetzner; only `fromBridgeLine` / `fromMetaLine` should move when they are - policy-document binding is not exercised (`terms()` returns `[]`) - Decision Record CWT block is `-70501`…`-70512`, not the `-70401` the task first named (countersign already holds `-70401`/`-70402`) - `CTY_DECISION_RECORD` and the effect-extract media type are not in the posted IANA table (spec text is a separate decision) - README and the site were not updated; those are external claims Gate, same day. The branch was read and re-run from a second worktree, and the golden file was run against the pre-seam source (`45bb020`): it passed there, so it does record the old behaviour. One spend sentence had still moved. An aborted receipt that kept its ref, presented beside a row on that ref, read "effect … against a refusal" instead of "settlement … has no spend receipt"; the golden's `aborted` case carried no ref, so the branch the seam added was never exercised. The three one-sided sentences now live on the profile, the golden gained `aborted-with-ref` (regenerated from `45bb020`, red on the branch before the fix), and the twelve conformance cases assert their counters. Two documentation sentences were corrected: the effect extract refuses a row outside its window whole, where a rail extract is accepted and the row is named; and the decision chain break borrows the spend code. A second sentence had moved, found while an outside model was reading the same diff. The seam told a rail extract from an effect extract by whether the body carried `effects`, and a record from a decision record by whether the claims carried `decision`. A rail may add members, so a rail extract with an extra `effects` member, balanced on the base commit, was refused on the branch as a malformed effect extract. The population is now the profile's call: under `SPEND_PROFILE` every document is read as spend, under `DECISION_PROFILE` as decision. The golden file gained `rail-extra-member-effects` (regenerated from `45bb020`, red on the branch before the fix) and the conformance cases gained a thirteenth, a rail extract presented to the decision profile, which is refused. Second eye, same day. An outside model read the branch at `3e5f066` with the whole diff and the two fix commits, ran the suite (528 tests, the same counts), and returned seven blocking items. Four held under measurement and are closed here. `verifyDecisionRecord` checked the signature and the payload against the carried claims but not the claim rules the signer applies, so an allow record with no `ref`, signed below the API by the pinned decider, verified true, was attested, and the audit said ok; the verifier now re-applies those rules on the decoded payload, and a record that fails them under the pinned key walks the chain and is named. An effect extract presented to the spend profile threw on an absent `settlements` array before any report existed; it is now refused the way a rail extract is refused on the decision profile. The spend `counterparty-unbound` warning fired on every decision audit that carried an extract, in spend words; the counterparty axis is now the profile's, and the decision profile has none. The documents said twelve cases where the tree had thirteen. Three did not hold as stated: a deny carrying an `effectHash` was not a hole (the profile binds refusals to the absence of a row, and a row on that ref is `effect-against-refusal` either way), but the stricter grammar is the simpler one, so signer and verifier now refuse it; the IG adapter reads the directory it is given, which is what a command-line adapter does; and swapping the two test key pairs wholesale cannot break a fixture that signs and pins with the same constants, though the point under it was real, so the cases gained a wrong effect-extract key and a wrong decider key. Seventeen cases now. Two smaller defaults were closed on the same pass: the IG adapter hashed an empty string for an allow line with no `replyText`, and now refuses the line; and the golden regeneration flag is refused under CI. Dialect pass, same day. Shared report sentences now come from `ReconciliationProfile.words`. Spend keeps today's English (`tests/fixtures/spend-golden.json` sha256 unchanged). Decision reports say decision record / effect / effect extract / effect-extract key / effect path / decider / channel. Twenty-three sentences moved; spend-only paths (manifest/terms, countersign/payee, `settled-without-ref`) stayed. The watcher is `tests/decision-profile.test.ts` ("no spend vocabulary"). Third reading, same day, of the companion draft (`spec/draft-dogru-cedulon-decision-profile-00.md`) against this tree and against the core -08, by a reader barred from editing either. Two items blocked posting and both held. The draft repeated the core's carried-key rule for a Decision Record, and the code did not follow it: `verifyDecisionRecord` required the carried key to equal the pin before checking the signature, so an honest record whose carried PEM had been swapped was dropped as `issuer-key-mismatch` and failed the checkpoint behind it, where the core (6.3, 10.1) keeps such an object attested under a `carried-key-mismatch` warning. The verifier now checks the signature under the pin, the chain walk verifies under the pins, and the audit's carried-key loop reads decision records; case 18 holds it. The draft also cited the core's generalization section as reserving this profile, and that section reserves compute, data and energy; the sentence was rewritten. Of the notes, three changed the tree: the unused `application/cedulon-effect-extract+cbor` export is gone (the extract is a JSON body with a detached signature and has no media type, as the rail extract has none), `timestampMs` is refused unless it is a non-negative safe integer (the label table said `uint`; the decoder accepted any number), and the media-types guard now reads the companions beside the core, so the exception list it carried is gone. The rest were sentences: the per-row window finding the audit can still name on a refused body, the reachable-code list, the order of the content-type check, the refusal count the report publishes as one number, and a claim about outside readers that the review log contradicted. ## Round 8 — decision profile -01, 4 Sep 2026 Origin. Iman Schrock, off-list review of decision-profile-00, same day the -00 text was posted. Item 3: bind `effectClass` into the signed Decision Record; otherwise the same signed record can be classified differently without the decider's signature moving. Items 1, 2, and 4 are text and are carried in -01. Item 3 is closed by this change. `effectClass` is claim `-70513`. An allow without a non-empty class is `allow-requires-effect-class`. Hash equal and class different is `effect-class-mismatch`. Cases 19 and 20 hold it. A -00 twelve-label record is refused as `decision-record-tstr-or-null`. Item 2 was measured, not closed in code. Case 1 with no inclusion receipt and no witness pin: `warnings` is `[]`, no `equivocation` finding, `guarantee` is `"unconditional"`, `scope` is the extract window. The report does not say a log was not asked. If the witness is optional, the claim that the chain controls equivocation has to narrow in the -01 text. ## Round 9 — release gate for 0.13.0, 5 Sep 2026 Origin. A read-only pass by an outside model over the three commits that had not been read by anyone but the author: the nine-package release path (9955382, 9eee4c8) and the version bump (096972d). Ten attack items, two FIX, nine NOTE. FIX-1: UPGRADING's 0.13.0 section said 54 codes and eighteen cases in C and 55 and twenty in D, so the rendered release notes contradicted themselves. FIX-2: STATUS carried a second 0.13.0 prepared paragraph from the profile's first merge, with the old counts and a typo. NOTE-4: the new workflow guard could be evaded by literal loops in comment lines and a loop over a shell variable in the run block. All three closed in 3637b62; the second pass reconstructed the evasion and watched the guard fail on it and pass on the checked-in file. Published as 0.13.0 on 5 September 2026 (22:50 UTC, nine packages, provenance on all nine, the MCP Registry and the bundle produced by the tagged run itself; the bundle job needed a rerun because two tarballs were not yet served ten seconds after the publish). ## Round 10 — one fixture, two readers, 5 Sep 2026 Origin. Agreed off-list with Iman Schrock (EMILIA) on 4 September: the same frozen bytes read by two separately owned adapters, with no claim that either reader speaks the other's native input. The bytes are the companion's `interop/mizan-ig/fixtures/leaked-refusal/` at commit `06c3119badc269ef5d6d3596ea3b3d48219d6ba4`: | file | sha256 | |---|---| | decisions.jsonl | `2db5f0a8491aa34031223d9d4e732620a1df86bb37747972fa86e9095dd48d72` | | sent.jsonl | `cfd9037ef183c04364d2c32951b108be2740d4b75bdd2f9cb83dc1ac7fd5d131` | | policy.txt | `41d79f176661c3ac24181dae71506e8d5738f0470102d9cdda1dd9222cfe0805` | One decision line (verdict silent, ref `leak-1`), one sent line under the same ref. Cedulon reader (this tree, `runFixture`): findings `[effect-against-refusal leak-1]`, warnings `[]`, `ok:false`, `guarantee:"unconditional"`. Held by `tests/mizan-ig-adapter.test.ts` ("leaked-refusal: silent but sent"). EMILIA reader (Outcome Binding, `verifyOutcomeBindingSet`), run by Iman Schrock against the same bytes, all three digests matching: `lifecycle_state=reconciled`, `outcome=divergent`, `valid=false`; the mapped absent prediction for `ig-dm-reply` at `leak-1` was contradicted by one observed effect. Mapping, in Iman's words: deny/defer to an `absent` predicate, `ref` to `target`, `effectClass` to `effect_type`, `effectHash` to the observed value. EMILIA commit `2986500386319342070590368a917eba78cbcd65` (), PR #735 "test: add Cedulon Mizan leaked-refusal fixture" (). Outcome Binding result `sha256:80d9c6f5f2c6fd57eb0931044fc8d530c3fc34fbe11d8611c14aee9145486a15`; harness report `sha256:56eeb27a388ce7dbf7515cc85ec5bccb29a643ef097fb0d02b0385d26c8f8b37`. Boundary, kept with the entry as Iman asked: this is agreement on one pinned raw fixture through separately owned adapters, not native-format interoperability. The keys are public test material and the timestamps are derived after the fact, so the run establishes neither real identity nor temporal precommitment. Neither reader identifies where the refusal failed to take effect; both say the population did not reconcile, each in its own word.