diff --git a/.agents/skills/sym/SKILL.md b/.agents/skills/sym/SKILL.md index 93cd5043..e147e10c 100644 --- a/.agents/skills/sym/SKILL.md +++ b/.agents/skills/sym/SKILL.md @@ -154,7 +154,7 @@ Every change you make to SYM.BOT code MUST be production quality: - **Fix what you find.** If you spot design issues, missing spec comments, or messy logic — fix them in the same change. No "TODO" or "FIXME" left behind. - **Spec compliance.** Code must match the MMP spec. Reference section numbers in comments where relevant. - **Clean architecture.** Each module has one responsibility. Proper error handling — not swallowed. -- **No tech debt.** The founder is solo. Sloppy code today is a blocker tomorrow. The protocol is the product — the implementation must be as rigorous as the spec. +- **No tech debt.** Sloppy code today is a blocker tomorrow, and the project has no spare capacity to pay it back. The protocol is the product — the implementation must be as rigorous as the spec. ## Behaviour diff --git a/.claude/skills/sym/SKILL.md b/.claude/skills/sym/SKILL.md index 93cd5043..e147e10c 100644 --- a/.claude/skills/sym/SKILL.md +++ b/.claude/skills/sym/SKILL.md @@ -154,7 +154,7 @@ Every change you make to SYM.BOT code MUST be production quality: - **Fix what you find.** If you spot design issues, missing spec comments, or messy logic — fix them in the same change. No "TODO" or "FIXME" left behind. - **Spec compliance.** Code must match the MMP spec. Reference section numbers in comments where relevant. - **Clean architecture.** Each module has one responsibility. Proper error handling — not swallowed. -- **No tech debt.** The founder is solo. Sloppy code today is a blocker tomorrow. The protocol is the product — the implementation must be as rigorous as the spec. +- **No tech debt.** Sloppy code today is a blocker tomorrow, and the project has no spare capacity to pay it back. The protocol is the product — the implementation must be as rigorous as the spec. ## Behaviour diff --git a/CHANGELOG.md b/CHANGELOG.md index 68c5b475..e8ac97dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -632,7 +632,7 @@ carried (cutoffs, ratification, budgets per delegation subtree, `role-chain-fetc ### MMP 2.0 update 1: the drafts this release implements, as the spec now states them -The founder ruled that the drafts sym 0.14.0 implements land in the published spec before it ships. +The drafts sym 0.14.0 implements land in the published spec before it ships. They are folded into one update, MMP 2.0 update 1 (sym-bot/meshcognition-website PR #43, merged to main at c07ff20: #24, #25, #17, #22, #26, #30, #23, #31, #37, #34, #35, #27 and #28 on the §6.6 errata), and sym is aligned with its text, its reference construction (scripts/mmp/lib.mjs) and its schemas and vectors, vendored in tests/fixtures with their @@ -653,7 +653,7 @@ sym-attest-v1 pass case by case, and every frame sym builds validates against th `detail` names this node's session or a probe ping is outstanding, while no newer session is waiting to supersede, and no faster than the retry backoff (fifty forged 1011s with no detail bought three re-handshakes in five seconds; now none). A sealed ping is answered with a pong. -- **The mood frame (founder ruling).** `{ type, mood, context, timestamp }`: mood 1 to 1,024 +- **The mood frame.** `{ type, mood, context, timestamp }`: mood 1 to 1,024 characters, context at most 4,096 or null; no `from` or `fromName` (the receiver labelled the event with the payload's name); a frame carrying them is refused. `broadcastMood` throws `EMOODFRAME` rather than send one out of bounds. @@ -720,7 +720,7 @@ sym-attest-v1 pass case by case, and every frame sym builds validates against th ### Breaking: `remix()` is asynchronous, and §15.8 runs on what it produces -By the founder's ruling, §15.8 is enforced where the spec puts it, on the remixing node, in 0.14.0. +§15.8 is enforced where the spec puts it, on the remixing node, in 0.14.0. `node.remix(categories, opts)` now evaluates the remix against its nearest verified lineage root (content-only, one kernel, α-weighted drift against Tguarded) before it is minted: a faithful remix keeps its lineage; one drifted past the floor is minted as a fresh root, with no parent from the @@ -774,8 +774,8 @@ key (below), whose records are quarantined. Upgrade every node of a room togethe peer sent it, under the key that session proved (the frame is sealed on the session); it is not a signed record. Nothing about it can be verified again later, relayed with its author's signature, or attributed to anyone but that peer. A host shows it as the peer's word, not as a verified record. - The mood frame is `{ type, mood, context, timestamp }` (MMP 2.0 update 1, by the founder's - ruling): it names no sender, and `mood-delivered.from` is the session's proven name. + The mood frame is `{ type, mood, context, timestamp }` (MMP 2.0 update 1): it names no + sender, and `mood-delivered.from` is the session's proven name. ### The loaded version, and hot-swap @@ -788,7 +788,7 @@ key (below), whose records are quarantined. Upgrade every node of a room togethe own, redelivers nothing the peer re-pushes, keeps every earlier id fetching its record, and logs no null dereference (on 0.13 this broke as "Cannot read properties of null (reading 'close')"). -### Redelivery and inbox ids (founder's bug report, 2026-10) +### Redelivery and inbox ids (bug report, 2026-10) - **A delivery is surfaced once for the life of the store.** After a hot-swap (a new SymNode for the same identity, as mesh-channel's `sym_join_room` builds) or a reconnect, records already received @@ -812,7 +812,7 @@ key (below), whose records are quarantined. Upgrade every node of a room togethe ### The final re-review of f0d936a (BLOCK) — what changed -The founder ruled three design changes. Rulings A and B (what grants are kept, and what a revoked +Three design changes were settled (rulings A, B and C). A and B (what grants are kept, and what a revoked key may still sign) are met by MMP §6.6 itself (above): authority is a function of the statements held, and no statement carries a time that could place it inside a period of trust. @@ -2350,7 +2350,7 @@ disconnected. ### Changed -- **Room names must be canonical** (founder ruling 2026-08-27): one name per +- **Room names must be canonical** (decision 2026-08-27): one name per room, one room per name. `isValidRoom` now requires the round trip through the service type to be identity, so **`sym` is refused**. It satisfies the kebab grammar, maps to `_sym._tcp`, and comes back as `default` — a second diff --git a/docs/DESIGN-room-join-authorization.md b/docs/DESIGN-room-join-authorization.md index 6643aecf..311bc5b7 100644 --- a/docs/DESIGN-room-join-authorization.md +++ b/docs/DESIGN-room-join-authorization.md @@ -16,8 +16,8 @@ > key, a gated room admits only its owner. Nothing in the estate gates a room today — no owner pins > on any node — so this closed a hole rather than breaking a user. -**Date:** 2026-08-26 · **Author:** dev1 (sym runtime lane) · **Status:** v2 — mesh review SHIP-WITH-FIXES folded (x-review cmb-3e58586f…, 12 findings, all applied); FOUNDER RULED 2026-08-26: (1) grammar fix NOW, ahead of the gate; (2) ADOPT B AS FOLDED (owner-only, 24h lifetime, join-time expiry, handshake enforcement, default+sym public). Implementation = ordinary mesh-reviewed sym-lane work -**Prior:** the C+D standing-rooms ruling (xmesh docs/DESIGN-tenancy-standing-rooms.md, founder +**Date:** 2026-08-26 · **Author:** dev1 (sym runtime lane) · **Status:** v2 — mesh review SHIP-WITH-FIXES folded (x-review cmb-3e58586f…, 12 findings, all applied); DECIDED 2026-08-26: (1) grammar fix NOW, ahead of the gate; (2) ADOPT B AS FOLDED (owner-only, 24h lifetime, join-time expiry, handshake enforcement, default+sym public). Implementation = ordinary mesh-reviewed sym-lane work +**Prior:** the C+D standing-rooms ruling (xmesh docs/DESIGN-tenancy-standing-rooms.md, 2026-08-26) names this as its residual: "sym resolves rooms by string … full closure needs sym-side join authorization." This is that design. diff --git a/lib/config.js b/lib/config.js index f65c57c2..f74e8223 100644 --- a/lib/config.js +++ b/lib/config.js @@ -35,11 +35,11 @@ const { SYM_STATE_DIR: SYM_DIR } = require('./core/state-root'); /** DER/PKCS8 Ed25519 private-key header — the 16 bytes preceding the raw 32-byte key. */ const ED25519_PKCS8_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex'); -/** The founder's ruling, carried verbatim into the error an operator actually sees. */ +/** The rule behind the halt, stated in the error an operator actually sees. */ const IDENTITY_RULING = - 'An agent may lose its machine and recover its data, but if it loses its private key, it ' + - 'cannot prove that it is the same agent. Therefore the key must be preserved, and the ' + - 'system must refuse to start rather than silently create a replacement identity.'; + 'An agent can lose its machine and still recover its data, but an agent that loses its ' + + 'private key can no longer prove it is the same agent. The key must therefore be kept, and ' + + 'the system refuses to start rather than silently create a replacement identity.'; /** * Raised instead of minting a replacement identity. @@ -2064,7 +2064,7 @@ function acquireLockAt(lockPath, meta = {}, depth = 0) { } /** - * NAME-SUFFIXING IS DELETED (founder ruling, agent id = node id). + * NAME-SUFFIXING IS DELETED (agent id = node id). * * The name-suffixing resolver used to answer a same-host collision by returning * `-2`, `-3`, … That was never collision handling. An agent id names an AGENT, and diff --git a/lib/core/cmb-encoder.js b/lib/core/cmb-encoder.js index b881990b..05823bd9 100644 --- a/lib/core/cmb-encoder.js +++ b/lib/core/cmb-encoder.js @@ -90,7 +90,7 @@ function categoriesPreimage(categories) { } /** - * The prefix every key is minted with, FROZEN (founder canon §17). Post-cutover there is one + * The prefix every key is minted with, FROZEN (canon §17). Post-cutover there is one * form — bare `cmb-` + 64-hex — and no dual-emit window (§19.1/§20): the freeze window plus the * fleet census make an emission flag unnecessary, and a flag IS the backcompat §19 forbids. The * prefix says only "this is a CMB"; the scheme lives inside the signed payload, and any future diff --git a/lib/core/cmb-signing.js b/lib/core/cmb-signing.js index 6b041897..63218c69 100644 --- a/lib/core/cmb-signing.js +++ b/lib/core/cmb-signing.js @@ -440,7 +440,7 @@ function verifyCMB(cmb, publicKeyB64url) { : { signed: true, valid: false, error: 'legacy-key-rejected' }; } // UNREACHABLE BY MEASUREMENT — the legacy verification arm that stood here is DELETED - // (CTO ruling, 2026-08-07). Every record with `.metadata` returns from the v2 branch above; + // (decision 2026-08-07). Every record with `.metadata` returns from the v2 branch above; // every record with a top-level `.sig` and no `.metadata` returns `unverified-legacy` from // the grandfather branch. Nothing could reach what followed, so nothing behind this line // ever ran. diff --git a/lib/core/mood-frame.js b/lib/core/mood-frame.js index 0390270a..37923ea6 100644 --- a/lib/core/mood-frame.js +++ b/lib/core/mood-frame.js @@ -2,8 +2,8 @@ /** * @module sym/core/mood-frame - * @description The `mood` control frame (MMP §9.3; control-frame.schema.json, MMP 2.0 update 1 by the - * founder's ruling): `{ type, mood, context, timestamp }`, sealed on a confirmed Core Secure session. + * @description The `mood` control frame (MMP §9.3; control-frame.schema.json, MMP 2.0 update 1): + * `{ type, mood, context, timestamp }`, sealed on a confirmed Core Secure session. * * mood 1 to 1,024 characters * context at most 4,096 characters, or null diff --git a/lib/core/record-shape.js b/lib/core/record-shape.js index 38981337..455f73d8 100644 --- a/lib/core/record-shape.js +++ b/lib/core/record-shape.js @@ -47,10 +47,10 @@ function resolveCategories(record) { if (!record) return null; // ONE NAME. `categories` is the container and the only container. This used to walk a candidate - // list so a record could carry it under either name — that is the name-check logic the founder - // ruled out (2026-08-09, "remove all name check logic"), and removing it is what makes this - // module's promise true rather than aspirational. With one name there is no precedence to get - // wrong, and no second reader that can disagree with this one. + // list so a record could carry it under either name — that is the name-check logic removed on + // 2026-08-09, and removing it is what makes this module's promise true rather than + // aspirational. With one name there is no precedence to get wrong, and no second reader that + // can disagree with this one. // // A record carrying only the retired name does not resolve, and that refusal is the point: it // is legible at the boundary rather than half-read further in. @@ -76,7 +76,7 @@ function requireCategories(record, where) { * requireCategories so that "I can cope with absence" is stated at the call site rather than * implied by a truthiness test. * - * USAGE RULE — BINDING (CTO ruling, 2026-08-07). `hasCategories` MAY ONLY BE USED WHERE THE + * USAGE RULE — BINDING (decision 2026-08-07). `hasCategories` MAY ONLY BE USED WHERE THE * FALSE BRANCH RETURNS A VERDICT. It must NEVER guard a block that control can fall out of. * * if (!hasCategories(r)) return { valid: false, error: 'cannot-verify: …' }; // CORRECT @@ -94,7 +94,7 @@ function hasCategories(record) { /** WHY the categories could not be read, as three distinct states rather than one falsy. * - * The distinction the CTO measured (2026-08-07): an E2E record verified BEFORE decryption + * The distinction measured on 2026-08-07: an E2E record verified BEFORE decryption * carries its container as a base64 CIPHERTEXT STRING with `_e2e` attached. `hasCategories` * correctly reports false — you must not be able to content-verify ciphertext — but reporting * that as "record carries no CAT7 container" MISDESCRIBES it. The container is right there; it diff --git a/lib/core/room-grant.js b/lib/core/room-grant.js index e17e73b6..1913bc45 100644 --- a/lib/core/room-grant.js +++ b/lib/core/room-grant.js @@ -3,7 +3,7 @@ /** * @module sym/core/room-grant * @description Ed25519-signed ROOM-JOIN grants — membership as a right, not as - * knowledge of a string (docs/DESIGN-room-join-authorization.md; founder ruling + * knowledge of a string (docs/DESIGN-room-join-authorization.md; decision * 2026-08-26, option B as folded). * * WHAT THIS EXISTS TO STOP, measured rather than imagined: a room is a NAME, and diff --git a/lib/frame-handler.js b/lib/frame-handler.js index 3fee8140..d9d86241 100644 --- a/lib/frame-handler.js +++ b/lib/frame-handler.js @@ -1998,7 +1998,7 @@ class FrameHandler { * * Identified 2026-07-05 in the liquid-substrate scope note, which ranked it bug-grade and * independent of any learning work; still present in published 0.12.1 when re-checked - * 2026-08-24. Founder decision D5: ship this fix on its own, and HOLD the recurrent-cell + * 2026-08-24. Decision D5: ship this fix on its own, and HOLD the recurrent-cell * dimension until the learning testbed says what shape it should be. * * WHAT THIS IS NOT, and the honesty matters more than the fix. This is still a STATELESS diff --git a/lib/interior.js b/lib/interior.js index 4ce28921..9cf98540 100644 --- a/lib/interior.js +++ b/lib/interior.js @@ -54,7 +54,7 @@ * deliveries that verified — a record signed to this node, or a room broadcast it admitted — never * a quarantined one. A recall item is { key, record, verified, storedAt, author }. * - * THE READ SIDE IS SCOPED TO THE MIND'S MISSION (final re-review, Finding 6; the founder's ruling C). + * THE READ SIDE IS SCOPED TO THE MIND'S MISSION (final re-review, Finding 6; ruling C). * A mind is the node's reasoning for ONE mission, and must not read what the node received for * another: * - each mind has its own READ VIEW: the deliveries that arrived while it runs (after its start), diff --git a/lib/node.js b/lib/node.js index 67d0e6d0..349c9e81 100644 --- a/lib/node.js +++ b/lib/node.js @@ -483,7 +483,7 @@ class SymNode extends EventEmitter { // Retention — how long to keep CMBs in local storage. // - // DEFAULT: UNLIMITED (founder ruling 2026-07-20). Accumulated cognition is the + // DEFAULT: UNLIMITED (decision 2026-07-20). Accumulated cognition is the // most valuable asset a node holds; deleting it on a timer destroys the very // thing the system exists to build. The previous 86400s (24h) default was // silently pruning authority-bearing history — observed live: a deputy store @@ -679,7 +679,7 @@ class SymNode extends EventEmitter { this._inboxSeq = 0; this._inboxCursor = 0; this._inboxMax = opts.inboxMax || 500; - // DURABLE (founder ruling 2026-08-04): communication is addressed to the + // DURABLE (decision 2026-08-04): communication is addressed to the // NODE, and a new session RELINKS to it — including what was delivered // while no session was attached. The inbox was process memory only, so a // restart wiped the delivery feed while every sender believed it had @@ -2932,7 +2932,7 @@ class SymNode extends EventEmitter { oldestUndrainedAt: undrained.length ? (undrained[0].receivedAt ?? null) : null, // Distinct from "has a backlog": a node that has NEVER been drained is // not behind, it is unattended — the condition that hid five directed - // CMBs, two of them founder words, for three days. + // CMBs for three days. neverDrained: this._inboxCursor === 0 && this._inboxSeq > 0 && !(this._inbox.length > 0 && this._inbox.every((m) => m.acked)), dropped: this._metrics.inboxDropped ?? 0, @@ -3219,7 +3219,7 @@ class SymNode extends EventEmitter { // was not connected wrote the CMB locally, logged a single line, and returned // an entry INDISTINGUISHABLE from a delivered one; every layer above reported // success for sends that reached nobody. Measured cost (bl-a6e63608c8c): five - // directed CMBs to one seat — including a founder authorization and a founder + // directed CMBs to one seat — including an authorization and a // question — undelivered for three days while both senders believed otherwise, // each building a prose relay workaround around a channel reporting success. // @@ -3237,7 +3237,7 @@ class SymNode extends EventEmitter { // so it counts frames handed to a transport, not frames any peer received. // Naming that "delivered" would be this exact class of overclaim one layer // up: a count bound to something other than what its name asserts. The word - // `delivered` stays reserved until an ack earns it (CTO gate on a371021). + // `delivered` stays reserved until an ack earns it (gate on a371021). // // `undelivered` is still sound as written, and is the load-bearing category: // never-dispatched does entail never-delivered, even though the converse diff --git a/lib/room-ownership.js b/lib/room-ownership.js index cc667d6b..2c125980 100644 --- a/lib/room-ownership.js +++ b/lib/room-ownership.js @@ -3,7 +3,7 @@ /** * @module sym/room-ownership * @description Which key owns which room — the receiver's local answer to - * "may this peer be here?" (docs/DESIGN-room-join-authorization.md, founder ruling + * "may this peer be here?" (docs/DESIGN-room-join-authorization.md, decision * 2026-08-26). * * WHAT OWNERSHIP IS FOR. Without an owner, a room's membership is knowledge of its diff --git a/tests/core-secure-final-review.test.js b/tests/core-secure-final-review.test.js index a75f2eb9..d7f1e1de 100644 --- a/tests/core-secure-final-review.test.js +++ b/tests/core-secure-final-review.test.js @@ -3,10 +3,9 @@ require('./_isolate-home'); // redirect $HOME before lib/config loads /** - * The final re-review of f0d936a (BLOCK): the founder's three rulings and the findings beside them, - * each as a regression test that fails on f0d936a. The reviewer's repros are in - * /private/tmp/claude-501/-Users-hongwei-sym-agent-a/sym-014-final-review/ (grant-probes.js P1-P4, - * node-probes.js Q1, digest-churn.js, fork-a, fork-b). + * The final re-review of f0d936a (BLOCK): the three design changes (rulings A, B and C) and the findings + * beside them, each as a regression test that fails on f0d936a. The reviewer's repros (grant-probes.js + * P1-P4, node-probes.js Q1, digest-churn.js, fork-a, fork-b) were written outside this repository. * * A, B retired with the time-replay grant rule (MMP §6.6; see below) * C the interior read side is scoped to the mind's mission (Finding 6) diff --git a/tests/delivery-ledger.test.js b/tests/delivery-ledger.test.js index 418cc9fe..e4759434 100644 --- a/tests/delivery-ledger.test.js +++ b/tests/delivery-ledger.test.js @@ -3,7 +3,7 @@ require('./_isolate-home'); // redirect $HOME before lib/config loads /** - * The redelivery and inbox-id bugs (founder, via agent-b, 2026-10; mesh-channel 0.10.1 on sym 0.13.16, + * The redelivery and inbox-id bugs (reported via agent-b, 2026-10; mesh-channel 0.10.1 on sym 0.13.16, * and reproduced on 0.14.0 at 6510bac): * * Bug 1: after a hot-swap (a new SymNode for the same identity) or a reconnect, CMBs already diff --git a/tests/directed-delivery-accountability.test.js b/tests/directed-delivery-accountability.test.js index c8b6cfb2..ee935b6c 100644 --- a/tests/directed-delivery-accountability.test.js +++ b/tests/directed-delivery-accountability.test.js @@ -19,7 +19,7 @@ require('./_isolate-home'); // redirect $HOME to a temp sandbox before lib/confi * the seat) therefore reported success for sends that reached no one. * * Live consequence: five directed CMBs addressed to the dev seat — including - * a founder authorization and a founder question — sat undelivered for three + * an authorization and a question — sat undelivered for three * days while every sender believed it had delivered. Two seats independently * built prose workarounds ("RELAY FOR DEV") around a channel that was * reporting success. diff --git a/tests/durable-inbox.test.js b/tests/durable-inbox.test.js index c8ae34f9..38dc2395 100644 --- a/tests/durable-inbox.test.js +++ b/tests/durable-inbox.test.js @@ -1,7 +1,7 @@ 'use strict'; /** - * Durable delivery inbox (founder ruling 2026-08-04): communication is + * Durable delivery inbox (decision 2026-08-04): communication is * addressed to the NODE, and a new session RELINKS to it — including what * was delivered while no session was attached. Before this, the inbox was * process memory only: a restart wiped the delivery feed while every sender diff --git a/tests/identity-halt.test.js b/tests/identity-halt.test.js index c40b72c1..ebe0a5dc 100644 --- a/tests/identity-halt.test.js +++ b/tests/identity-halt.test.js @@ -5,9 +5,9 @@ require('./_isolate-home'); // sandbox HOME/USERPROFILE before anything reads os /** * B-3 / AC-3.2 — the system refuses to start rather than replace an identity. * - * Founder ruling D-04: "An agent may lose its machine and recover its data, but if it loses its - * private key, it cannot prove that it is the same agent. Therefore the key must be preserved, - * and the system must refuse to start rather than silently create a replacement identity." + * Decision D-04: the private key is what makes an agent the same agent. An agent can lose its + * machine and still recover its data, but without its key it can no longer prove it is itself, so + * the system refuses to start rather than silently create a replacement identity. * * What this replaces was quiet and destructive: an unreadable identity.json logged a warning, * generated a fresh keypair, and OVERWROTE the file. The agent kept its name and lost the only @@ -65,8 +65,8 @@ test('unparseable identity HALTS and does not touch the file', () => { assert.throws(() => cfg.loadOrCreateIdentity('agent-b@mesh'), (e) => { assert.equal(e.name, 'IdentityHaltError'); assert.match(e.message, /REFUSING TO START/); - // The ruling itself is in the operator-facing text, not just in a doc. - assert.match(e.message, /refuse to start rather than silently create a replacement identity/); + // The rule itself is in the operator-facing text, not just in a doc. + assert.match(e.message, /refuses to start rather than silently create a replacement identity/); return true; }); @@ -163,7 +163,7 @@ test('AC-3.2: no `-2` identity can be created by any path', () => { test('the lease refuses a second holder and never suggests renaming', (t) => { // The old error advised "set a different SYM_NODE_NAME", which under agent-id-is-identity - // is advice to become a different agent — the exact move the ruling forbids. + // is advice to become a different agent — the exact move the rule forbids. // // The foreign holder is a real, live child process recorded with its real start time — the // current lock format exactly as a running holder writes it. This used to be PID 1 with a diff --git a/tests/identity-migration-by-id.test.js b/tests/identity-migration-by-id.test.js index 72d772c3..3a001bb4 100644 --- a/tests/identity-migration-by-id.test.js +++ b/tests/identity-migration-by-id.test.js @@ -11,8 +11,8 @@ require('./_isolate-home'); // redirect $HOME to a temp sandbox before lib/confi * every answer it did not recognise as "no such agent" and MINTED: it renamed the 0.13 directory (the old * identity.json still in it) onto the new node's path and wrote the new identity over the old key. * - * The tests below hold the line the founder drew ("the system must refuse to start rather than silently - * create a replacement identity"): a name that has an identity on this host never gets a new one. + * The tests below hold one line: the system refuses to start rather than silently create a replacement + * identity, so a name that has an identity on this host never gets a new one. * Every test uses its own agent name; the state root is shared by the file, as in config.test.js. */ diff --git a/tests/inbox-holder-accountability.test.js b/tests/inbox-holder-accountability.test.js index 241256a9..c774bff5 100644 --- a/tests/inbox-holder-accountability.test.js +++ b/tests/inbox-holder-accountability.test.js @@ -10,9 +10,8 @@ require('./_isolate-home'); // redirect $HOME to a temp sandbox before lib/confi * * Measured live 2026-08-09 — sym-daemon-mac: seq=96, cursor=0. Ninety-six * messages, never drained once, span 08-06 → 08-09 and still accumulating. - * Five of them were CMBs addressed directly to the dev seat, two carrying - * founder words. Every seat node beside it drained normally, so the condition - * was specific and detectable — nobody could detect it. + * Five of them were CMBs addressed directly to the dev seat. Every seat node beside it + * drained normally, so the condition was specific and detectable — nobody could detect it. * * Worse, the ring silently shredded its own backlog. _inbox.shift() evicted * oldest-first without asking whether the message had ever been read, so a diff --git a/tests/no-owner-citings.test.js b/tests/no-owner-citings.test.js new file mode 100644 index 00000000..698eb209 --- /dev/null +++ b/tests/no-owner-citings.test.js @@ -0,0 +1,231 @@ +'use strict'; + +// The library never cites its owner. A public package carries what was decided and why, not who said it: no +// ruling or decision credited to a person, no quoted words, no personal name or address, in a comment, a doc, the +// CHANGELOG, a test title or an error string. This scans every tracked file (git ls-files; a directory walk when +// there is no .git, as in an export of the package) and fails on a citing. +// +// "founder" is also a ROLE in the authority model and part of the product's vocabulary, so the scan is deny-by-default: +// every "founder" in a line must be covered by the allowed vocabulary below, or it is a hit. The allowed vocabulary: +// - identifiers and env vars: founderAction, _meta.founderAction, detectFounderAction, SYM_FOUNDER_ANCHOR +// - the role: "founder key", "founder root", "founder anchor", "1-of-1 founder case" +// - a node, task owner or source literally named 'founder', and the detector's keywords 'brief founder' and +// 'brief the founder' +// - per-file entries (ALLOW_IN_FILE) for the words that describe the founderAction signal itself +// Skipped: tests/fixtures/** (hash-pinned vendored vectors), package-lock.json, binary files, node_modules, and this file +// (its own patterns would match themselves; the negative tests below run the same scanner on a planted tree). + +const { describe, it } = require('node:test'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { execFileSync } = require('node:child_process'); + +const ROOT = path.join(__dirname, '..'); +const SELF = 'tests/no-owner-citings.test.js'; + +const SKIP_DIRS = new Set(['node_modules', '.git', 'coverage']); +const SKIP_PREFIXES = ['tests/fixtures/']; +const SKIP_FILES = new Set(['package-lock.json', SELF]); +const BINARY = /\.(png|jpe?g|gif|ico|webp|pdf|woff2?|ttf|otf|zip|gz|tgz|mp4|mov)$/i; + +// "founder" as vocabulary: stripped from a line before the line is judged. +const VOCABULARY = [ + /[A-Za-z0-9_]*[Ff]ounder[A-Z][A-Za-z0-9_]*/g, // founderAction, detectFounderAction + /[A-Z0-9_]*(?:_FOUNDER|FOUNDER_)[A-Z0-9_]*/g, // SYM_FOUNDER_ANCHOR + /\bfounder (?:key|root|anchor|case)s?\b/gi, // the role in the authority model + /'(?:brief (?:the )?)?founder'/gi, // a node, task owner or source named 'founder'; the detector's keywords +]; + +// Words that describe the founderAction signal itself, allowed in the one file that implements it. +const ALLOW_IN_FILE = [ + { file: 'lib/mesh-agent.js', re: /Founder Action Detection|founder attention|the founder needs to see/gi, + reason: 'describes the founderAction signal (a CMB a human should see)' }, +]; + +// The owner's names and handles, built from parts so that this file holds none of them whole. +const NAMES = [['hong', 'wei'].join(''), ['hong', ' wei'].join(''), ['mx', 'michael'].join(''), ['symbot', 'dev'].join(''), 'michael']; + +// Other citings, judged on the whole line. +const RULES = [ + { name: 'personal name or address', re: new RegExp(NAMES.join('|'), 'i') }, + { name: 'the user as the source of a decision', re: /\bthe user (?:said|asked|ruled|wants|approved|decided|told)\b|\buser ruling\b|\buser(?:'|’)s (?:ruling|word|rule)\b/i }, + { name: 'an executive or the owner as the source of a ruling', re: /\b(?:ceo|cto|coo|boss)(?:'s|’s)? (?:ruling|rulings|ruled|decision|decided|said|canon|approval|gate|measured)\b|\b(?:owner|user)(?:'s|’s)? (?:ruling|rulings|ruled|canon)\b/i }, + { name: 'a request or approval attributed to someone', re: /\b(?:as|per) (?:requested|asked|ruled|decided|instructed) by\b|\bper the (?:user|owner|ceo|cto|boss)\b/i }, + { name: 'a dated attribution, "(user, 2026-10-02: ...)"', re: /\((?:user|owner|boss|ceo|cto|coo),? *\d{4}-\d{2}-\d{2}/i }, +]; + +/** The files of a tree: git ls-files when it is a checkout, a directory walk when it is not. */ +function listFiles(root) { + if (fs.existsSync(path.join(root, '.git'))) { + try { + return execFileSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf8', maxBuffer: 1 << 26 }) + .split('\0').filter(Boolean); + } catch { /* git missing or not usable: walk */ } + } + const out = []; + const walk = (dir, rel) => { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + if (SKIP_DIRS.has(e.name)) continue; + const r = rel ? `${rel}/${e.name}` : e.name; + if (e.isDirectory()) walk(path.join(dir, e.name), r); + else out.push(r); + } + }; + walk(root, ''); + return out; +} + +const skipped = (rel) => SKIP_FILES.has(rel) || BINARY.test(rel) || SKIP_PREFIXES.some((p) => rel.startsWith(p)); + +/** Every citing in `root`: [{ file, line, rule, text }]. */ +function scan(root, files = listFiles(root)) { + const hits = []; + for (const rel of files) { + if (skipped(rel)) continue; + let text; + try { + if (!fs.statSync(path.join(root, rel)).isFile()) continue; + text = fs.readFileSync(path.join(root, rel), 'utf8'); + } catch { continue; } // tracked but not on disk (a staged removal) + if (text.includes('\0')) continue; // binary + const allow = ALLOW_IN_FILE.filter((a) => a.file === rel); + text.split('\n').forEach((raw, i) => { + const at = (rule) => hits.push({ file: rel, line: i + 1, rule, text: raw.trim().slice(0, 160) }); + let residue = raw; + for (const re of VOCABULARY) residue = residue.replace(re, ' '); + for (const a of allow) residue = residue.replace(a.re, ' '); + if (/founder/i.test(residue)) at('"founder" outside the allowed vocabulary'); + for (const r of RULES) if (r.re.test(raw)) at(r.name); + }); + if (RULES.some((r) => r.re.test(rel))) hits.push({ file: rel, line: 0, rule: 'personal name in a file name', text: rel }); + } + return hits; +} + +const show = (hits) => hits.map((h) => `${h.file}:${h.line} [${h.rule}] ${h.text}`).join('\n'); + +describe('the library does not cite its owner', () => { + it('finds the files it checks', () => { + const files = listFiles(ROOT).filter((f) => !skipped(f)); + assert.ok(files.length >= 100, `scanned ${files.length} files`); + }); + + it('no tracked file cites the owner (a ruling, a bug report, quoted words, a name)', () => { + const hits = scan(ROOT); + assert.deepStrictEqual(hits, [], `state the decision or the rule, not who gave it:\n${show(hits)}`); + }); + + it('every per-file allowance still matches something (the allowlist does not rot)', () => { + for (const a of ALLOW_IN_FILE) { + const p = path.join(ROOT, a.file); + if (!fs.existsSync(p)) continue; // an export without this file + assert.ok(new RegExp(a.re.source, a.re.flags.replace('g', '')).test(fs.readFileSync(p, 'utf8')), + `${a.file}: allowance for "${a.reason}" matches nothing; remove it`); + } + }); +}); + +describe('the scanner itself (a planted tree, no .git: the directory-walk path)', () => { + const plant = (files) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sym-citings-')); + for (const [rel, body] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true }); + fs.writeFileSync(path.join(dir, rel), body); + } + return dir; + }; + const withTree = (files, fn) => { + const dir = plant(files); + try { return fn(dir); } finally { fs.rmSync(dir, { recursive: true, force: true }); } + }; + + // The words the scanner looks for are built from parts, so this file holds no citing of its own. + const T = { + f: 'found' + 'er', F: 'Found' + 'er', FF: 'FOUND' + 'ER', u: 'us' + 'er', + name: ['Hong', 'wei'].join(''), mail: ['symbot', 'dev@gmail.com'].join(''), handle: ['mx', 'michael'].join(''), + }; + const CITINGS = { + 'a ruling in a comment': `// the key is kept (${T.f} ruling 2026-08-04)`, + 'a ruling in capitals': `**Status:** v2; ${T.FF} RULED 2026-08-26: ship it`, + 'a possessive ruling': `/** The ${T.f}'s ruling, carried into the error. */`, + 'a decision label': ` * ${T.F} decision D5: ship this fix on its own`, + 'a changelog sentence': `The ${T.f} ruled three design changes.`, + 'a bug report': `### Redelivery (${T.f}'s bug report, 2026-10)`, + 'a relayed report': ` * The bugs (${T.f}, via agent-b, 2026-10; mesh-channel 0.10.1)`, + 'words attributed to the owner': ` * two of them ${T.f} words, lost for three days`, + 'a line the owner drew': ` * hold the line the ${T.f} drew`, + 'a canon reference': ` * FROZEN (${T.f} canon §17)`, + 'a statement credited to the user': `// the ${T.u} said to drop the fallback`, + 'a ruling credited to the user': `// per the ${T.u} ruling, no suffixes`, + 'a dated user attribution': '// the name is never spelled so (user, 2026-10-02: "use it in code")', + 'an executive ruling': '// (CTO ruling, 2026-08-07)', + 'an executive gate': '// stays reserved until an ack earns it (CTO gate on a371021)', + 'a requested-by line': '// added as requested by the boss', + 'the owner\'s name': `// agreed with ${T.name} on the call`, + 'the owner\'s address': `contact: ${T.mail}`, + 'a handle': `// ask ${T.handle}`, + }; + + for (const [what, line] of Object.entries(CITINGS)) { + it(`catches ${what}`, () => { + const hits = withTree({ 'lib/x.js': `'use strict';\n${line}\nmodule.exports = 1;\n` }, (dir) => scan(dir)); + assert.strictEqual(hits.length >= 1, true, `not caught: ${line}`); + assert.ok(hits.every((h) => h.file === 'lib/x.js' && h.line === 2), show(hits)); + }); + } + + it('catches a citing in a doc, a test title and the CHANGELOG, and in a file name', () => { + const hits = withTree({ + 'docs/a.md': `The ${T.f} ruled that it ships.\n`, + 'tests/t.test.js': `it('does it (${T.f} ruling, #26)', () => {});\n`, + 'CHANGELOG.md': `- **Thing** (${T.f} ruling 2026-08-27)\n`, + [`notes-${T.name.toLowerCase()}.md`]: 'clean text\n', + }, (dir) => scan(dir)); + assert.deepStrictEqual([...new Set(hits.map((h) => h.file))].sort(), + ['CHANGELOG.md', 'docs/a.md', `notes-${T.name.toLowerCase()}.md`, 'tests/t.test.js']); + }); + + it('lets the vocabulary through: identifiers, the role, a node named founder, the detector\'s keywords', () => { + const clean = [ + "if (!meta?.founderAction && detectFounderAction(categories)) meta.founderAction = true;", + "const pin = process.env.SYM_FOUNDER_ANCHOR || null;", + "// One founder key (the 1-of-1 case). The nodeId lets that node act as the anchor itself.", + "canonization is reserved to anchor authority (the founder root or an anchor-rooted grantee)", + "new SymNode({ name: 'founder', anchor: { threshold: 1, keys: [] } });", + "t.agent !== 'founder'", + "f.source = 'founder';", + "'founder', 'human', 'decision', 'brief founder', 'brief the founder',", + "/** A node pinned as its own 1-of-1 anchor (the founder case). */", + "_meta.founderAction=true ONLY if your role definition's rules for it are satisfied", + ].join('\n'); + assert.deepStrictEqual(withTree({ 'lib/x.js': clean + '\n' }, (dir) => scan(dir)), []); + }); + + it('lets a per-file allowance through in its own file only', () => { + const body = '// Auto-detect whether a CMB warrants founder attention.\n'; + assert.deepStrictEqual(withTree({ 'lib/mesh-agent.js': body }, (dir) => scan(dir)), []); + assert.strictEqual(withTree({ 'lib/other.js': body }, (dir) => scan(dir)).length, 1); + }); + + it('still catches a citing on a line that also holds vocabulary', () => { + const hits = withTree({ 'lib/x.js': `// founderAction is set by the ${T.f}'s ruling\n` }, (dir) => scan(dir)); + assert.strictEqual(hits.length, 1); + }); + + it('skips the vendored fixtures, the lockfile, binaries and node_modules', () => { + const hits = withTree({ + 'tests/fixtures/vec.json': `{"note":"${T.f} ruling"}\n`, + 'package-lock.json': `{"name":"${T.f} ruling"}\n`, + 'docs/pic.png': `${T.f} ruling`, + 'node_modules/x/index.js': `// ${T.f} ruling\n`, + }, (dir) => scan(dir)); + assert.deepStrictEqual(hits, []); + }); + + it('reports the file, the line and the rule', () => { + const hits = withTree({ 'docs/a.md': `fine\nfine\nThe ${T.f} said so.\n` }, (dir) => scan(dir)); + assert.deepStrictEqual(hits.map((h) => [h.file, h.line, h.rule]), [['docs/a.md', 3, '"founder" outside the allowed vocabulary']]); + }); +}); diff --git a/tests/p6-legacy-grandfather.test.js b/tests/p6-legacy-grandfather.test.js index ec2e488f..426c4479 100644 --- a/tests/p6-legacy-grandfather.test.js +++ b/tests/p6-legacy-grandfather.test.js @@ -83,7 +83,7 @@ test('P-6: a pre-boundary block is REFUSED — grandfathering is retired', async // That policy required reading the audience under its retired name, because the companion case // below — a pre-boundary block from the WRONG audience — still had to be refused. Surfacing and // audience-checking together are only possible while the old name is still read, and the old - // name is gone (founder ruling 2026-08-09: remove all fallback and dead v1 code). + // name is gone (decision 2026-08-09: remove all fallback and dead v1 code). // // So the audience of such a record can no longer be established, and core refuses it as // `unreadable-audience` rather than treating an absent room as a broadcast. Refusing is the diff --git a/tests/product-name.test.js b/tests/product-name.test.js index f730c523..8f5543e5 100644 --- a/tests/product-name.test.js +++ b/tests/product-name.test.js @@ -1,7 +1,7 @@ 'use strict'; -// The product name is never "xMesh" (user, 2026-10-02: "don't use any xMesh, use xmesh in code, and XMesh in -// sentence and XMESH for brand or product name"). This walks every README, doc, comment and log string in the +// The product name is never "xMesh": it is XMesh in a sentence, XMESH for the brand or product name, and xmesh in +// code. This walks every README, doc, comment and log string in the // repo and fails on the camel-case spelling. Identifiers are untouched by it: the XMesh class, the // _handleXMeshInsight handler and XMESH_* names do not match, and nothing in the code is named xMesh. // Lowercase xmesh is the spelling for code (package, paths, CLI, wire ids) and is not checked here: telling a diff --git a/tests/remember-targeted.test.js b/tests/remember-targeted.test.js index 09c77e0b..acf8b183 100644 --- a/tests/remember-targeted.test.js +++ b/tests/remember-targeted.test.js @@ -124,8 +124,8 @@ describe('remember({to}) — MMP §4.4.4 targeted CMB send', () => { // test asserted the local write and stopped, so a GREEN here certified a // send that reached nobody and reported success. The store surviving a // partition is necessary and was never the question — the question is - // whether the CALLER can tell. Five directed CMBs, two of them founder - // words, were lost for three days under this green. + // whether the CALLER can tell. Five directed CMBs were lost for three days + // under this green. assert.strictEqual(entry.delivery.undelivered, true, 'a directed send that reached no peer must be reported as undelivered'); assert.strictEqual(entry.delivery.dispatched, 0, 'no frame was even handed to a transport'); diff --git a/tests/room-join-auth.test.js b/tests/room-join-auth.test.js index e040c9c3..ef4329e9 100644 --- a/tests/room-join-auth.test.js +++ b/tests/room-join-auth.test.js @@ -2,8 +2,8 @@ /** * Room join authorization — the grant (core/room-grant.js) and the receiver's - * ownership registry (room-ownership.js). Founder ruling 2026-08-26, option B as - * folded by the mesh review; every ruled property is pinned here. + * ownership registry (room-ownership.js). Decision 2026-08-26, option B as + * folded by the mesh review; every decided property is pinned here. */ const { describe, it } = require('node:test'); diff --git a/tests/spec-update-attest.test.js b/tests/spec-update-attest.test.js index b50bf3a1..f8303e0d 100644 --- a/tests/spec-update-attest.test.js +++ b/tests/spec-update-attest.test.js @@ -3,7 +3,7 @@ require('./_isolate-home'); // redirect $HOME before lib/config loads /** - * sym-attest-v1 as MMP 2.0 update 1 registers it (#27), and the founder's rulings for it: what this + * sym-attest-v1 as MMP 2.0 update 1 registers it (#27), and the decisions for it: what this * node never attests or relays (S3), the wire grammar (closed attestation, lowercase UUID signers, a * role of up to 64 characters, a method of up to 32), equivocation relayed once as evidence, the * scope of an attester's role judged on the record its assertion names, and no checkpoint root over diff --git a/tests/spec-update-frames.test.js b/tests/spec-update-frames.test.js index 69f45170..b5f57367 100644 --- a/tests/spec-update-frames.test.js +++ b/tests/spec-update-frames.test.js @@ -4,7 +4,7 @@ require('./_isolate-home'); // redirect $HOME before lib/config loads /** * The session frames as MMP 2.0 update 1 (meshcognition-website main at c07ff20) defines them: the - * mood frame by the founder's ruling, cmb-fetch and cmb-fetch-result as their schemas close them, + * mood frame, cmb-fetch and cmb-fetch-result as their schemas close them, * fetched records verified by the whole of §8.8.5 (D4), cmb-anchors as §9.4 sends it, a clear 1011 * acted on only when it names this node's session or answers its probe, sealed ping and pong, and the * control envelope's bounds. Each test fails on 958592a. @@ -40,7 +40,7 @@ function capture(session) { } const HEX = () => crypto.randomBytes(32).toString('hex'); -describe('the mood frame: {type, mood, context, timestamp} (founder ruling, #26)', () => { +describe('the mood frame: {type, mood, context, timestamp} (#26)', () => { it('is sent with no sender fields, and the text and context are bounded', async () => { try { const A = node('m-a'); const B = node('m-b'); diff --git a/tests/spec-update-remix-tether.test.js b/tests/spec-update-remix-tether.test.js index b72499ff..e7f513dd 100644 --- a/tests/spec-update-remix-tether.test.js +++ b/tests/spec-update-remix-tether.test.js @@ -3,7 +3,7 @@ require('./_isolate-home'); // redirect $HOME before lib/config loads /** - * §15.8 on the remix path (founder ruling, 0.14.0): remix() is asynchronous and evaluates what it + * §15.8 on the remix path (0.14.0): remix() is asynchronous and evaluates what it * produces against its nearest verified lineage root before it is minted. A faithful remix keeps its * lineage, a remix drifted past the reject floor is minted as a fresh root, and one with no verified * root in reach keeps its lineage, unverified; each verdict is signed as an mmp-tether-v1 attestation. diff --git a/tests/spec-update-session.test.js b/tests/spec-update-session.test.js index 9da8dc0a..ac2915e2 100644 --- a/tests/spec-update-session.test.js +++ b/tests/spec-update-session.test.js @@ -4,7 +4,7 @@ require('./_isolate-home'); // redirect $HOME before lib/config loads /** * sym 0.14.0 aligned with the MMP 2.0 update 1 drafts it implements (the session and sealed-control - * set: #26, #30, #31, #37, #23), the founder's rulings for that update, and the reviews of each draft + * set: #26, #30, #31, #37, #23), the decisions for that update, and the reviews of each draft * against sym 390b4af. Each test here fails on 390b4af. */ diff --git a/tests/wire-fromname.test.js b/tests/wire-fromname.test.js index 5267a2b0..5e5218e1 100644 --- a/tests/wire-fromname.test.js +++ b/tests/wire-fromname.test.js @@ -25,7 +25,7 @@ const { admitAs, deliver, identity } = require('./_core-secure'); describe('a frame\'s fromName is taken as text (R5)', () => { // 0.14: frames arrive on a confirmed session (design D1); the session's proven name is the fallback. - // MMP 2.0 update 1 (founder ruling): the mood frame names no sender. A mood frame carrying any + // MMP 2.0 update 1: the mood frame names no sender. A mood frame carrying any // fromName is not that frame and is refused; the event is labelled with the session's proven name. it('mood: a frame naming a sender is refused, whatever its fromName; the session\'s name labels the rest', () => { const name = uniq('fromname-mood');