From fdf76b52a26f16117efa6be1eb170bc1c1e2d55a Mon Sep 17 00:00:00 2001 From: s0fractal Date: Thu, 27 Aug 2026 00:02:21 +0300 Subject: [PATCH 1/4] feat(x5E10): ActionIntent commits over CNP-0-JCS canonical bytes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trinity half of the joint adoption slice. Pairs with myc `ec6a754e`. ccc26b8b460fe2debf0ad069d55ec170a78b7b70861f1f54c03e401e4576c3be The vendored contract is replaced wholesale with MYC's current one — the shared domain check, the closed member set, set semantics for requested_effects, and the CNP-0-JCS encoder — so the two copies are byte-identical where it matters. **A LIVE parity test replaces two matching constants.** `src/action_intent_parity_test.ts` imports MYC's module when the submodule is present and compares canonical TEXT, canonical BYTES, then digests, over eight inputs chosen for where two implementations could plausibly diverge: astral scalars (UTF-16 vs code-point ordering), duplicated effects, both permutations, empty arrays, escapes, BMP non-ASCII. It also asserts both boundaries refuse the same out-of-domain values and that neither reaches a digest. Two constants in two files prove the constants match. Verified the test detects real drift: renaming MYC's `numeric_profile` value turns it red on canonical text. Verified it skips honestly with the submodule absent — core CI stays self-sufficient, and a skipped parity check is never counted as passing. CI gates that: with submodules present, a SKIPPED line fails the job, so the gate cannot pass vacuously. **The CLI validates runtime input fail-closed.** `warrant intent` and `warrant admit --intent` parsed JSON and cast to ActionIntent. Parsing says the bytes are JSON; it says nothing about whether they are an intent. Both now run `validateIntent` and report the reason instead of letting a bare RangeError reach the caller. **The superseded digest is an `intent_mismatch` at the gate**, not a near miss — that is the shape a stale grant takes when there is no dual-hash path. **The canonical bytes are judged by the ratified verifier-only path** (`probes/cnp-0-seed-v0/ts/reject.ts`), which shares no code with the encoder and cannot repair what it reads. Checking them against our own encoder would only say the encoder agrees with itself. The check is discriminating: the same value with members in source order is rejected. Scope held: the proposal-body `stable()` in this same file is deliberately NOT migrated and now says so — it recomputes a commitment MYC already wrote with that stringification, so changing it alone would reject every existing proposal. Receipt envelope, registry amendments and effectVerdictHash untouched. No status claim moved. The submodule pin is the myc candidate for joint review; it becomes the myc merge SHA mechanically once that side lands. ./t check READY: 579 tests, 367 chords valid. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01DRHMVB9rUYNggn4wLmc1An --- .github/workflows/ci.yml | 17 ++ myc | 2 +- src/action_intent_parity_test.ts | 180 +++++++++++++++++++ src/warrant_test.ts | 58 ++++++- src/x5E10_warrant.ts | 279 ++++++++++++++++++++++++++++-- src/x8788_network.myc.md | 8 +- src/x88E0_readme_bootstrap.myc.md | 2 +- src/x88F0_agents_bootstrap.myc.md | 2 +- src/x8CF0_skills_bootstrap.myc.md | 2 +- 9 files changed, 526 insertions(+), 24 deletions(-) create mode 100644 src/action_intent_parity_test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e19b5ddc..619d4666 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,6 +148,23 @@ jobs: if: steps.subm.outputs.present == 'true' run: ./t court --live | jq -e '.law_bridge.omega_native != null and .law_bridge.trinity_witnessed != null and .law_bridge.consistent == true' + # With the submodule present the ActionIntent commitment must be verified by + # RUNNING both implementations, not by comparing two constants. Trinity + # vendors MYC's contract byte-for-byte; a vendored copy that drifted while + # both suites kept the same expected string would otherwise pass. + - name: Verify ActionIntent cross-substrate parity exercised + if: steps.subm.outputs.present == 'true' + run: | + out=$(deno test --allow-all src/action_intent_parity_test.ts 2>&1) + echo "$out" + # The live cases skip themselves when the submodule is absent; here it + # is present, so a SKIPPED line means the gate passed vacuously. + if echo "$out" | grep -q "SKIPPED"; then + echo "::error::parity skipped while submodules are present" + exit 1 + fi + echo "$out" | grep -q "4 passed" + - name: Verify recipes detail lookup run: ./t recipes show recipe.metacognition.field-cycle --json | jq -e '.type == "recipes" and .position == "3/C" and .action == "show" and .recipes[0].id == "recipe.metacognition.field-cycle"' diff --git a/myc b/myc index de1f8a93..ec6a754e 160000 --- a/myc +++ b/myc @@ -1 +1 @@ -Subproject commit de1f8a93efeadb667f9f56ad409d4b9b42346a99 +Subproject commit ec6a754e9c7f85a45efbe98aefd7a3bee9a955ae diff --git a/src/action_intent_parity_test.ts b/src/action_intent_parity_test.ts new file mode 100644 index 00000000..80862e44 --- /dev/null +++ b/src/action_intent_parity_test.ts @@ -0,0 +1,180 @@ +// LIVE cross-substrate parity for the ActionIntent commitment. +// +// Trinity vendors MYC's contract byte-for-byte because it cannot static-import +// the submodule under CI decoupling. Until now the only thing holding the two +// copies together was a known-answer constant written into each suite - which +// proves the two CONSTANTS match and says nothing about whether the two +// implementations do. A vendored copy that drifted while both files kept the +// same expected string would pass. +// +// So when the submodule is present, this executes BOTH implementations over the +// same inputs and compares their canonical BYTES first, then their digests. +// Bytes first on purpose: two implementations agreeing on a wrong encoding +// produce one matching digest and no evidence about either. +// +// When the submodule is absent - Trinity's core CI runs without it - the live +// cases report SKIPPED with a reason and nothing is asserted about MYC. A +// skipped parity check is never counted as a passing one. + +import { assert, assertEquals } from "@std/assert"; +import { + canonicalIntentBytes as trinityBytes, + canonicalIntentText as trinityText, + intentCommitment as trinityCommitment, + validateIntent as trinityValidate, +} from "./x5E10_warrant.ts"; + +const MYC_CONTRACT = new URL( + "../myc/src/x5820_action_intent.ts", + import.meta.url, +); + +type MycModule = { + canonicalIntentText: (i: unknown) => string; + canonicalIntentBytes: (i: unknown) => Uint8Array; + intentCommitment: (i: unknown) => Promise; + validateIntent: (v: unknown) => { ok: boolean; error?: string }; +}; + +async function loadMyc(): Promise { + try { + await Deno.stat(MYC_CONTRACT); + } catch { + return null; // submodule not checked out: core CI stays self-sufficient + } + return await import(MYC_CONTRACT.href) as MycModule; +} + +const VECTOR = { + verb: "apply", + target_substrate: "myc" as const, + args_commitment: "c1", + input_commitments: ["a", "b"], + requested_effects: ["receipt", "write"], +}; +const EXPECTED_CANONICAL = + '{"args_commitment":"c1","canonical_encoding":"hsp-jcs@v0",' + + '"input_commitments":["a","b"],"numeric_profile":"cnp-0",' + + '"requested_effects":["receipt","write"],"target_substrate":"myc",' + + '"verb":"apply"}'; +const EXPECTED = + "ccc26b8b460fe2debf0ad069d55ec170a78b7b70861f1f54c03e401e4576c3be"; +const SUPERSEDED = + "d02d75adca7e0dbbd10244c7ea1e9aeafa7b6d019a0f570bcad471a38d997552"; + +// Inputs chosen for where two implementations could plausibly diverge, not to +// re-assert the happy path: astral scalars (UTF-16 vs code-point ordering), +// duplicate effects (set semantics), permutations, empty arrays, escapes. +const ASTRAL = "\u{1D11E}-clef-\u{1F600}"; +const CASES: [string, typeof VECTOR][] = [ + ["the shared vector", VECTOR], + ["effects permuted", { ...VECTOR, requested_effects: ["write", "receipt"] }], + ["effects duplicated", { + ...VECTOR, + requested_effects: ["write", "write", "receipt"], + }], + ["inputs permuted", { ...VECTOR, input_commitments: ["b", "a"] }], + ["empty arrays", { ...VECTOR, input_commitments: [], requested_effects: [] }], + ["astral scalars", { ...VECTOR, args_commitment: ASTRAL }], + ["BMP non-ascii", { + ...VECTOR, + verb: "\u0437\u0430\u0441\u0442\u043e\u0441\u0443\u0432\u0430\u0442\u0438", + }], + ["escapes", { ...VECTOR, args_commitment: 'a"b\\cd' }], +]; + +Deno.test("action_intent parity - LIVE: both implementations, same canonical bytes", async () => { + const myc = await loadMyc(); + if (!myc) { + console.log( + " SKIPPED: myc submodule is not checked out; no parity claim is made", + ); + return; + } + for (const [name, intent] of CASES) { + assertEquals( + myc.canonicalIntentText(intent), + trinityText(intent), + name + ": canonical TEXT differs across substrates", + ); + assertEquals( + Array.from(myc.canonicalIntentBytes(intent)), + Array.from(trinityBytes(intent)), + name + ": canonical BYTES differ across substrates", + ); + assertEquals( + await myc.intentCommitment(intent), + await trinityCommitment(intent), + name + ": digest differs across substrates", + ); + } +}); + +Deno.test("action_intent parity - LIVE: both refuse the same out-of-domain values", async () => { + const myc = await loadMyc(); + if (!myc) { + console.log(" SKIPPED: myc submodule is not checked out"); + return; + } + const bad: [string, unknown][] = [ + ["number in effects", { ...VECTOR, requested_effects: [1] }], + ["invalid substrate", { ...VECTOR, target_substrate: "mars" }], + ["empty verb", { ...VECTOR, verb: "" }], + ["extra member", { ...VECTOR, escalate: true }], + ["lone surrogate", { ...VECTOR, verb: "x\uD834y" }], + ]; + for (const [name, v] of bad) { + assertEquals( + myc.validateIntent(v).ok, + trinityValidate(v).ok, + name + ": the two boundaries disagree", + ); + assert(!trinityValidate(v).ok, name + ": trinity admitted it"); + for ( + const [who, fn] of [ + ["myc", myc.intentCommitment], + ["trinity", trinityCommitment], + ] as const + ) { + let threw = false; + try { + await (fn as (i: unknown) => Promise)(v); + } catch { + threw = true; + } + assert(threw, name + ": " + who + " produced a digest"); + } + } +}); + +Deno.test("action_intent parity - the pinned vector, without the submodule", async () => { + // Trinity's own half, so the vector is guarded even in core CI where the live + // comparison above is skipped. + assertEquals(trinityText(VECTOR), EXPECTED_CANONICAL); + assertEquals(await trinityCommitment(VECTOR), EXPECTED); + assert(await trinityCommitment(VECTOR) !== SUPERSEDED); +}); + +Deno.test("action_intent parity - a missing profile identifier moves the commitment", async () => { + // Both identifiers live INSIDE the hashed root (RFC-0003 5.1.2.1). If either + // can be dropped without moving the digest, they are decoration. + const text = trinityText(VECTOR); + for ( + const id of [ + '"canonical_encoding":"hsp-jcs@v0"', + '"numeric_profile":"cnp-0"', + ] + ) { + assert(text.includes(id), id + " is not in the hashed root"); + const without = text.replace(id + ",", ""); + assert(without !== text, "the replacement did not apply"); + const d = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(without), + ); + const hex = Array.from(new Uint8Array(d)).map((b) => + b.toString(16).padStart(2, "0") + ).join(""); + assert(hex !== EXPECTED, "removing " + id + " left the digest unchanged"); + } +}); diff --git a/src/warrant_test.ts b/src/warrant_test.ts index 6eec5a23..735fa84f 100644 --- a/src/warrant_test.ts +++ b/src/warrant_test.ts @@ -5,6 +5,7 @@ import { import { actionBoundAuthority, type ActionIntent, + canonicalIntentBytes, intentCommitment, type ProposalDescriptor, } from "./x5E10_warrant.ts"; @@ -17,6 +18,14 @@ const desc = (over: Partial = {}): ProposalDescriptor => ({ const IC = "deadbeef"; // a stand-in intent commitment +const VECTOR_INTENT = { + verb: "apply", + target_substrate: "myc" as const, + args_commitment: "c1", + input_commitments: ["a", "b"], + requested_effects: ["receipt", "write"], +}; + Deno.test("warrant — terminal state is NOT a capability: final without action_grant is denied", () => { // codex acceptance #1: a ratified governance proposal (no action_grant) grants // NO actuation authority, even though it is final:implemented. @@ -119,6 +128,53 @@ Deno.test("warrant — intent commitment matches the SHARED cross-substrate vect }); assertEquals( commitment, - "d02d75adca7e0dbbd10244c7ea1e9aeafa7b6d019a0f570bcad471a38d997552", + "ccc26b8b460fe2debf0ad069d55ec170a78b7b70861f1f54c03e401e4576c3be", + ); +}); + +// ADOPTION checks (RFC-0003 Part 01 5.1, Tranche A3). The commitment now runs +// over CNP-0-JCS canonical bytes; these guard what that must mean at the +// authority boundary rather than at the encoder. +Deno.test("warrant - the superseded digest is an intent_mismatch, not a near miss", () => { + // A grant minted under the pre-adoption algorithm must not authorize the same + // intent. There is no dual-hash path, so this is the shape a stale grant takes + // when it reaches the gate: denied, with a reason an actor can act on. + const SUPERSEDED = + "d02d75adca7e0dbbd10244c7ea1e9aeafa7b6d019a0f570bcad471a38d997552"; + const CURRENT = + "ccc26b8b460fe2debf0ad069d55ec170a78b7b70861f1f54c03e401e4576c3be"; + const v = actionBoundAuthority(CURRENT, { + fqdn: "h.deadbeef0000.proposal.myc.md", + commitment: "deadbeef0000", + action_grant: { intent_commitment: SUPERSEDED }, + }, "implemented"); + assertEquals(v.authorized, false); + assertEquals(v.reason_code, "intent_mismatch"); +}); + +Deno.test("warrant - the canonical bytes pass the ratified verifier-only path", async () => { + // The bytes this commitment hashes must be canonical under the encoding that + // was ratified, judged by the path that CANNOT repair what it reads. Asserting + // this against our own encoder would only say the encoder agrees with itself. + const { verifyRaw } = await import( + "../probes/cnp-0-seed-v0/ts/reject.ts" + ); + const bytes = canonicalIntentBytes(VECTOR_INTENT); + const out = await verifyRaw(bytes); + assertEquals( + out.ok, + true, + "the verifier-only path rejected our canonical bytes: " + + (out.ok ? "" : out.rejection + " - " + out.detail), + ); + + // And it is discriminating: a non-canonical spelling of the same value is + // rejected, so the pass above is not vacuous. + const reordered = new TextEncoder().encode( + '{"verb":"apply","args_commitment":"c1","canonical_encoding":"hsp-jcs@v0",' + + '"input_commitments":["a","b"],"numeric_profile":"cnp-0",' + + '"requested_effects":["receipt","write"],"target_substrate":"myc"}', ); + const bad = await verifyRaw(reordered); + assertEquals(bad.ok, false, "member order was not enforced"); }); diff --git a/src/x5E10_warrant.ts b/src/x5E10_warrant.ts index 02da1c78..81d60ed6 100644 --- a/src/x5E10_warrant.ts +++ b/src/x5E10_warrant.ts @@ -54,6 +54,236 @@ export interface ActionIntent { requested_effects: string[]; } +const SUBSTRATES = ["trinity", "myc", "liquid", "omega"]; + +/** The closed member set. An ActionIntent has exactly these and nothing else. */ +const MEMBERS = [ + "verb", + "target_substrate", + "args_commitment", + "input_commitments", + "requested_effects", +] as const; + +/** The ONE domain check. `validateIntent` and the canonical encoder both call + * it, because two guards that are supposed to agree and are written twice are + * two guards that will disagree. + * + * An earlier version repeated only the surrogate check inside the encoder and + * described itself as "refusing anything else". It did not: a runtime value + * with `requested_effects: [1]` was encoded as the JSON number `1` and given a + * digest, despite `validateIntent` rejecting it. In an authority path a + * commitment must be unreachable for any value the boundary would refuse. */ +function domainError(v: unknown): string | null { + if (!v || typeof v !== "object" || Array.isArray(v)) { + return "intent must be an object"; + } + const o = v as Record; + + // Extra members are REJECTED, not dropped. Silently discarding an unknown + // member means two different callers can commit to the same digest while + // believing they asked for different things. + const extra = Object.keys(o).filter( + (k) => !(MEMBERS as readonly string[]).includes(k), + ).sort(); + if (extra.length) { + return `unknown member(s): ${ + extra.join(", ") + }; the ActionIntent schema is closed`; + } + for (const k of MEMBERS) { + if (!(k in o)) return `${k} is missing`; + } + + if (typeof o.verb !== "string" || !o.verb.trim()) { + return "verb must be a non-empty string"; + } + if ( + typeof o.target_substrate !== "string" || + !SUBSTRATES.includes(o.target_substrate) + ) { + return `target_substrate must be one of: ${SUBSTRATES.join(", ")}`; + } + if (typeof o.args_commitment !== "string") { + return "args_commitment must be a string"; + } + for (const k of ["input_commitments", "requested_effects"] as const) { + const a = o[k]; + if (!Array.isArray(a) || !a.every((e) => typeof e === "string")) { + return `${k} must be an array of strings`; + } + } + + // Surrogates last, so a shape error is reported as a shape error. + const strings: [string, string][] = [ + ["verb", o.verb as string], + ["args_commitment", o.args_commitment as string], + ...(o.input_commitments as string[]).map(( + x, + i, + ): [string, string] => [`input_commitments[${i}]`, x]), + ...(o.requested_effects as string[]).map(( + x, + i, + ): [string, string] => [`requested_effects[${i}]`, x]), + ]; + for (const [where, value] of strings) { + const bad = unpairedSurrogateIndex(value); + if (bad >= 0) { + return `${where} contains an unpaired UTF-16 surrogate at index ${bad}; ` + + `strict I-JSON admits only Unicode scalar values, and the string has no ` + + `UTF-8 encoding a second implementation could reproduce`; + } + } + return null; +} + +/** Validate an untrusted value as an ActionIntent. Fail closed: anything + * missing, mistyped, carrying an unknown member, or holding an unpaired + * surrogate is rejected with a reason. */ +export function validateIntent( + v: unknown, +): { ok: true; intent: ActionIntent } | { ok: false; error: string } { + const err = domainError(v); + if (err) return { ok: false, error: err }; + const o = v as Record; + return { + ok: true, + intent: { + verb: o.verb as string, + target_substrate: o.target_substrate as ActionIntent["target_substrate"], + args_commitment: o.args_commitment as string, + input_commitments: o.input_commitments as string[], + requested_effects: o.requested_effects as string[], + }, + }; +} + +/** RFC-0003 Part 01 §5.1.2.1: the wire encoding and the numeric profile are + * separate identifiers and BOTH live inside the hashed root. A profile without + * a byte encoding does not determine a digest. */ +export const CANONICAL_ENCODING = "hsp-jcs@v0"; +export const NUMERIC_PROFILE = "cnp-0"; + +/** The index of the first unpaired UTF-16 surrogate, or -1. + * + * Strict I-JSON admits only Unicode scalar values, and RFC-0003 §5.1.3 names + * `unpaired-surrogate` as a rejection class for exactly this. The reason is + * interoperability rather than anything local: a lone surrogate has no UTF-8 + * encoding at all, so a second implementation cannot reproduce the commitment. + * Python raises `UnicodeEncodeError` on `'x\ud834y'.encode('utf-8')`; Rust + * cannot hold the value in a `String`. JavaScript is the outlier that accepts + * it, and a cross-substrate authority commitment must not depend on the + * quirks of the language that happens to compute it first. + * + * (It is NOT a same-language collision: ES2019 well-formed `JSON.stringify` + * escapes a lone surrogate to `\udXXX`, so the canonical text differs from the + * U+FFFD text. Measured before this comment was written.) */ +export function unpairedSurrogateIndex(s: string): number { + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i); + if (c >= 0xd800 && c <= 0xdbff) { + const next = i + 1 < s.length ? s.charCodeAt(i + 1) : 0; + if (next < 0xdc00 || next > 0xdfff) return i; + i++; // a well-formed pair; skip the low half + } else if (c >= 0xdc00 && c <= 0xdfff) { + return i; // a low surrogate with no high half before it + } + } + return -1; +} + +/** CNP-0-JCS for the ActionIntent shape, and refusing anything else. + * + * Deliberately not a general encoder: this domain is a map of string keys to + * strings and arrays of strings, with no numbers, and a serializer that quietly + * handled more than its callers can produce would be untested surface in an + * authority path. Trinity's `conformance/cnp-0-jcs-v0/` kit is where a general + * implementation gets measured. + * + * Member names are ordered by UTF-16 code unit (RFC 8785 §3.2.3), which is what + * JavaScript's default string comparison already does — written explicitly here + * because "correct by coincidence" is not a property anyone can check. */ +function jcsString(s: string): string { + const bad = unpairedSurrogateIndex(s); + if (bad >= 0) { + throw new RangeError( + `unpaired-surrogate at index ${bad}: the value has no UTF-8 encoding and ` + + `cannot be reproduced by a second implementation`, + ); + } + // ES2019 well-formed JSON.stringify implements RFC 8785 §3.2.2.2 escaping for + // well-formed input: shortest form for \b \f \n \r \t, \u00XX for other + // controls, literal otherwise. Verified against the rule, not assumed. + return JSON.stringify(s); +} + +function jcsValue(v: string | string[]): string { + if (typeof v === "string") return jcsString(v); + if (Array.isArray(v)) return `[${v.map(jcsString).join(",")}]`; + throw new TypeError("ActionIntent members are strings or arrays of strings"); +} + +/** `requested_effects` is a SET: deduplicated, then ordered. + * + * Sorting alone is not set semantics, and the previous code claimed it was — + * `["write","write"]` and `["write"]` request the same effect and produced two + * different commitments, so an authority grant could be made to depend on how + * many times a caller happened to name an effect. Duplicates are canonicalised + * away rather than rejected, because a repeated effect is not an error about + * what was asked for; it is the same ask written twice. + * + * Ordering is by UTF-16 code unit, matching RFC 8785's member ordering, which + * is what JavaScript's default string comparison already does — written out + * because "correct by coincidence" is not a property anyone can check. */ +function canonicalEffects(effects: string[]): string[] { + return [...new Set(effects)].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); +} + +/** The canonical bytes' text form. Exported so a test can assert the BYTES + * rather than only the digest — a digest test passes for two implementations + * that agree on a wrong encoding, and says nothing about which bytes either + * produced. */ +export function canonicalIntentText(intent: ActionIntent): string { + // The SAME domain check the boundary uses. Not a repeat of part of it. + const err = domainError(intent); + if (err) { + throw new RangeError(`ActionIntent is outside its domain: ${err}`); + } + const root: Record = { + args_commitment: intent.args_commitment, + canonical_encoding: CANONICAL_ENCODING, + input_commitments: intent.input_commitments, // ORDER PRESERVED + numeric_profile: NUMERIC_PROFILE, + requested_effects: canonicalEffects(intent.requested_effects), + target_substrate: intent.target_substrate, + verb: intent.verb, + }; + const names = Object.keys(root).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); + return `{${ + names.map((k) => `${jcsString(k)}:${jcsValue(root[k])}`).join(",") + }}`; +} + +/** The canonical bytes themselves: UTF-8, no BOM. */ +export function canonicalIntentBytes( + intent: ActionIntent, +): Uint8Array { + return new TextEncoder().encode(canonicalIntentText(intent)); +} + +async function sha256Bytes(b: Uint8Array): Promise { + const d = await crypto.subtle.digest("SHA-256", b); + return Array.from(new Uint8Array(d)).map((x) => + x.toString(16).padStart(2, "0") + ).join(""); +} + +// The PROPOSAL-BODY digest, deliberately NOT migrated. It recomputes a +// commitment MYC already wrote with this same stringification, so changing it +// here alone would reject every existing proposal — and migrating it is a +// separate slice with its own stored-state question. Only ActionIntent adopts +// CNP-0-JCS in this commit. type Json = null | boolean | number | string | Json[] | { [k: string]: Json }; function stable(v: Json): string { if (v === null) return "null"; @@ -69,21 +299,22 @@ async function sha256(s: string): Promise { const d = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(s)); return Array.from(new Uint8Array(d)).map((b) => b.toString(16).padStart(2, "0") - ) - .join(""); + ).join(""); } -/** The content commitment of a normalized intent (codex §1+§5). VENDORED byte-for- - * byte from MYC's canonical contract `myc/src/x5820_action_intent.ts` (MYC owns it, - * Trinity cannot static-import the submodule under CI decoupling). A shared - * known-answer vector pins both sides — see warrant_test.ts + x5820_action_intent_test.ts. */ + +/** The content commitment of a normalized intent. SHA-256 over CNP-0-JCS + * canonical bytes — RFC-0003 Part 01 §5.1, Tranche A3, ratified 2026-08-26. + * + * VENDORED byte-for-byte from MYC's canonical contract + * `myc/src/x5820_action_intent.ts` (MYC owns it; Trinity cannot static-import + * the submodule under CI decoupling). Two things pin the copy, and the second + * is the one that matters: a shared known-answer vector in both suites, AND a + * LIVE parity test that executes both implementations and compares their + * canonical BYTES when the submodule is present — see + * `src/action_intent_parity_test.ts`. Two matching constants in two files prove + * the constants match; only running both proves the implementations do. */ export async function intentCommitment(intent: ActionIntent): Promise { - return await sha256(stable({ - args_commitment: intent.args_commitment, - input_commitments: intent.input_commitments, // ORDER PRESERVED (codex §5) - requested_effects: [...intent.requested_effects].sort(), // a set - target_substrate: intent.target_substrate, - verb: intent.verb, - })); + return await sha256Bytes(canonicalIntentBytes(intent)); } /** The committed proposal descriptor — the PROOF (not the lifecycle summary, which @@ -234,9 +465,20 @@ async function runCli(args: string[] = Deno.args): Promise { // proposer can mint the action_grant a future warrant will match. ONE algorithm, // here, never re-implemented on the propose side. if (sub === "intent" && fqdn) { + // VALIDATE runtime input before it can reach a commitment. Parsing JSON + // says the bytes are JSON; it says nothing about whether they are an + // ActionIntent. Without this the encoder throws a bare RangeError at the + // caller, which is a failure but not a reason. let intent: ActionIntent; try { - intent = JSON.parse(await Deno.readTextFile(fqdn)); + const raw = JSON.parse(await Deno.readTextFile(fqdn)); + const v = validateIntent(raw); + if (!v.ok) { + console.error(`# error: invalid intent: ${v.error}`); + Deno.exitCode = 1; + return; + } + intent = v.intent; } catch { console.error(`# error: could not read intent from ${fqdn}`); Deno.exitCode = 1; @@ -284,7 +526,14 @@ async function runCli(args: string[] = Deno.args): Promise { } let intent: ActionIntent; try { - intent = JSON.parse(await Deno.readTextFile(ipath)); + const raw = JSON.parse(await Deno.readTextFile(ipath)); + const v = validateIntent(raw); + if (!v.ok) { + console.error(`# error: invalid intent: ${v.error}`); + Deno.exitCode = 1; + return; + } + intent = v.intent; } catch { console.error(`# error: could not read intent from ${ipath}`); Deno.exitCode = 1; diff --git a/src/x8788_network.myc.md b/src/x8788_network.myc.md index 59cf0037..71b30e86 100644 --- a/src/x8788_network.myc.md +++ b/src/x8788_network.myc.md @@ -1,9 +1,9 @@ - + # The FQDN network -**2385 nodes across 4 substrates.** A federation of living substrates: every +**2387 nodes across 4 substrates.** A federation of living substrates: every organ sits at a fixed hex coordinate (by role, not by content), every chord is a signed provenance event, and every edge — what a node hears, closes, references, imports — is git- and crypto-checkable. Nothing here is hand-placed; it is @@ -17,9 +17,9 @@ from the ledger, never edited. | trinity | 1076 | 835 | | omega | 633 | 0 | | liquid | 436 | 0 | -| myc | 240 | 0 | +| myc | 242 | 0 | -Made of: 835 chord, 648 organ, 334 other, 255 doc, 205 test, 62 rust, 41 data, 5 +Made of: 835 chord, 648 organ, 336 other, 255 doc, 205 test, 62 rust, 41 data, 5 script. **Most-cited — what the network leans on:** diff --git a/src/x88E0_readme_bootstrap.myc.md b/src/x88E0_readme_bootstrap.myc.md index 50218928..5596a7c8 100644 --- a/src/x88E0_readme_bootstrap.myc.md +++ b/src/x88E0_readme_bootstrap.myc.md @@ -1,6 +1,6 @@ - + diff --git a/src/x88F0_agents_bootstrap.myc.md b/src/x88F0_agents_bootstrap.myc.md index 8ed4c55f..bf5a14d7 100644 --- a/src/x88F0_agents_bootstrap.myc.md +++ b/src/x88F0_agents_bootstrap.myc.md @@ -1,6 +1,6 @@ - + diff --git a/src/x8CF0_skills_bootstrap.myc.md b/src/x8CF0_skills_bootstrap.myc.md index cc8aa984..c5f0cac9 100644 --- a/src/x8CF0_skills_bootstrap.myc.md +++ b/src/x8CF0_skills_bootstrap.myc.md @@ -1,6 +1,6 @@ - + From a92addf46164b4d51d169f0557b23e62002e5815 Mon Sep 17 00:00:00 2001 From: s0fractal Date: Thu, 27 Aug 2026 00:04:16 +0300 Subject: [PATCH 2/4] chore(projections): regenerate for the ActionIntent adoption x8788_network reflects the new cross-substrate parity surface. Separate commit from the adoption itself. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01DRHMVB9rUYNggn4wLmc1An --- src/x8788_network.myc.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/x8788_network.myc.md b/src/x8788_network.myc.md index 71b30e86..fff727da 100644 --- a/src/x8788_network.myc.md +++ b/src/x8788_network.myc.md @@ -1,9 +1,9 @@ - + # The FQDN network -**2387 nodes across 4 substrates.** A federation of living substrates: every +**2388 nodes across 4 substrates.** A federation of living substrates: every organ sits at a fixed hex coordinate (by role, not by content), every chord is a signed provenance event, and every edge — what a node hears, closes, references, imports — is git- and crypto-checkable. Nothing here is hand-placed; it is @@ -14,12 +14,12 @@ from the ledger, never edited. | substrate | nodes | chords | | --------- | ----: | -----: | -| trinity | 1076 | 835 | +| trinity | 1077 | 835 | | omega | 633 | 0 | | liquid | 436 | 0 | | myc | 242 | 0 | -Made of: 835 chord, 648 organ, 336 other, 255 doc, 205 test, 62 rust, 41 data, 5 +Made of: 835 chord, 648 organ, 336 other, 255 doc, 206 test, 62 rust, 41 data, 5 script. **Most-cited — what the network leans on:** From 2db70569551a186b106765e03d00bed2bf2c05ef Mon Sep 17 00:00:00 2001 From: s0fractal Date: Thu, 27 Aug 2026 00:47:15 +0300 Subject: [PATCH 3/4] fix(x5E10): strict raw-byte intake, one snapshot, and a cross-stored E2E MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three P1s from the joint gate round 1/2. All reproduced before fixing. **Raw JSON erased forbidden evidence before validation.** `warrant intent` and `warrant admit --intent` did `JSON.parse(await Deno.readTextFile(...))`, which collapses duplicate member names last-wins and replaces invalid UTF-8 with U+FFFD. `{"verb":"deny","verb":"apply",...}` emitted ccc26b8… at exit 0. Both boundaries now take raw bytes through the vendored `parseActionIntentBytes`: UTF-8 decoded fatally, duplicate top-level member names detected before `JSON.parse` can collapse them — names decoded first, so `"verb"` cannot smuggle a second `verb` past the check — then the closed domain normalization. These are the rejection classes §5.1.1 rule 4 exists for, and this project's own conformance kit says they are "properties of the input bytes that a permissive JSON parser resolves before you can see them". The authority boundary was using a permissive parser. **Check-then-reread.** `normalizeIntent` now reads every property exactly once into owned copies, arrays included, and the encoder uses only that snapshot. A getter answering the validator `["write"]` and the encoder `[1]` produced canonical bytes containing a JSON number; it cannot now, on either side. **The stored proposal and the authority gate were never one path.** MYC proved storage; Trinity tested authority on a descriptor built in the test. The new live E2E runs MYC's `propose --action-intent` into a temp root, reads what was written to disk, computes the intent through Trinity, and passes the STORED descriptor to `actionBoundAuthority` — asserting the current commitment authorizes and the superseded one is `intent_mismatch`. It reads the grant where `readProposal` reads it (`body.action_grant`, `commitment.value`), because a test that invents its own shape passes while the real path finds nothing. Verified by drift: renaming MYC's numeric_profile value turns both the byte parity AND the stored-grant E2E red. Verified the submodule-absent path still reports SKIPPED, and CI already fails the job if SKIPPED appears while submodules are present. **Status disposition.** `adoption-evidenced: true` in Part 00, Part 01 §5.1.3 and Part 07 §18 — scoped to ONE named path, with the same entries saying the proposal-body digest and every other `stable()` still use the old stringification, so nothing may read it as "Trinity computes references under hsp-jcs@v0". `interop-confirmed` stays false: both implementations are under one maintainer, which is what that level exists to distinguish. A3 is untouched. Part 07 §18 records the five executed attacks alongside the adoption, because a state that records only the success teaches nothing. Pairs with myc e02d7f98637c1ea7fcc793c7023e3e015bbecf7e. ./t check READY: 582 tests, 367 chords valid. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01DRHMVB9rUYNggn4wLmc1An --- conformance/cnp-0-jcs-v0/MANIFEST.sha256 | 2 +- conformance/cnp-0-jcs-v0/SPEC-EXTRACT.md | 34 +- .../00-architecture-and-ratification-map.md | 17 +- .../01-canonical-identity-and-encoding.md | 22 +- .../07-revision-history.md | 63 ++++ myc | 2 +- src/action_intent_parity_test.ts | 174 ++++++++++ src/x5E10_warrant.ts | 306 +++++++++++++----- src/x88E0_readme_bootstrap.myc.md | 2 +- src/x88F0_agents_bootstrap.myc.md | 2 +- src/x8CF0_skills_bootstrap.myc.md | 2 +- 11 files changed, 512 insertions(+), 114 deletions(-) diff --git a/conformance/cnp-0-jcs-v0/MANIFEST.sha256 b/conformance/cnp-0-jcs-v0/MANIFEST.sha256 index c39a6e6a..fd4d802a 100644 --- a/conformance/cnp-0-jcs-v0/MANIFEST.sha256 +++ b/conformance/cnp-0-jcs-v0/MANIFEST.sha256 @@ -1,7 +1,7 @@ bfdc30247c4c399f9b9ab6ccd04c9de56785d5850f881b48064a1a69a8c6aa3d CONTRACT.md b040b943c3cd4b13e1e18fd776b8ceef98c43272ef264344b7d2cb715ad7c172 INTERFACE.md b19a92a73cbec859a853e4ed12c11dde7ab9c77269e311e16a4d901d9ba12e52 README.md -4f289ee10fcb3e5698b8823af6241291b34b326406354a40d79708371b39ab0d SPEC-EXTRACT.md +941acc21d2c1c59e928733e401c2083dc9ebebe1adcb72df6b745114a67a2d6a SPEC-EXTRACT.md 17aaf7ed6d92040b0d1c4b88f4157912c95029368b6d28e56f2144c103082e5a corpus/extended.ndjson dde58a183fa746900cc82de3f1935a09177483297ab16a650c2cba25c07e9e19 corpus/required.ndjson 7c7fee476b75768345e882c27bc24066a7f3896472de3b3023750c8221248041 run_conformance.py diff --git a/conformance/cnp-0-jcs-v0/SPEC-EXTRACT.md b/conformance/cnp-0-jcs-v0/SPEC-EXTRACT.md index 127ba540..ac7d8db2 100644 --- a/conformance/cnp-0-jcs-v0/SPEC-EXTRACT.md +++ b/conformance/cnp-0-jcs-v0/SPEC-EXTRACT.md @@ -13,7 +13,7 @@ RFC, not so it can replace it. ## §5.1.1 — Canonical encoding is normative, not an implementation detail - + The encoding MUST satisfy: @@ -43,7 +43,7 @@ The encoding MUST satisfy: ## §5.1.2 — Floating point - + In canonical form: @@ -72,7 +72,7 @@ In canonical form: ## §5.1.2 — Non-integer values inside an integers-only domain - + Two patterns are admissible. Both keep every number in the integer domain and both are exact. Each is a **tagged form**, carrying the reserved discriminator @@ -117,7 +117,7 @@ independent implementations will agree on. ## §5.1.2.1 — CNP-0-JCS: the ratified selection - + ##### 5.1.2.1 CNP-0-JCS: the ratified selection @@ -190,7 +190,7 @@ the path does not do and not what it is given. ## §5.1.2.2 — Fixed-point scale identity - + A fixed-point domain MUST bind one content-addressed scale descriptor of this shape: @@ -223,7 +223,7 @@ simplex, `Σ value_i` MUST equal `radix^places` exactly. ## §5.1.3 — Parity is proven, not assumed - + #### 5.1.3 Parity is proven, not assumed @@ -294,9 +294,13 @@ That list is the whole of A3. Two further states sit outside it, and neither blocks it, because ratifying a specification, running it, and having someone else run it are three different acts: -**Adoption-evidenced** — at least one substrate computing real references under -`hsp-jcs@v0` on a path that matters. Until it holds, no document may claim the -encoding is in use. +**Adoption-evidenced — TRUE since 2026-08-27.** `ActionIntent.intentCommitment` +computes its commitment over CNP-0-JCS canonical bytes in both Trinity +(`src/x5E10_warrant.ts`) and MYC (`myc/src/x5820_action_intent.ts`), and that +commitment is the authority gate: `actionBoundAuthority` permits actuation only +when a committed proposal's `action_grant.intent_commitment` equals it exactly. +A live end-to-end test authorizes the proposal MYC actually wrote to disk. This +is one path, not the whole substrate, and it is named rather than generalised. **Interop-confirmed** — at least two independently _maintained_ implementations, or adoption by a party outside this project, with parity evidence in both @@ -310,12 +314,14 @@ party _has_ verified, and the difference is exactly what this level names. That is a statement about the specification, not about the world it is meant to run in. The honest status is: -> **A3: RATIFIED. adoption-evidenced: false. interop-confirmed: false.** +> **A3: RATIFIED. adoption-evidenced: true (one authority path — ActionIntent). +> interop-confirmed: false.** -Anything depending on cross-substrate reference _equality_ still depends on -`adoption-evidenced`, which is false: no substrate computes references under -`hsp-jcs@v0` today. Ratification removed the specification-side blocker and -nothing else. +One authority path computes real references under `hsp-jcs@v0`; nothing else in +either substrate does. `adoption-evidenced` says the encoding is in use, not +that it is used everywhere, and no document may read it as the latter. +`interop-confirmed` remains **false**: both implementations are under one +maintainer, which is exactly what that level exists to distinguish. diff --git a/docs/rfc/0003-heterogeneous-state-protocol/00-architecture-and-ratification-map.md b/docs/rfc/0003-heterogeneous-state-protocol/00-architecture-and-ratification-map.md index ac558054..c3a20eac 100644 --- a/docs/rfc/0003-heterogeneous-state-protocol/00-architecture-and-ratification-map.md +++ b/docs/rfc/0003-heterogeneous-state-protocol/00-architecture-and-ratification-map.md @@ -61,10 +61,11 @@ standalone conformance kit all exist, and the steward closed the tranche. | **adoption-evidenced** | **false** | nothing may claim the encoding is in use | | **interop-confirmed** | **false** | nothing may claim independent interoperability or multi-implementation confirmation | -Anything depending on cross-substrate reference _equality_ still depends on -`adoption-evidenced`, which is false: no substrate computes references under -`hsp-jcs@v0` today. Ratification removed the specification-side blocker and -nothing else, and A3 being closed moves neither of the other two. +`adoption-evidenced` became **true** on 2026-08-27: +`ActionIntent.intentCommitment` computes over CNP-0-JCS canonical bytes in both +Trinity and MYC, and that value gates actuation. It says the encoding is in use +on one named path, not that it is used everywhere. `interop-confirmed` stays +**false** — both implementations are under one maintainer. **Substrate adoption was never a ratification condition.** It is its own state, `adoption-evidenced`, for the same reason the second encoder stopped being one: @@ -1474,10 +1475,10 @@ where a reference resolves: raw input and non-canonical ratios, and a reproducible conformance kit all exist, and the steward ratified the tranche. This settles the specification; it settles neither of the two states below. -- **Adoption-evidenced: false** (a state, not a tranche, and never a blocker on - A3). At least one substrate computing real references under `hsp-jcs@v0` on a - path that matters. Until it holds, no document may claim the encoding is in - use. +- **Adoption-evidenced: true** since 2026-08-27 (a state, not a tranche, and + never a blocker on A3). At least one substrate computing real references under + `hsp-jcs@v0` on a path that matters. Until it holds, no document may claim the + encoding is in use. - **Interop-confirmed: false** (a level above both, and never a blocker on either). Two independently maintained implementations, or adoption by a party outside this project, with parity evidence in both directions. Until it holds, diff --git a/docs/rfc/0003-heterogeneous-state-protocol/01-canonical-identity-and-encoding.md b/docs/rfc/0003-heterogeneous-state-protocol/01-canonical-identity-and-encoding.md index e9d24846..678956af 100644 --- a/docs/rfc/0003-heterogeneous-state-protocol/01-canonical-identity-and-encoding.md +++ b/docs/rfc/0003-heterogeneous-state-protocol/01-canonical-identity-and-encoding.md @@ -493,9 +493,13 @@ That list is the whole of A3. Two further states sit outside it, and neither blocks it, because ratifying a specification, running it, and having someone else run it are three different acts: -**Adoption-evidenced** — at least one substrate computing real references under -`hsp-jcs@v0` on a path that matters. Until it holds, no document may claim the -encoding is in use. +**Adoption-evidenced — TRUE since 2026-08-27.** `ActionIntent.intentCommitment` +computes its commitment over CNP-0-JCS canonical bytes in both Trinity +(`src/x5E10_warrant.ts`) and MYC (`myc/src/x5820_action_intent.ts`), and that +commitment is the authority gate: `actionBoundAuthority` permits actuation only +when a committed proposal's `action_grant.intent_commitment` equals it exactly. +A live end-to-end test authorizes the proposal MYC actually wrote to disk. This +is one path, not the whole substrate, and it is named rather than generalised. **Interop-confirmed** — at least two independently _maintained_ implementations, or adoption by a party outside this project, with parity evidence in both @@ -509,12 +513,14 @@ party _has_ verified, and the difference is exactly what this level names. That is a statement about the specification, not about the world it is meant to run in. The honest status is: -> **A3: RATIFIED. adoption-evidenced: false. interop-confirmed: false.** +> **A3: RATIFIED. adoption-evidenced: true (one authority path — ActionIntent). +> interop-confirmed: false.** -Anything depending on cross-substrate reference _equality_ still depends on -`adoption-evidenced`, which is false: no substrate computes references under -`hsp-jcs@v0` today. Ratification removed the specification-side blocker and -nothing else. +One authority path computes real references under `hsp-jcs@v0`; nothing else in +either substrate does. `adoption-evidenced` says the encoding is in use, not +that it is used everywhere, and no document may read it as the latter. +`interop-confirmed` remains **false**: both implementations are under one +maintainer, which is exactly what that level exists to distinguish. #### 5.1.4 The selection is narrower than it looks diff --git a/docs/rfc/0003-heterogeneous-state-protocol/07-revision-history.md b/docs/rfc/0003-heterogeneous-state-protocol/07-revision-history.md index 1c83f9ab..c307026d 100644 --- a/docs/rfc/0003-heterogeneous-state-protocol/07-revision-history.md +++ b/docs/rfc/0003-heterogeneous-state-protocol/07-revision-history.md @@ -1175,3 +1175,66 @@ correction is here rather than in the verdict, which stays untouched. None of this changes what the probe produced, which was nothing, or what it is evidence of, which is nothing. + +## 18. adoption-evidenced becomes true on one named path (2026-08-27) + +`ActionIntent.intentCommitment` computes its commitment over CNP-0-JCS canonical +bytes in both substrates. This is the first path in either to compute a real +`hsp-jcs@v0` reference rather than an ad-hoc stable stringification. + + {"args_commitment":"c1","canonical_encoding":"hsp-jcs@v0", + "input_commitments":["a","b"],"numeric_profile":"cnp-0", + "requested_effects":["receipt","write"],"target_substrate":"myc", + "verb":"apply"} + → ccc26b8b460fe2debf0ad069d55ec170a78b7b70861f1f54c03e401e4576c3be + +Chosen because it is an authority gate rather than a convenient hash: +`actionBoundAuthority` permits actuation only when a committed proposal's +`action_grant.intent_commitment` equals this value exactly. + +### What the state now says, and what it does not + +**`adoption-evidenced: true`** means the encoding is in use on **one named +path**. It does not mean the substrates have adopted it generally: the +proposal-body digest in the same file still uses the old stringification, and so +do the other `stable()` copies. Nothing may read this state as "Trinity computes +references under `hsp-jcs@v0`". + +**`interop-confirmed` remains false.** Both implementations are under one +maintainer, which is exactly what that level exists to distinguish. Trinity +vendors MYC's contract byte-for-byte; agreement between them is internal +consistency, not independent interoperability. + +**A3 is unaffected.** It was ratified on 2026-08-26 and this changes nothing +about it. Adoption is a separate state precisely so that running a specification +and ratifying it stay distinguishable. + +### The implementation evidence + +- both boundaries parse **raw bytes** strictly — UTF-8 decoded fatally, + duplicate member names detected before `JSON.parse` collapses them last-wins, + including escape-equivalent spellings such as `"verb"`; +- one normalization reads every property exactly once into owned copies, and the + encoder uses only that snapshot; +- a **live** parity test executes both implementations and compares canonical + text, canonical bytes and digests across eight divergence-prone inputs; +- a **live end-to-end** test authorizes the proposal MYC actually wrote to disk, + rather than a descriptor built in a test; +- the canonical bytes are judged canonical by the ratified verifier-only path, + which shares no code with the encoder. + +### How this was found to be wrong three times first + +The reviewer's executed attacks, each of which produced a real commitment or a +written proposal before it was closed: a duplicate `verb` accepted last-wins; an +escape-equivalent duplicate; a `0xff` byte becoming U+FFFD; a getter answering +the validator and the encoder differently; and an encoder that accepted +`requested_effects: [1]` while its own comment said it refused everything +outside the domain. + +The first three are the rejection classes §5.1.1 rule 4 exists for, and this +project's own conformance kit documents them as "properties of the input bytes +that a permissive JSON parser resolves before you can see them" — while the +authority boundary used a permissive parser. Adoption is recorded here together +with that, because a state that records only the success is a state that teaches +nothing. diff --git a/myc b/myc index ec6a754e..e02d7f98 160000 --- a/myc +++ b/myc @@ -1 +1 @@ -Subproject commit ec6a754e9c7f85a45efbe98aefd7a3bee9a955ae +Subproject commit e02d7f98637c1ea7fcc793c7023e3e015bbecf7e diff --git a/src/action_intent_parity_test.ts b/src/action_intent_parity_test.ts index 80862e44..8a891f8b 100644 --- a/src/action_intent_parity_test.ts +++ b/src/action_intent_parity_test.ts @@ -17,10 +17,12 @@ // skipped parity check is never counted as a passing one. import { assert, assertEquals } from "@std/assert"; +import { actionBoundAuthority } from "./x5E10_warrant.ts"; import { canonicalIntentBytes as trinityBytes, canonicalIntentText as trinityText, intentCommitment as trinityCommitment, + parseActionIntentBytes as trinityParse, validateIntent as trinityValidate, } from "./x5E10_warrant.ts"; @@ -32,6 +34,7 @@ const MYC_CONTRACT = new URL( type MycModule = { canonicalIntentText: (i: unknown) => string; canonicalIntentBytes: (i: unknown) => Uint8Array; + parseActionIntentBytes: (b: Uint8Array) => { ok: boolean; error?: string }; intentCommitment: (i: unknown) => Promise; validateIntent: (v: unknown) => { ok: boolean; error?: string }; }; @@ -178,3 +181,174 @@ Deno.test("action_intent parity - a missing profile identifier moves the commitm assert(hex !== EXPECTED, "removing " + id + " left the digest unchanged"); } }); + +Deno.test("action_intent parity - LIVE E2E: Trinity authorizes the proposal MYC actually wrote", async () => { + // The gap this closes: MYC's own E2E proved storage, and Trinity's authority + // tests used a descriptor built in the test. Nothing fed the STORED proposal + // into the authority comparison, so the two halves could agree separately + // about different things. + const myc = await loadMyc(); + if (!myc) { + console.log(" SKIPPED: myc submodule is not checked out"); + return; + } + const { runCli } = await import( + new URL("../myc/src/x5800_propose.ts", import.meta.url).href + ) as { runCli: (a: string[]) => Promise }; + + const root = await Deno.makeTempDir({ prefix: "e2e_intent_" }); + try { + const intentPath = `${root}/intent.json`; + await Deno.writeTextFile(intentPath, JSON.stringify(VECTOR)); + + await runCli([ + "--root", + root, + "--proposal", + "cross-substrate adoption e2e", + "--requires", + "trinity", + "--proposer", + "claude", + "--action-intent", + intentPath, + "--json", + ]); + + // 2. Read what was actually written, not what a helper returned. + const dir = `${root}/public/proposals`; + const names: string[] = []; + for await (const e of Deno.readDir(dir)) if (e.isFile) names.push(e.name); + assertEquals(names.length, 1, `expected one proposal, got ${names}`); + const written = await Deno.readTextFile(`${dir}/${names[0]}`); + const body = JSON.parse( + written.match(/```json myc\s*\n([\s\S]*?)\n```/)![1], + ); + // Read it exactly where readProposal reads it: the grant lives inside the + // descriptor's BODY, and the commitment is `commitment.value`. Mirroring the + // production reader matters — a test that invented its own shape would pass + // while the real path found nothing. + const storedGrant = body.body?.action_grant?.intent_commitment as string; + assert( + storedGrant, + `the stored proposal carries no action_grant: ${written.slice(0, 200)}`, + ); + + // 3. Compute the intent through TRINITY, independently of what MYC stored. + const trinityIC = await trinityCommitment(VECTOR); + assertEquals( + storedGrant, + trinityIC, + "the stored grant is not what Trinity computes for the same intent", + ); + + // 4. Pass the STORED descriptor to Trinity's authority gate. + const descriptor = { + fqdn: String(body.fqdn), + commitment: String(body.commitment?.value ?? body.commitment), + action_grant: { intent_commitment: storedGrant }, + }; + const ok = actionBoundAuthority(trinityIC, descriptor, "implemented"); + assertEquals(ok.authorized, true, `denied: ${ok.reason_code} ${ok.reason}`); + + // 5. And the superseded commitment does not authorize the same proposal. + const denied = actionBoundAuthority(SUPERSEDED, descriptor, "implemented"); + assertEquals(denied.authorized, false); + assertEquals(denied.reason_code, "intent_mismatch"); + } finally { + await Deno.remove(root, { recursive: true }); + } +}); + +Deno.test("action_intent parity - LIVE: both boundaries refuse duplicate names and invalid UTF-8 from raw bytes", async () => { + const myc = await loadMyc(); + if (!myc) { + console.log(" SKIPPED: myc submodule is not checked out"); + return; + } + const enc = (s: string) => new TextEncoder().encode(s); + const attacks: [string, Uint8Array, string][] = [ + [ + "duplicate member name", + enc( + '{"verb":"deny","verb":"apply","target_substrate":"myc",' + + '"args_commitment":"c1","input_commitments":["a"],' + + '"requested_effects":["write"]}', + ), + "duplicate-member-name", + ], + [ + "escaped duplicate name", + enc( + '{"verb":"deny","ve\\u0072b":"apply","target_substrate":"myc",' + + '"args_commitment":"c1","input_commitments":["a"],' + + '"requested_effects":["write"]}', + ), + "duplicate-member-name", + ], + [ + "invalid UTF-8", + new Uint8Array([ + ...enc('{"verb":"ap'), + 0xff, + ...enc( + 'ly","target_substrate":"myc","args_commitment":"c1",' + + '"input_commitments":["a"],"requested_effects":["write"]}', + ), + ]), + "invalid-utf8", + ], + ]; + for (const [name, bytes, expect] of attacks) { + const t = trinityParse(bytes); + const m = myc.parseActionIntentBytes(bytes); + assertEquals(t.ok, false, `${name}: trinity admitted it`); + assertEquals(m.ok, false, `${name}: myc admitted it`); + if (!t.ok) assert(t.error.includes(expect), `${name}: trinity: ${t.error}`); + if (!m.ok) { + assert(String(m.error).includes(expect), `${name}: myc: ${m.error}`); + } + } +}); + +Deno.test("action_intent parity - LIVE: a changing getter cannot reach canonical bytes on either side", async () => { + const myc = await loadMyc(); + if (!myc) { + console.log(" SKIPPED: myc submodule is not checked out"); + return; + } + const mk = (badFrom: number) => { + let reads = 0; + const evil: Record = { + verb: "apply", + target_substrate: "myc", + args_commitment: "c1", + input_commitments: ["a"], + }; + Object.defineProperty(evil, "requested_effects", { + enumerable: true, + get() { + reads++; + return reads >= badFrom ? [1] : ["write"]; + }, + }); + return evil; + }; + for (const from of [1, 2, 3]) { + for ( + const [who, text] of [ + ["trinity", trinityText], + ["myc", myc.canonicalIntentText], + ] as const + ) { + let out = ""; + try { + out = (text as (i: unknown) => string)(mk(from)); + } catch { /* refusing is the other correct answer */ } + assert( + !out.includes("[1]"), + `${who}: canonical text carried a number: ${out}`, + ); + } + } +}); diff --git a/src/x5E10_warrant.ts b/src/x5E10_warrant.ts index 81d60ed6..c3cfbeb4 100644 --- a/src/x5E10_warrant.ts +++ b/src/x5E10_warrant.ts @@ -65,64 +65,99 @@ const MEMBERS = [ "requested_effects", ] as const; -/** The ONE domain check. `validateIntent` and the canonical encoder both call - * it, because two guards that are supposed to agree and are written twice are - * two guards that will disagree. +/** Normalize an untrusted value into an OWNED ActionIntent, or say why not. * - * An earlier version repeated only the surrogate check inside the encoder and - * described itself as "refusing anything else". It did not: a runtime value - * with `requested_effects: [1]` was encoded as the JSON number `1` and given a - * digest, despite `validateIntent` rejecting it. In an authority path a - * commitment must be unreachable for any value the boundary would refuse. */ -function domainError(v: unknown): string | null { + * One operation, and it reads every property EXACTLY ONCE into a local. The + * previous shape was check-then-reread: `domainError(intent)` validated the + * properties and `canonicalIntentText` read them again, so an accessor could + * return `["write"]` to the validator and `[1]` to the encoder. That produced + * canonical bytes containing a JSON number and a digest over them. Anything + * built from a re-read of caller-controlled input is built from a value the + * check never saw. + * + * Arrays are copied, not aliased, for the same reason: a caller keeping a + * reference could mutate one after validation. + * + * It is also the ONE domain check — `validateIntent` and the canonical encoder + * both go through it. Two guards meant to agree, written twice, disagree: an + * earlier encoder repeated only the surrogate check while calling itself + * "refusing anything else", and encoded `requested_effects: [1]`. */ +function normalizeIntent( + v: unknown, +): { ok: true; intent: ActionIntent } | { ok: false; error: string } { + const no = (error: string) => ({ ok: false as const, error }); + if (!v || typeof v !== "object" || Array.isArray(v)) { - return "intent must be an object"; + return no("intent must be an object"); } const o = v as Record; // Extra members are REJECTED, not dropped. Silently discarding an unknown - // member means two different callers can commit to the same digest while - // believing they asked for different things. - const extra = Object.keys(o).filter( - (k) => !(MEMBERS as readonly string[]).includes(k), - ).sort(); + // member lets two callers commit to one digest believing they asked for + // different things. + const keys = Object.keys(o); + const extra = keys.filter((k) => !(MEMBERS as readonly string[]).includes(k)) + .sort(); if (extra.length) { - return `unknown member(s): ${ - extra.join(", ") - }; the ActionIntent schema is closed`; + return no( + `unknown member(s): ${ + extra.join(", ") + }; the ActionIntent schema is closed`, + ); } for (const k of MEMBERS) { - if (!(k in o)) return `${k} is missing`; + if (!keys.includes(k)) return no(`${k} is missing`); } - if (typeof o.verb !== "string" || !o.verb.trim()) { - return "verb must be a non-empty string"; + // ONE read per property, into a local. Everything below uses the local. + const verb = o.verb; + const target_substrate = o.target_substrate; + const args_commitment = o.args_commitment; + const input_raw = o.input_commitments; + const effects_raw = o.requested_effects; + + if (typeof verb !== "string" || !verb.trim()) { + return no("verb must be a non-empty string"); } if ( - typeof o.target_substrate !== "string" || - !SUBSTRATES.includes(o.target_substrate) + typeof target_substrate !== "string" || + !SUBSTRATES.includes(target_substrate) ) { - return `target_substrate must be one of: ${SUBSTRATES.join(", ")}`; + return no(`target_substrate must be one of: ${SUBSTRATES.join(", ")}`); + } + if (typeof args_commitment !== "string") { + return no("args_commitment must be a string"); } - if (typeof o.args_commitment !== "string") { - return "args_commitment must be a string"; + if (!Array.isArray(input_raw)) { + return no("input_commitments must be an array of strings"); } - for (const k of ["input_commitments", "requested_effects"] as const) { - const a = o[k]; - if (!Array.isArray(a) || !a.every((e) => typeof e === "string")) { - return `${k} must be an array of strings`; + if (!Array.isArray(effects_raw)) { + return no("requested_effects must be an array of strings"); + } + // Copy first, then check the COPY. Checking the original and copying after + // would re-read every element. + const input_commitments = Array.from(input_raw) as unknown[]; + const requested_effects = Array.from(effects_raw) as unknown[]; + for ( + const [name, arr] of [ + ["input_commitments", input_commitments], + ["requested_effects", requested_effects], + ] as const + ) { + if (!arr.every((e) => typeof e === "string")) { + return no(`${name} must be an array of strings`); } } - // Surrogates last, so a shape error is reported as a shape error. + // Surrogates last, so a shape error reports as a shape error. const strings: [string, string][] = [ - ["verb", o.verb as string], - ["args_commitment", o.args_commitment as string], - ...(o.input_commitments as string[]).map(( + ["verb", verb], + ["args_commitment", args_commitment], + ...(input_commitments as string[]).map(( x, i, ): [string, string] => [`input_commitments[${i}]`, x]), - ...(o.requested_effects as string[]).map(( + ...(requested_effects as string[]).map(( x, i, ): [string, string] => [`requested_effects[${i}]`, x]), @@ -130,35 +165,34 @@ function domainError(v: unknown): string | null { for (const [where, value] of strings) { const bad = unpairedSurrogateIndex(value); if (bad >= 0) { - return `${where} contains an unpaired UTF-16 surrogate at index ${bad}; ` + - `strict I-JSON admits only Unicode scalar values, and the string has no ` + - `UTF-8 encoding a second implementation could reproduce`; + return no( + `${where} contains an unpaired UTF-16 surrogate at index ${bad}; ` + + `strict I-JSON admits only Unicode scalar values, and the string has ` + + `no UTF-8 encoding a second implementation could reproduce`, + ); } } - return null; -} -/** Validate an untrusted value as an ActionIntent. Fail closed: anything - * missing, mistyped, carrying an unknown member, or holding an unpaired - * surrogate is rejected with a reason. */ -export function validateIntent( - v: unknown, -): { ok: true; intent: ActionIntent } | { ok: false; error: string } { - const err = domainError(v); - if (err) return { ok: false, error: err }; - const o = v as Record; return { ok: true, intent: { - verb: o.verb as string, - target_substrate: o.target_substrate as ActionIntent["target_substrate"], - args_commitment: o.args_commitment as string, - input_commitments: o.input_commitments as string[], - requested_effects: o.requested_effects as string[], + verb, + target_substrate: target_substrate as ActionIntent["target_substrate"], + args_commitment, + input_commitments: input_commitments as string[], + requested_effects: requested_effects as string[], }, }; } +/** Validate an untrusted value as an ActionIntent. Fail closed, and return an + * OWNED normalization the caller can encode from safely. */ +export function validateIntent( + v: unknown, +): { ok: true; intent: ActionIntent } | { ok: false; error: string } { + return normalizeIntent(v); +} + /** RFC-0003 Part 01 §5.1.2.1: the wire encoding and the numeric profile are * separate identifiers and BOTH live inside the hashed root. A profile without * a byte encoding does not determine a digest. */ @@ -245,19 +279,22 @@ function canonicalEffects(effects: string[]): string[] { * that agree on a wrong encoding, and says nothing about which bytes either * produced. */ export function canonicalIntentText(intent: ActionIntent): string { - // The SAME domain check the boundary uses. Not a repeat of part of it. - const err = domainError(intent); - if (err) { - throw new RangeError(`ActionIntent is outside its domain: ${err}`); + // Normalize ONCE, then encode only from the snapshot. The caller's object is + // never read again: a getter that answered the validator and then answered the + // encoder differently is how `requested_effects: [1]` reached canonical bytes. + const n = normalizeIntent(intent); + if (!n.ok) { + throw new RangeError(`ActionIntent is outside its domain: ${n.error}`); } + const i = n.intent; const root: Record = { - args_commitment: intent.args_commitment, + args_commitment: i.args_commitment, canonical_encoding: CANONICAL_ENCODING, - input_commitments: intent.input_commitments, // ORDER PRESERVED + input_commitments: i.input_commitments, // ORDER PRESERVED numeric_profile: NUMERIC_PROFILE, - requested_effects: canonicalEffects(intent.requested_effects), - target_substrate: intent.target_substrate, - verb: intent.verb, + requested_effects: canonicalEffects(i.requested_effects), + target_substrate: i.target_substrate, + verb: i.verb, }; const names = Object.keys(root).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); return `{${ @@ -317,6 +354,113 @@ export async function intentCommitment(intent: ActionIntent): Promise { return await sha256Bytes(canonicalIntentBytes(intent)); } +/** Parse ActionIntent JSON from RAW BYTES, strictly. + * + * `JSON.parse(await Deno.readTextFile(p))` destroys the evidence two ratified + * rejection classes are about, before any validator can see it: + * + * * **duplicate member names** collapse last-wins, so + * `{"verb":"deny","verb":"apply"}` parses as `apply` and a proposal is + * written for an intent that also said `deny`; + * * **invalid UTF-8** becomes U+FFFD during decoding, so a commitment is made + * over a replacement character the caller never wrote. + * + * RFC-0003 §5.1.1 rule 4 exists for exactly this, and the conformance kit's own + * interface says three rejection categories "describe properties of the input + * BYTES that a permissive JSON parser resolves before you can see them". This + * boundary was using a permissive parser anyway. + * + * Escape-equivalence matters: `"verb"` and `"verb"` are the same member + * name, so names are decoded before they are compared. Only the top-level + * object is scanned, which is the whole of this schema — nested objects are + * refused by the domain check that follows. */ +export function parseActionIntentBytes( + bytes: Uint8Array, +): { ok: true; intent: ActionIntent } | { ok: false; error: string } { + let text: string; + try { + text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); + } catch { + return { + ok: false, + error: + "invalid-utf8: the input is not well-formed UTF-8; a permissive decode " + + "would commit to a replacement character nobody wrote", + }; + } + + const dup = duplicateTopLevelMember(text); + if (dup) { + return { + ok: false, + error: + `duplicate-member-name: ${ + JSON.stringify(dup) + } appears more than once; ` + + "a permissive parser keeps the last one and the other becomes invisible", + }; + } + + let raw: unknown; + try { + raw = JSON.parse(text); + } catch (e) { + return { ok: false, error: `syntax: ${(e as Error).message}` }; + } + return normalizeIntent(raw); +} + +/** The first top-level member name that appears twice, or null. + * + * A minimal scanner rather than a JSON parser: it tracks string literals and + * nesting depth, collects names at depth 1, and decodes each before comparing, + * so an escaped spelling cannot smuggle a second copy past the check. */ +function duplicateTopLevelMember(text: string): string | null { + const seen = new Set(); + let depth = 0; + let i = 0; + let expectName = false; + + while (i < text.length) { + const c = text[i]; + if (c === '"') { + const start = i; + i++; + while (i < text.length) { + if (text[i] === "\\") i += 2; + else if (text[i] === '"') break; + else i++; + } + const literal = text.slice(start, i + 1); + i++; + // A member name is a string followed (after whitespace) by ':'. + let j = i; + while (j < text.length && /\s/.test(text[j])) j++; + const isName = text[j] === ":"; + if (isName && depth === 1 && expectName) { + let name: string; + try { + name = JSON.parse(literal) as string; + } catch { + return null; // malformed; JSON.parse below will report it + } + if (seen.has(name)) return name; + seen.add(name); + } + continue; + } + if (c === "{" || c === "[") { + depth++; + expectName = c === "{"; + } else if (c === "}" || c === "]") { + depth--; + expectName = depth === 1; + } + i++; + } + return null; +} + /** The committed proposal descriptor — the PROOF (not the lifecycle summary, which * is only an index). */ export interface ProposalDescriptor { @@ -469,21 +613,24 @@ async function runCli(args: string[] = Deno.args): Promise { // says the bytes are JSON; it says nothing about whether they are an // ActionIntent. Without this the encoder throws a bare RangeError at the // caller, which is a failure but not a reason. + // RAW BYTES, strictly: reading text and calling JSON.parse erases duplicate + // member names and invalid UTF-8 before any validator can refuse them. let intent: ActionIntent; + let bytes: Uint8Array; try { - const raw = JSON.parse(await Deno.readTextFile(fqdn)); - const v = validateIntent(raw); - if (!v.ok) { - console.error(`# error: invalid intent: ${v.error}`); - Deno.exitCode = 1; - return; - } - intent = v.intent; + bytes = await Deno.readFile(fqdn); } catch { console.error(`# error: could not read intent from ${fqdn}`); Deno.exitCode = 1; return; } + const parsed = parseActionIntentBytes(bytes); + if (!parsed.ok) { + console.error(`# error: invalid intent: ${parsed.error}`); + Deno.exitCode = 1; + return; + } + intent = parsed.intent; console.log(JSON.stringify( { type: "intent_commitment", @@ -525,20 +672,21 @@ async function runCli(args: string[] = Deno.args): Promise { return; } let intent: ActionIntent; + let ibytes: Uint8Array; try { - const raw = JSON.parse(await Deno.readTextFile(ipath)); - const v = validateIntent(raw); - if (!v.ok) { - console.error(`# error: invalid intent: ${v.error}`); - Deno.exitCode = 1; - return; - } - intent = v.intent; + ibytes = await Deno.readFile(ipath); } catch { console.error(`# error: could not read intent from ${ipath}`); Deno.exitCode = 1; return; } + const parsedAdmit = parseActionIntentBytes(ibytes); + if (!parsedAdmit.ok) { + console.error(`# error: invalid intent: ${parsedAdmit.error}`); + Deno.exitCode = 1; + return; + } + intent = parsedAdmit.intent; const [descriptor, fs, ic] = await Promise.all([ readProposal(fqdn), finalState(fqdn), diff --git a/src/x88E0_readme_bootstrap.myc.md b/src/x88E0_readme_bootstrap.myc.md index 5596a7c8..e65392f6 100644 --- a/src/x88E0_readme_bootstrap.myc.md +++ b/src/x88E0_readme_bootstrap.myc.md @@ -1,6 +1,6 @@ - + diff --git a/src/x88F0_agents_bootstrap.myc.md b/src/x88F0_agents_bootstrap.myc.md index bf5a14d7..18c9349e 100644 --- a/src/x88F0_agents_bootstrap.myc.md +++ b/src/x88F0_agents_bootstrap.myc.md @@ -1,6 +1,6 @@ - + diff --git a/src/x8CF0_skills_bootstrap.myc.md b/src/x8CF0_skills_bootstrap.myc.md index c5f0cac9..b0e1b9ad 100644 --- a/src/x8CF0_skills_bootstrap.myc.md +++ b/src/x8CF0_skills_bootstrap.myc.md @@ -1,6 +1,6 @@ - + From 87274a29d977368423f34040beec958dace9b5f5 Mon Sep 17 00:00:00 2001 From: s0fractal Date: Thu, 27 Aug 2026 00:57:31 +0300 Subject: [PATCH 4/4] docs(rfc-0003): reconcile adoption status --- .../00-architecture-and-ratification-map.md | 35 ++++++++++--------- myc | 2 +- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/docs/rfc/0003-heterogeneous-state-protocol/00-architecture-and-ratification-map.md b/docs/rfc/0003-heterogeneous-state-protocol/00-architecture-and-ratification-map.md index c3a20eac..f40ad4c0 100644 --- a/docs/rfc/0003-heterogeneous-state-protocol/00-architecture-and-ratification-map.md +++ b/docs/rfc/0003-heterogeneous-state-protocol/00-architecture-and-ratification-map.md @@ -53,12 +53,12 @@ on 2026-08-26** (Part 01 §5.1.3, Part 07 §17): the contract, the CNP-0 corpus, reference encoder, a verifier-only rejection path sharing no code with it, and a standalone conformance kit all exist, and the steward closed the tranche. -**Three independent states, and only the first has changed:** +**Three independent states; the first two have now changed:** | state | value | what it licenses | | ---------------------- | ------------ | ----------------------------------------------------------------------------------- | | **A3** | **RATIFIED** | the encoding is determined and checkable | -| **adoption-evidenced** | **false** | nothing may claim the encoding is in use | +| **adoption-evidenced** | **true** | the named ActionIntent authority path may claim the encoding is in use | | **interop-confirmed** | **false** | nothing may claim independent interoperability or multi-implementation confirmation | `adoption-evidenced` became **true** on 2026-08-27: @@ -711,7 +711,8 @@ rather than "proved" for that reason. with it, a standalone conformance kit, and steward ratification. All six exist, and **the steward ratified A3 on 2026-08-26** — see Part 07 §17 for the statement. Federation adoption was **not** among them and remains tracked - separately as `adoption-evidenced`, which is false. + separately as `adoption-evidenced`, which became true for the named + ActionIntent authority path on 2026-08-27. **Restated 2026-08-26: two levels, not one.** This entry previously required **two independent encoders** for A3. That conflated four separable things — @@ -794,9 +795,9 @@ specification, not the world it runs in. Specifically: 2. **Ratification is not adoption authority.** Part 01 selects CNP-0-JCS and the steward ratified it. That does not make Warrant the owner of federation encoding, import its governance, or bind substrates that have not adopted and - implemented the contract. `adoption-evidenced` is **false**: no substrate - computes references under `hsp-jcs@v0` today, and A3 being closed does not - change that. + implemented the contract. `adoption-evidenced` is **true only for the named + ActionIntent authority path**: it does not license a claim that either + substrate generally computes references under `hsp-jcs@v0`. 3. **Version pinning is mandatory if adopted.** `ski@v1` names Book I v0.5 specifically, and `GOV-ANCHORS` pins its dependencies by content hash for the stated reason that a STANDARD must not rest on a moving target. Any adoption @@ -1476,9 +1477,9 @@ where a reference resolves: exist, and the steward ratified the tranche. This settles the specification; it settles neither of the two states below. - **Adoption-evidenced: true** since 2026-08-27 (a state, not a tranche, and - never a blocker on A3). At least one substrate computing real references under - `hsp-jcs@v0` on a path that matters. Until it holds, no document may claim the - encoding is in use. + never a blocker on A3). The named ActionIntent authority path computes real + references under `hsp-jcs@v0`; this licenses that bounded adoption claim and + no substrate-wide one. - **Interop-confirmed: false** (a level above both, and never a blocker on either). Two independently maintained implementations, or adoption by a party outside this project, with parity evidence in both directions. Until it holds, @@ -1489,10 +1490,10 @@ where a reference resolves: content-addressed principal and custody records, and fail unresolved or shared custody closed against double-counting (§22.1). -Tranche A is a prerequisite for every other tranche. Until A2 and A3 land, -§5.1's guarantees do not hold across substrate boundaries and nothing that -depends on cross-substrate reference equality should be claimed. A draft choice -reduces design uncertainty; it does not weaken this gate. +Tranche A is a prerequisite for every other tranche. A2 and A3 have landed, but +their guarantees extend only as far as the separately reported adoption and +interoperability evidence. The named ActionIntent path supplies bounded adoption +evidence; independent interoperability remains unconfirmed. ### Tranche B — State-domain typing (depends on A) @@ -1616,9 +1617,11 @@ What matters going forward rather than backward: 1. **Ratification is per tranche, per document.** A ratified tranche constrains something. An unratified complete specification constrains nothing while looking as though it does, which is the more dangerous of the two states. -2. **Part 01 still blocks conformance.** Its draft encoding is selected; the - missing contract, corpus, independent implementations, rejection path, and - adoption keep every later part non-conforming across substrate boundaries. +2. **Part 01 no longer blocks conformance.** Its encoding is ratified and its + contract, corpus, rejection path, conformance kit, and one named adoption + path now exist. Independent interoperability remains unconfirmed and + therefore cannot be claimed, but it is not a prerequisite for A3 or bounded + adoption. 3. **The first post-erratum artifact is code, not another prose-only revision.** `probes/hsp-fast-path-debt-scope-v0` makes the debt-locality term of §15.0 executable with eleven fail-closed tests and a local benchmark. It is a diff --git a/myc b/myc index e02d7f98..b1e94b03 160000 --- a/myc +++ b/myc @@ -1 +1 @@ -Subproject commit e02d7f98637c1ea7fcc793c7023e3e015bbecf7e +Subproject commit b1e94b03df9d0a34df693380b6bdeea9b970e2dc