/** * The published names may only use words the TARGET uses, or a marker that says why * they exist here. * * ── Why this is a test and not a rule ───────────────────────────────────── * The library corrected its vocabulary on 2026-07-30 — upstream a *wallet* is only a * keyring, and what owns stores is a **user** (a *site*) — by a manual pass over the * code and docs. `walletInbox` survived that pass and lived on for weeks, and it did * damage: the name made "one inbox per wallet" sound obvious, hiding that a user * upstream has **two** (public store repo and protected store repo — the only two * `AddInboxCap` commits in the engine, `engine/verifier/src/site.rs:128,149`). A * discipline applied by hand misses one; a test does not. * * So this pins the naming half of the design principle (`README.md`): a name either * belongs to the target's vocabulary — in which case it needs no translation and * survives migration — or it carries a marker saying WHY it exists only here, which * also says when it disappears. * * ── What it checks, and what it deliberately does not ───────────────────── * Only the PUBLISHED names, the ones a consumer application types. Internal names are * held to the same intent but not mechanically: the folder they live in already states * their fate, and pinning every internal identifier would fight refactoring for little. */ import { test, expect } from "bun:test"; import * as fs from "node:fs"; import * as path from "node:path"; /** * Words the TARGET itself uses, verified in `nextgraph-rs`. A published name built * from these needs no translation at migration. */ const TARGET_WORDS = new Set([ // addressing and objects "nuri", "doc", "docs", "document", "repo", "store", "stores", "branch", "graph", "overlay", "cap", "caps", "read", "write", "link", "links", "shape", "shapes", // actors and containers "user", "users", "session", "wallet", "inbox", "inboxes", "site", "principal", // scopes (upstream store types, `StoreRepo::from_type_and_repo`) "public", "protected", "private", "group", "dialog", "scope", // acts the target performs "create", "subscribe", "unsubscribe", "query", "update", "post", "share", "open", "fetch", "init", "watch", "sparql", "ng", "orm", "type", "types", // RDF / SPARQL terms the engine's own query paths use "subject", "base", "schema", "connected", "identity", "identities", // the reactive model the ORM exposes (`OrmSubscription`, `DeepSignalSet`) "observable", "deep", "signal", "set", ]); /** * Markers that name WHY something exists only in this library. Each says when it * disappears, which a bare `fake`/`tmp` would not. */ const EMULATION_MARKERS = new Set([ "virtual", "physical", "shim", "emulated", "polyfill", // `shared` as in "shared wallet" — the single fact every piece of scaffolding in this // library descends from. A name carrying it says both what it is and when it goes. "shared", ]); /** Glue with no domain meaning — never the load-bearing part of a name. */ const NEUTRAL = new Set([ "get", "set", "is", "has", "to", "for", "of", "my", "own", "all", "by", "with", "current", "reset", "configure", "config", "deps", "id", "ids", "address", "entity", "list", "resolve", "assert", "escape", "literal", "iri", "record", "registry", "change", "changed", "state", "value", "data", "info", "count", "the", "a", "an", "options", "opts", "result", "error", "signal", "filter", "placement", "and", "or", "make", "use", "on", "off", "from", "into", "at", "in", "out", "up", "down", // `union` is OURS — the bounded multi-document read — but it names an operation, // not a domain notion a consumer would have to unlearn. `eventually` is the // library's own name. "union", "eventually", "ensure", // `…Like` is a structural-typing suffix (`NgLike` = "whatever has ng's shape"), not // a domain word: it describes how the injection is typed, not what the thing is. "like", ]); /** `documentInboxAddress` → ["document","inbox","address"] ; `NG` → ["ng"]. */ function words(name: string): string[] { return name .replace(/([a-z0-9])([A-Z])/g, "$1 $2") .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2") .split(/[\s_]+/) .map((w) => w.toLowerCase()) .filter(Boolean); } const SRC = path.join(import.meta.dir, "..", "src"); /** Every identifier the two entry points publish, read from the `export` statements. */ function publishedNames(): string[] { const out = new Set(); for (const entry of ["index.ts", "polyfill.ts"]) { const text = fs.readFileSync(path.join(SRC, entry), "utf8"); // `export * as ns from "…"` for (const m of text.matchAll(/export \* as (\w+) from/g)) out.add(m[1]!); // `export { a, b as c }` / `export type { … }`, single- and multi-line for (const m of text.matchAll(/export (?:type )?\{([^}]*)\}/g)) { for (const raw of m[1]!.split(",")) { const name = raw.trim().replace(/^type /, "").split(/\s+as\s+/).pop()?.trim(); if (name) out.add(name); } } // `export const x` / `export function x` / `export interface x` for (const m of text.matchAll(/export (?:declare )?(?:const|function|class|interface|type) (\w+)/g)) { out.add(m[1]!); } // `export * from "./x"` — re-exports every name that module declares. for (const m of text.matchAll(/export \* from "\.\/([^"]+)"/g)) { const file = path.join(SRC, m[1]! + ".ts"); if (!fs.existsSync(file)) continue; const t = fs.readFileSync(file, "utf8"); for (const mm of t.matchAll(/^export (?:declare )?(?:const|function|class|interface|type) (\w+)/gm)) { out.add(mm[1]!); } } } return [...out]; } test("every published name is built from the target's vocabulary, or carries an emulation marker", () => { const offenders: string[] = []; for (const name of publishedNames()) { const ws = words(name); // A marker anywhere in the name licenses the whole name: it declares the thing // as ours and says when it goes. if (ws.some((w) => EMULATION_MARKERS.has(w))) continue; const unknown = ws.filter((w) => !TARGET_WORDS.has(w) && !NEUTRAL.has(w)); if (unknown.length > 0) offenders.push(`${name} → ${unknown.join(", ")}`); } // A failure here is not "rename to satisfy the test": it is a question. Does the // target have a word for this? Use it. Does the thing exist only here? Say so with a // marker. Is the word genuinely neutral glue? Add it to NEUTRAL, deliberately. expect(offenders).toEqual([]); }); test("no published name says `wallet` where the target says `user`", () => { // The specific regression that motivated this file. `wallet` is a legitimate target // word (a keyring IS a wallet upstream), so the generic check above cannot catch it — // what is wrong is using it for the thing that owns stores and inboxes. const wrong = publishedNames().filter((n) => /wallet/i.test(n) && /(inbox|store|doc|cap)/i.test(n), ); expect(wrong).toEqual([]); }); // --- the invariant the internal contract flagged as a migration risk ------- test("a reserved-namespace key cannot be produced by a consumer's normalizeId", async () => { // The reserved namespace hosts infrastructure accounts, and its guarantee is that no // user id lands there. That guarantee is not the library's to make — `normalizeId` is // injected by the consumer — so a careless one must be refused, not trusted. A // collision would key a user onto an infrastructure account: reads and writes on // documents that are not theirs. const { configureStoreRegistry, resetStoreRegistry } = await import("../src/polyfill"); const { ensureAccount, resetRegistryCache } = await import( "../src/shared-wallet/account-registry" ); configureStoreRegistry({ getSession: async () => ({ sessionId: "s", privateStoreId: "did:ng:o:p" }), normalizeId: () => "reserved:index", // pretends to be infrastructure }); resetRegistryCache(); await expect(ensureAccount("mallory")).rejects.toThrow(/reserved namespace/i); resetStoreRegistry(); resetRegistryCache(); }); // --- the contract's inventory must match the code ------------------------ /** * Read the appendix of `docs/api-contract.md` back into `{ group: names }`. * The appendix is a generated block; this parses the same shape. */ function contractInventory(): Record { const md = fs.readFileSync( path.join(import.meta.dir, "..", "..", "..", "docs", "api-contract.md"), "utf8", ); const appendix = md.slice(md.indexOf("## Appendix — full export inventory")); const out: Record = {}; for (const block of appendix.matchAll(/```text\n([\s\S]*?)```/g)) { for (const line of block[1]!.trim().split("\n")) { const i = line.indexOf(":"); if (i < 0) continue; const group = line.slice(0, i).trim(); const names = line.slice(i + 1).split(",").map((n) => n.trim()).filter(Boolean); out[group] = [...(out[group] ?? []), ...names].sort(); } } return out; } test("the api-contract appendix lists exactly what the entries export", () => { // The appendix is the instrument a reader diffs against when the surface moves. It // went stale once — still naming `storeRegistry`'s shim internals after the entry had // been narrowed to seven functions — and a stale inventory is worse than none: it // reads as verified. So the code decides, and this test is what makes the document // follow rather than drift. const inventory = contractInventory(); const direct = new Set(publishedNames()); // The namespace names themselves are the appendix's group headings, not entries. for (const group of Object.keys(inventory)) if (group !== "direct") direct.delete(group); const missing = [...direct].filter( (n) => !Object.values(inventory).some((names) => names.includes(n)), ); const extra = Object.values(inventory) .flat() .filter((n) => !direct.has(n) && !isNamespaceMember(n)); expect({ missing, extra }).toEqual({ missing: [], extra: [] }); }); /** Names that live inside a re-exported namespace rather than on the entry itself. */ function isNamespaceMember(name: string): boolean { const src = path.join(import.meta.dir, "..", "src"); for (const entry of ["index.ts", "polyfill.ts"]) { const text = fs.readFileSync(path.join(src, entry), "utf8"); for (const m of text.matchAll(/export \* as \w+ from "\.\/([^"]+)"/g)) { const file = path.join(src, m[1]! + ".ts"); if (fs.existsSync(file) && new RegExp(`\\b${name}\\b`).test(fs.readFileSync(file, "utf8"))) { return true; } } } return false; }