feat(foundation): offline synthetic scope/permission inspector (FI-FILBERT-8 APPROVED r6)

Rocko-authored, Filbert-reviewed inspector (r6 manifest
a4a44930...) with full review/build/verdict evidence under
docs/plans/reviews. 43/0 selftests, oracle zero-disagreement,
foundation checker PASS. Owner A9 acceptance recorded separately.
This commit is contained in:
2026-09-07 14:06:35 -05:00
parent 127a54fdff
commit 8ebddd6f93
423 changed files with 665972 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
/**
* inspector-content-digest/1 — canonical form and digest for MOCK registry
* content only (charter candidate 3 §6; feasibility r2 §6.1).
*
* Domain: objects, arrays, ASCII strings, booleans, null and safe integers.
* Object keys are sorted by UTF-16 code unit; separators are compact; array
* order is preserved; the digest is "sha256:" + hex(SHA-256(UTF-8 bytes)).
* Anything outside the domain refuses with CanonicalError (reason
* unsupported-capability): non-ASCII strings, non-integral or unsafe numbers,
* negative zero, undefined, functions, symbols, bigints.
*
* This is NOT the candidate README launch-fingerprint projection and makes no
* JCS (RFC 8785) claim. Pure module: imports only node:crypto.
*/
import { createHash } from "node:crypto";
export const DIGEST_ALGORITHM = "inspector-content-digest/1";
export class CanonicalError extends Error {
constructor(code) {
super(code);
this.name = "CanonicalError";
this.code = code;
this.reason = "unsupported-capability";
}
}
function isAscii(text) {
for (let i = 0; i < text.length; i += 1) {
if (text.charCodeAt(i) > 0x7f) return false;
}
return true;
}
function encodeString(text) {
if (!isAscii(text)) throw new CanonicalError("string-not-ascii");
// JSON.stringify on an ASCII string escapes exactly ", \ and U+0000..U+001F
// (short forms \b \f \n \r \t, otherwise \u00XX lowercase), matching Python
// json.dumps for the ASCII domain.
return JSON.stringify(text);
}
function encodeValue(value, depth) {
if (depth > 64) throw new CanonicalError("depth-exceeded");
if (value === null) return "null";
switch (typeof value) {
case "boolean":
return value ? "true" : "false";
case "number":
if (!Number.isSafeInteger(value) || Object.is(value, -0)) {
throw new CanonicalError("number-not-safe-integer");
}
return String(value);
case "string":
return encodeString(value);
case "object":
break;
default:
throw new CanonicalError("value-not-json");
}
if (Array.isArray(value)) {
const parts = [];
for (const item of value) parts.push(encodeValue(item, depth + 1));
return `[${parts.join(",")}]`;
}
const keys = Object.keys(value);
keys.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
const parts = [];
for (const key of keys) {
const item = value[key];
if (item === undefined) throw new CanonicalError("value-not-json");
parts.push(`${encodeString(key)}:${encodeValue(item, depth + 1)}`);
}
return `{${parts.join(",")}}`;
}
/** Canonical compact encoding, or throws CanonicalError. */
export function canonicalize(value) {
return encodeValue(value, 0);
}
/** "sha256:<hex>" of the canonical encoding, or throws CanonicalError. */
export function digestOf(value) {
const canonical = canonicalize(value);
return `sha256:${createHash("sha256").update(canonical, "utf8").digest("hex")}`;
}
+106
View File
@@ -0,0 +1,106 @@
// Unit tests for inspector-content-digest/1 (charter §6, feasibility r2 §6.1 vectors).
import { test } from "node:test";
import assert from "node:assert/strict";
import { canonicalize, digestOf, CanonicalError, DIGEST_ALGORITHM } from "./canonical.mjs";
const V1 = {
restrictions: {
operations: ["work.read", "file.read"],
readPaths: [{ root: "workspace", path: null }],
writePaths: [],
network: "none",
endpointRefs: [],
},
};
function refuses(value, code) {
assert.throws(() => digestOf(value), (e) => e instanceof CanonicalError && e.code === code && e.reason === "unsupported-capability");
}
test("algorithm identifier is fixed", () => {
assert.equal(DIGEST_ALGORITHM, "inspector-content-digest/1");
});
test("V1: pinned digest of the reader restrictions", () => {
assert.equal(
canonicalize(V1),
'{"restrictions":{"endpointRefs":[],"network":"none","operations":["work.read","file.read"],"readPaths":[{"path":null,"root":"workspace"}],"writePaths":[]}}',
);
assert.equal(digestOf(V1), "sha256:0bc44e14fd8354a8a85be879306a54881da7a0cb4d3dcf1f88bc4bc08a40dc9c");
});
test("V2: array order is significant", () => {
const v2 = structuredClone(V1);
v2.restrictions.operations = ["file.read", "work.read"];
assert.equal(digestOf(v2), "sha256:63817bffc57803ca6ac5df971aa0a160d4be73646394b3bc536578bd66c82660");
assert.notEqual(digestOf(v2), digestOf(V1));
});
test("V3: writer restrictions", () => {
const v3 = structuredClone(V1);
v3.restrictions.operations = ["work.read", "file.read", "file.change"];
v3.restrictions.writePaths = [{ root: "workspace", path: "docs" }];
assert.equal(digestOf(v3), "sha256:d538c865808cfe7665956615fb48f69819baa0833626d1ef6e1cb3614acbc7da");
});
test("object key order is not significant", () => {
const reordered = {
restrictions: {
writePaths: [], endpointRefs: [], network: "none",
readPaths: [{ path: null, root: "workspace" }],
operations: ["work.read", "file.read"],
},
};
assert.equal(digestOf(reordered), digestOf(V1));
const nullProto = Object.create(null);
nullProto.restrictions = reordered.restrictions;
assert.equal(digestOf(nullProto), digestOf(V1));
});
test("V4: non-integral numbers refuse", () => {
const v4 = structuredClone(V1);
v4.restrictions.weight = 1.5;
refuses(v4, "number-not-safe-integer");
refuses({ a: -0 }, "number-not-safe-integer");
refuses({ a: 9007199254740992 }, "number-not-safe-integer");
refuses({ a: Number.NaN }, "number-not-safe-integer");
refuses({ a: Number.POSITIVE_INFINITY }, "number-not-safe-integer");
assert.equal(canonicalize({ a: 9007199254740991, b: -5, c: 0 }), '{"a":9007199254740991,"b":-5,"c":0}');
});
test("V5: non-ASCII strings and keys refuse", () => {
const v5 = structuredClone(V1);
v5.restrictions.note = "café";
refuses(v5, "string-not-ascii");
refuses({ "kéy": 1 }, "string-not-ascii");
refuses({ a: "\u{1F600}" }, "string-not-ascii");
});
test("ASCII control characters are escaped like Python json.dumps", () => {
assert.equal(canonicalize({ a: "x\ty\n\"\\" }), '{"a":"x\\ty\\n\\u0001\\"\\\\"}');
assert.equal(canonicalize({ a: "" }), '{"a":""}');
});
test("keys sort by UTF-16 code unit, not locale", () => {
assert.equal(canonicalize({ b: 1, B: 2, a: 3, A: 4, _: 5, "10": 6, "2": 7 }), '{"10":6,"2":7,"A":4,"B":2,"_":5,"a":3,"b":1}');
});
test("undefined, functions, symbols, bigints refuse; depth is bounded", () => {
refuses({ a: undefined }, "value-not-json");
refuses({ a: () => 1 }, "value-not-json");
refuses({ a: Symbol("s") }, "value-not-json");
refuses({ a: 1n }, "value-not-json");
refuses(undefined, "value-not-json");
let deep = 1;
for (let i = 0; i < 64; i += 1) deep = [deep];
assert.ok(canonicalize(deep));
deep = [deep];
refuses(deep, "depth-exceeded");
});
test("booleans, null and nesting encode compactly and deterministically", () => {
const value = { z: [true, false, null, { y: [], x: {} }] };
assert.equal(canonicalize(value), '{"z":[true,false,null,{"x":{},"y":[]}]}');
assert.equal(digestOf(value), digestOf(structuredClone(value)));
assert.match(digestOf(value), /^sha256:[0-9a-f]{64}$/);
});
+317
View File
@@ -0,0 +1,317 @@
// Isolated CLI tests: usage, I/O failures, bounds, output escaping, privacy and
// non-effect inventory (charter §7, §8, §10.3). Every run uses a disposable synthetic
// sandbox (home, cwd, fixtures, data-root) with canary files; nothing here touches
// the operator's real HOME, ~/.mosaic or any credential.
import { test, before, after } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import {
mkdtempSync, mkdirSync, writeFileSync, symlinkSync, readdirSync, lstatSync, readFileSync, rmSync, chmodSync,
} from "node:fs";
import { join, dirname } from "node:path";
import { tmpdir } from "node:os";
import { createHash } from "node:crypto";
import { fileURLToPath } from "node:url";
import { renderText, renderJson, escapeText, run } from "../foundation-inspect.mjs";
import { exitFor } from "./resolve.mjs";
const HERE = dirname(fileURLToPath(import.meta.url));
const CLI = join(HERE, "..", "foundation-inspect.mjs");
const DEMO = join(HERE, "fixtures", "demo");
const CANARY = "CANARY-DO-NOT-PRINT";
let root;
let env;
let cwd;
function sha256(buf) {
return createHash("sha256").update(buf).digest("hex");
}
/** path/type/size/mode/uid/gid/ino/mtime/content-hash inventory of a tree, sorted by path (charter §8 ownership included). */
function inventory(dir, out = []) {
for (const name of readdirSync(dir).sort()) {
const p = join(dir, name);
const st = lstatSync(p);
const type = st.isDirectory() ? "dir" : st.isSymbolicLink() ? "link" : st.isFIFO() ? "fifo" : st.isFile() ? "file" : "other";
const content = st.isFile() ? sha256(readFileSync(p)) : null;
out.push([p.slice(root.length), type, st.size, st.mode, st.uid, st.gid, st.ino, st.mtimeMs, content].join("|"));
if (st.isDirectory()) inventory(p, out);
}
return out;
}
function inspect(args, opts = {}) {
const r = spawnSync(process.execPath, [CLI, ...args], {
cwd: opts.cwd || cwd, env: opts.env || env, encoding: "buffer", timeout: 30000,
});
assert.equal(r.error, undefined, "spawn failed");
return { status: r.status, stdout: r.stdout.toString("utf8"), stderr: r.stderr.toString("utf8"), raw: r.stdout };
}
function jsonRun(args, opts) {
const r = inspect(["--json", ...args], opts);
assert.equal(r.stderr, "", "stderr must be empty");
const parsed = JSON.parse(r.stdout);
assert.ok(!("exit" in parsed), "exit is process metadata, never a serialized field");
assert.equal(r.status, exitFor(parsed), "process exit code is derived from the closed result");
return { ...r, parsed };
}
before(() => {
root = mkdtempSync(join(tmpdir(), "foundation-cli-"));
for (const d of ["home", "home/.mosaic", "home/.config", "home/.config/mosaic", "cwd", "fixtures", "data-root", "data-root/runs"]) {
mkdirSync(join(root, d), { recursive: true });
}
writeFileSync(join(root, "home", ".mosaic", "credentials.json"), `{"token":"${CANARY}-1"}\n`);
writeFileSync(join(root, "home", ".config", "mosaic", "config.json"), `{"secret":"${CANARY}-2"}\n`);
writeFileSync(join(root, "cwd", "bundle.json"), `{"canary":"${CANARY}-3"}\n`);
writeFileSync(join(root, "data-root", "runs", "result.json"), `{"canary":"${CANARY}-4"}\n`);
writeFileSync(join(root, "cwd", "package.json"), `{"name":"${CANARY}-5"}\n`);
for (const name of readdirSync(DEMO)) {
if (name.endsWith(".json") && !name.includes(".expected.")) writeFileSync(join(root, "fixtures", name), readFileSync(join(DEMO, name)));
}
cwd = join(root, "cwd");
env = {
HOME: join(root, "home"),
TMPDIR: join(root, "data-root"),
PATH: "/nonexistent",
MOSAIC_CONFIG: join(root, "home", ".config", "mosaic", "config.json"),
XDG_CONFIG_HOME: join(root, "home", ".config"),
MOSAIC_DATA_ROOT: join(root, "data-root"),
LANG: "C",
};
});
after(() => {
if (root) rmSync(root, { recursive: true, force: true });
});
test("usage errors exit 2 usage-invalid without touching any file", () => {
const before = inventory(root);
for (const args of [[], ["--json"], ["a.json", "b.json"], ["--nope", "a.json"], ["--json", "--json", "a.json"], ["-x"]]) {
const r = jsonRun(args);
assert.equal(r.status, 2, JSON.stringify(args));
assert.equal(r.parsed.rule, "usage-invalid");
assert.equal(r.parsed.reason, "invalid-request");
assert.equal(r.parsed.result, "invalid");
assert.equal(r.parsed.diagnostic, null);
}
const t = inspect([]);
assert.equal(t.status, 2);
assert.match(t.stdout, /^SYNTHETIC PREVIEW/);
assert.deepEqual(inventory(root), before);
});
test("missing file: exit 4 input-open-failed with the input path echoed in the diagnostic only", () => {
const r = jsonRun(["does-not-exist.json"]);
assert.equal(r.status, 4);
assert.equal(r.parsed.rule, "input-open-failed");
assert.equal(r.parsed.reason, "io-failure");
assert.deepEqual(r.parsed.diagnostic, { byteOffset: null, inputPath: "does-not-exist.json" });
assert.equal(r.parsed.selection, null);
});
test("symlink to a valid bundle is refused (O_NOFOLLOW) with exit 4", () => {
symlinkSync(join(root, "fixtures", "demo-read-w1.json"), join(root, "cwd", "link.json"));
const r = jsonRun(["link.json"]);
assert.equal(r.status, 4);
assert.equal(r.parsed.rule, "input-open-failed");
assert.equal(r.parsed.diagnostic.inputPath, "link.json");
});
test("directory and FIFO are not regular files: exit 4 input-not-regular", () => {
const d = jsonRun([join(root, "fixtures")]);
assert.equal(d.status, 4);
assert.equal(d.parsed.rule, "input-not-regular");
const fifo = join(root, "cwd", "pipe.json");
const mk = spawnSync("mkfifo", [fifo], { env: { ...env, PATH: "/usr/bin:/bin" } });
if (mk.status !== 0) {
assert.fail("mkfifo unavailable; FIFO case cannot be exercised");
}
const f = jsonRun(["pipe.json"]);
assert.equal(f.status, 4, "FIFO must be refused after O_NONBLOCK open, not read");
assert.equal(f.parsed.rule, "input-not-regular");
});
test("unreadable regular file: exit 4 input-open-failed", (t) => {
if (typeof process.getuid === "function" && process.getuid() === 0) {
t.skip("root ignores mode bits");
return;
}
const p = join(root, "cwd", "unreadable.json");
writeFileSync(p, "{}\n");
chmodSync(p, 0o000);
const r = jsonRun(["unreadable.json"]);
assert.equal(r.status, 4);
assert.equal(r.parsed.rule, "input-open-failed");
chmodSync(p, 0o600);
});
test("oversize file: exit 2 input-too-large with a null diagnostic (no path echo)", () => {
const p = join(root, "cwd", "big.json");
const buf = Buffer.alloc(1024 * 1024 + 1, 0x20);
buf[0] = 0x7b; buf[buf.length - 1] = 0x7d;
writeFileSync(p, buf);
const r = jsonRun(["big.json"]);
assert.equal(r.status, 2);
assert.equal(r.parsed.rule, "input-too-large");
assert.equal(r.parsed.reason, "invalid-request");
assert.equal(r.parsed.diagnostic, null);
rmSync(p);
// exactly 1 MiB is read and parsed (here: an object, so a shape refusal, not a bound)
const exact = Buffer.alloc(1024 * 1024, 0x20);
exact[0] = 0x7b; exact[exact.length - 1] = 0x7d;
writeFileSync(p, exact);
const e = jsonRun(["big.json"]);
assert.equal(e.status, 2);
assert.equal(e.parsed.rule, "shape-missing-field");
rmSync(p);
});
test("parse failure carries byteOffset and never the path", () => {
writeFileSync(join(root, "cwd", "dup.json"), '{"a": 1, "a": 2}');
const r = jsonRun(["dup.json"]);
assert.equal(r.status, 2);
assert.equal(r.parsed.rule, "input-parse-failed");
assert.deepEqual(r.parsed.diagnostic, { byteOffset: 9, inputPath: null });
});
test("text output is derived from the same result as JSON output", () => {
for (const name of ["demo-read-w1", "demo-change-w1", "adm-registration-revoked"]) {
const j = jsonRun([join(root, "fixtures", `${name}.json`)]);
const t = inspect([join(root, "fixtures", `${name}.json`)]);
assert.equal(t.stderr, "");
assert.equal(t.status, j.status);
assert.equal(t.stdout, renderText(j.parsed), name);
assert.equal(j.stdout, renderJson(j.parsed), name);
assert.equal(j.stdout, renderJson(JSON.parse(j.stdout)), `${name}: JSON output is a pure round-trip`);
}
});
test("owner demo: permitted preview exits 0 and prints the disclaimer first", () => {
const t = inspect([join(root, "fixtures", "demo-read-w1.json")]);
assert.equal(t.status, 0);
const lines = t.stdout.split("\n");
assert.equal(lines[0], "SYNTHETIC PREVIEW — NO LIVE EFFECTS");
assert.equal(lines[1], "preview: no live registrations or permission grants");
assert.ok(lines.includes("result: allowed"));
assert.ok(!lines.some((l) => l.startsWith("exit")), "no exit line in text output");
});
test("risky code points in echoed fields are escaped in both renderings", () => {
const bundle = JSON.parse(readFileSync(join(root, "fixtures", "demo-read-w1.json"), "utf8"));
bundle.operation = { name: "file.read", target: { root: "workspace", path: "src/a\u2028b" } };
writeFileSync(join(root, "cwd", "sep.json"), `${JSON.stringify(bundle)}\n`);
const j = jsonRun(["sep.json"]);
assert.equal(j.status, 0, "U+2028 is Zl, accepted by the pinned checker");
assert.ok(!j.stdout.includes("\u2028"), "JSON output escapes U+2028");
assert.ok(j.stdout.includes("\\u2028"));
const t = inspect(["sep.json"]);
assert.ok(!t.stdout.includes("\u2028"));
assert.ok(t.stdout.includes("operation: file.read target workspace:src/a\\u2028b"));
// ESC in a selection id is a shape refusal; the id must still not reach the terminal raw
bundle.operation = { name: "work.read", target: null };
bundle.selection.agentId = "agent-a\u001b[31m";
writeFileSync(join(root, "cwd", "esc.json"), `${JSON.stringify(bundle)}\n`);
const e = inspect(["esc.json"]);
assert.equal(e.status, 2);
assert.ok(!e.stdout.includes("\u001b"));
assert.ok(e.stdout.includes("selection: null"));
assert.equal(escapeText("a\u001b\u2028\\\u{1F600}b"), "a\\u001b\\u2028\\u005c\u{1F600}b");
});
test("canaries never reach stdout: bundle strings, HOME, config, cwd, data-root", () => {
const bundle = JSON.parse(readFileSync(join(root, "fixtures", "demo-read-w1.json"), "utf8"));
for (const r of bundle.records) if (r.kind === "project") r.payload.displayName = `${CANARY}-6`;
writeFileSync(join(root, "cwd", "canary.json"), `${JSON.stringify(bundle)}\n`);
const outputs = [];
for (const args of [["canary.json"], ["--json", "canary.json"], ["missing.json"], ["--json", "missing.json"], []]) {
const r = inspect(args);
outputs.push(r.stdout, r.stderr);
}
const joined = outputs.join("\n");
assert.ok(!joined.includes(CANARY), "canary content leaked to output");
assert.ok(!joined.includes(root), "sandbox root path leaked to output");
});
test("HOME/config independence: identical bytes across different synthetic environments", () => {
const args = ["--json", join(root, "fixtures", "demo-change-w1.json")];
const a = inspect(args);
const altHome = join(root, "home2");
mkdirSync(altHome, { recursive: true });
const b = inspect(args, { env: { HOME: altHome, PATH: "/nonexistent", MOSAIC_CONFIG: "/nonexistent/config.json", NODE_OPTIONS: "" }, cwd: root });
const c = inspect(args);
assert.equal(a.stdout, b.stdout);
assert.equal(a.stdout, c.stdout);
assert.equal(a.status, 3);
assert.equal(JSON.parse(a.stdout).result, "unresolved");
});
test("non-effect: before/after inventory of the sandbox is unchanged by every kind of run", () => {
rmSync(join(root, "cwd", "pipe.json"), { force: true });
const before = inventory(root);
const runs = [
["demo-read-w1.json"], ["--json", "demo-change-w1.json"], ["adm-registration-revoked.json"],
["prop-message-is-not-authority.json"], ["demo-file-change-src.json"],
];
for (const args of runs) {
const r = inspect(args.map((a) => (a.endsWith(".json") ? join(root, "fixtures", a) : a)));
assert.equal(r.stderr, "");
assert.ok([0, 3].includes(r.status), args.join(" "));
}
inspect(["../home/.mosaic/credentials.json"]);
inspect([join(root, "home", ".config", "mosaic", "config.json")]);
inspect(["missing.json"]);
inspect([]);
const after = inventory(root);
assert.deepEqual(after, before);
assert.deepEqual(readdirSync(join(root, "data-root")), ["runs"]);
assert.deepEqual(readdirSync(join(root, "data-root", "runs")), ["result.json"]);
});
test("in-process run() agrees with the spawned CLI", () => {
const p = join(root, "fixtures", "demo-read-w1.json");
const inproc = run(["--json", p]);
const spawned = jsonRun([p]);
assert.deepEqual(inproc.result, spawned.parsed);
assert.equal(inproc.json, true);
assert.equal(run([]).result.rule, "usage-invalid");
assert.equal(run(["--json"]).json, true);
});
test("profile negative control: one final LF in a typed field is refused pre-admission through the CLI and never echoed", () => {
const bundle = JSON.parse(readFileSync(join(root, "fixtures", "demo-read-w1.json"), "utf8"));
bundle.selection.agentId = `${bundle.selection.agentId}\n`;
const p = join(root, "cwd", "profile-lf.json");
writeFileSync(p, `${JSON.stringify(bundle)}\n`);
const r = jsonRun(["profile-lf.json"]);
assert.equal(r.status, 2);
assert.equal(r.parsed.result, "invalid");
assert.equal(r.parsed.reason, "invalid-request");
assert.equal(r.parsed.rule, "profile-pattern-mismatch");
assert.equal(r.parsed.selection, null);
assert.equal(r.parsed.operation, null);
assert.equal(r.parsed.proposal, null);
assert.equal(r.parsed.diagnostic, null);
assert.ok(!r.stdout.includes("agent-a"), "typed value never echoed");
const t = inspect(["profile-lf.json"]);
assert.equal(t.status, 2);
assert.equal(t.stderr, "");
assert.ok(t.stdout.includes("\nrule: profile-pattern-mismatch\n"));
assert.ok(t.stdout.includes("\nselection: null\n"));
assert.ok(!t.stdout.includes("agent-a"));
// Two final LFs fail the schema pattern itself (both implementations), not the profile.
bundle.selection.agentId = `${bundle.selection.agentId}\n`;
writeFileSync(p, `${JSON.stringify(bundle)}\n`);
const two = jsonRun(["profile-lf.json"]);
assert.equal(two.status, 2);
assert.equal(two.parsed.rule, "shape-pattern-mismatch");
// Positive control: free-form text with escaped newlines is not blanket-rejected.
const ok = JSON.parse(readFileSync(join(root, "fixtures", "demo-read-w1.json"), "utf8"));
ok.records.find((x) => x.kind === "mission" && x.id === "m-w1").payload.objective = "line one\nline two\n";
writeFileSync(join(root, "cwd", "text-lf.json"), `${JSON.stringify(ok)}\n`);
const allowed = jsonRun(["text-lf.json"]);
assert.equal(allowed.status, 0);
assert.equal(allowed.parsed.result, "allowed");
});
+98
View File
@@ -0,0 +1,98 @@
// Every fixture case runs through the real CLI in a fresh subprocess; the observed
// exit/result/reason/rule/proposalRule/byteOffset must equal index.json. The owner
// demo goldens are compared byte-for-byte.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { readFileSync, readdirSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { renderText, run } from "../foundation-inspect.mjs";
import { exitFor } from "./resolve.mjs";
const HERE = dirname(fileURLToPath(import.meta.url));
const FIXTURES = join(HERE, "fixtures");
const CLI = join(HERE, "..", "foundation-inspect.mjs");
const INDEX = JSON.parse(readFileSync(join(FIXTURES, "index.json"), "utf8"));
const ENV = { PATH: "/nonexistent", HOME: "/nonexistent", LANG: "C" };
const RAW_RISKY_RE = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u2028\u2029\ufeff\u200b]/;
function spawnJson(file) {
const r = spawnSync(process.execPath, [CLI, "--json", file], { cwd: FIXTURES, env: ENV, encoding: "utf8", timeout: 30000 });
assert.equal(r.error, undefined);
assert.equal(r.stderr, "", `${file}: stderr must be empty`);
const parsed = JSON.parse(r.stdout);
assert.ok(!("exit" in parsed), `${file}: exit is not a serialized field`);
assert.equal(r.status, exitFor(parsed), `${file}: exit code derived from the closed result`);
return { parsed, stdout: r.stdout, status: r.status };
}
test("index is complete and every listed file exists", () => {
assert.equal(INDEX.count, INDEX.cases.length);
assert.equal(INDEX.generator, "build-fixtures.mjs");
const names = INDEX.cases.map((c) => c.name);
assert.deepEqual(names, [...names].sort(), "index sorted by name");
assert.equal(new Set(names).size, names.length);
const listed = new Set(INDEX.cases.map((c) => c.file));
for (const dir of ["bundles", "raw"]) {
for (const f of readdirSync(join(FIXTURES, dir))) assert.ok(listed.has(`${dir}/${f}`), `${dir}/${f} not in index`);
}
for (const c of INDEX.cases) assert.ok(existsSync(join(FIXTURES, c.file)), c.file);
});
test("every fixture case matches its expected verdict through the CLI", () => {
const failures = [];
for (const c of INDEX.cases) {
const { parsed, status } = spawnJson(c.file);
const got = {
exit: status, result: parsed.result, reason: parsed.reason, rule: parsed.rule,
proposalRule: parsed.proposal === null ? null : parsed.proposal.rule,
};
const want = { exit: c.expect.exit, result: c.expect.result, reason: c.expect.reason, rule: c.expect.rule, proposalRule: c.expect.proposalRule };
if ("byteOffset" in c.expect) {
got.byteOffset = parsed.diagnostic === null ? null : parsed.diagnostic.byteOffset;
want.byteOffset = c.expect.byteOffset;
if (parsed.diagnostic !== null) assert.equal(parsed.diagnostic.inputPath, null, `${c.name}: path never echoed on parse failure`);
} else {
assert.equal(parsed.diagnostic, null, `${c.name}: diagnostic only on I/O or parse failure`);
}
if (JSON.stringify(got) !== JSON.stringify(want)) failures.push(`${c.name}: got ${JSON.stringify(got)} want ${JSON.stringify(want)}`);
if (status === 0) assert.equal(parsed.rule, null, c.name);
if (parsed.proposal !== null) assert.equal(parsed.result, parsed.proposal.result, c.name);
}
assert.deepEqual(failures, []);
});
test("in-process text rendering agrees with the JSON result for every case", () => {
for (const c of INDEX.cases) {
const file = join(FIXTURES, c.file);
const text = run([file]);
const json = run(["--json", file]);
assert.equal(text.json, false);
assert.deepEqual(text.result, json.result, c.name);
assert.equal(text.exit, c.expect.exit, c.name);
assert.equal(json.exit, c.expect.exit, c.name);
const rendered = renderText(text.result);
assert.ok(rendered.startsWith("SYNTHETIC PREVIEW — NO LIVE EFFECTS\n"), c.name);
assert.ok(/\ndiagnostic: [^\n]*\n$/.test(rendered), `${c.name}: text ends with the diagnostic line, no exit line`);
assert.ok(!/^exit/m.test(rendered), c.name);
assert.ok(!RAW_RISKY_RE.test(rendered), `${c.name}: raw control in text output`);
}
});
test("owner demo goldens match byte-for-byte", () => {
const names = readdirSync(join(FIXTURES, "demo")).filter((f) => f.endsWith(".json") && !f.includes(".expected."));
assert.equal(names.length, 5);
for (const name of names) {
const base = name.slice(0, -5);
const file = join(FIXTURES, "demo", name);
const text = spawnSync(process.execPath, [CLI, file], { cwd: FIXTURES, env: ENV, encoding: "utf8" });
const json = spawnSync(process.execPath, [CLI, "--json", file], { cwd: FIXTURES, env: ENV, encoding: "utf8" });
assert.equal(text.stdout, readFileSync(join(FIXTURES, "demo", `${base}.expected.txt`), "utf8"), `${base}.expected.txt`);
assert.equal(json.stdout, readFileSync(join(FIXTURES, "demo", `${base}.expected.json`), "utf8"), `${base}.expected.json`);
assert.equal(text.status, json.status);
assert.equal(String(text.status), readFileSync(join(FIXTURES, "demo", `${base}.expected.exit`), "utf8").trim());
// the demo copy equals the indexed bundle it was copied from
assert.equal(readFileSync(file, "utf8"), readFileSync(join(FIXTURES, "bundles", name), "utf8"), `${base}: demo copy drifted`);
}
});
+101
View File
@@ -0,0 +1,101 @@
# Foundation inspector fixtures
Synthetic, deterministic inputs for `scripts/foundation-inspect.mjs` and its tests.
Nothing here is live: identities, digests, times and paths are invented by
`build-fixtures.mjs` (SHA-256 of `synthetic:<label>` for every fake digest).
| Path | Content |
| --- | --- |
| `build-fixtures.mjs` | Generator. `node scripts/foundation/fixtures/build-fixtures.mjs <out-dir>` |
| `bundles/<case>.json` | One complete bundle per evaluation case (pretty JSON, trailing newline) |
| `raw/<case>.json` | Byte-exact lexical cases (duplicate keys, BOM, invalid UTF-8, oversize, ...) |
| `index.json` | Expected `exit/result/reason/rule/proposalRule[/byteOffset]` per case, sorted by name |
| `demo/<case>.json` | Owner-demo copies of five bundles, byte-identical to `bundles/` |
| `demo/<case>.expected.{txt,json,exit}` | Goldens: text output, `--json` output and exit code |
## Regeneration
```sh
node scripts/foundation/fixtures/build-fixtures.mjs /tmp/fx
diff -r /tmp/fx/bundles scripts/foundation/fixtures/bundles
diff -r /tmp/fx/raw scripts/foundation/fixtures/raw
diff /tmp/fx/index.json scripts/foundation/fixtures/index.json
```
`scripts/test-foundation.sh` performs exactly this comparison; a checked-in
fixture that differs from a fresh generation fails the suite. Goldens are
produced by running the inspector on the demo copies:
```sh
cd scripts/foundation/fixtures/demo
for f in *.json; do case "$f" in *.expected.*) continue;; esac
b="${f%.json}"
node ../../../foundation-inspect.mjs "$f" > "$b.expected.txt"; echo $? > "$b.expected.exit"
node ../../../foundation-inspect.mjs --json "$f" > "$b.expected.json"
done
```
## Case groups (see `index.json`)
positive, shape, profile, identity, structure, cycle, continuity, ownership,
operation, admission, registration-delegation, proposal, lexical (raw). Every
rule in the closed vocabulary except the CLI/I-O rules (covered by
`../cli.test.mjs` with a disposable sandbox) is exercised by at least one case;
`../resolve.test.mjs` enforces that.
The `profile-*` cases (addendum FI-C2-1) cover the strict typed-string profile:
one final LF on an `id`/`runtimeId`/`digest` value is schema-valid under the pinned
checker's `$` semantics and refused by the inspector as `profile-pattern-mismatch`
before any identity lookup (group `profile`); two final LFs, CRLF, CR, interior LF,
U+2028 and U+2029 fail the schema pattern itself (group `shape`); free-form text
with escaped newlines stays allowed (group `positive`).
The FI-FILBERT-6 correction cases (verdict findings F1F5) are the review's witness
recipes rebuilt from the frozen demo bundles, each with positive controls:
`adm-registry-declaration-*` and `adm-*-authorization-undeclared` (F1: every consulted
registry reference resolves by exact registry/id/revision/digest, rule
`registry-declaration-missing`; consulted dependency/mission/subject-task
authorizations must be declared), `adm-assigned-task-without-work-read*` and
`prop-task-only-*` (F2: a bound task is consulted work), `prop-issuer-*` (F3: the
issuer ceiling narrows both requester calculations and work access is rechecked),
`cycle-*versioned*` / `cycle-*-cross-revision` / `cycle-mission-parent-*revision*`
(F4: revision-exact cycle graphs), `shape-order-*` and `prop-message-*` /
`prop-delegation-missing-after-requester-admission` (F5: first failure independent of
record input order; message-is-not-authority after structural validation, before
requester admission). `../resolve.test.mjs` asserts each recipe's first failure and
permutation independence directly.
The FI-ROCKO-7 ordering cases extend F5 to the declared inventories (charter §10.4
applies beyond records): `shape-order-registries-*`, `shape-order-artifacts-*`,
`shape-order-authorizations-*`, `shape-order-delegation-inputs-*` (two different shape
errors per pair, forward and reversed, plus a "swapped" pair where the errors change
places and a malformed entry without a readable typed key, which sorts last),
`shape-order-family-precedence-*` (records, registries, artifacts, authorizations,
delegationInputs stay in that stage order), `profile-order-*` (strict-profile
violations follow the same key; any shape failure precedes them),
`shape-order-registry-digest-*` / `shape-order-profile-precedes-digest-registries` /
`shape-order-two-digest-mismatches-reversed` (content-digest traversal in
registry/id/revision/digest order after the profile stage), `dup-order-*` (identity
stage) and `struct-order-delegation-inputs-*` (structural stage). Typed keys:
registries `registry/id/revision/digest`, artifacts `runId/artifactId/digest`,
authorizations the runtime id, delegationInputs `decisionRef` kind/id/scope/revision;
equal keys tie-break on the entry's total ordering form (below). Semantic sequence arrays (instruction,
skill, endpoint, dependency, subject and evidence lists, paths, canonical content) are
never reordered. `../resolve.test.mjs` asserts the located first failure over every
permutation (all permutations for inventories of up to four entries, a fixed
deterministic family beyond that).
The FI-ROCKO-8 cases (FI-FILBERT-7 finding R5-1) fix the tie-break itself: r5 broke
ties on the ASCII-only mock-digest canonicalizer and mapped every non-ASCII entry to one
empty form, so two distinct Unicode entries with equal (or no) typed keys fell back to
input order. The inspector now orders ties by a total ordering form defined over the
whole strict-JSON input domain (sorted keys, array order kept, every string as a JSON
literal, no normalization), which is injective on distinct values and never a digest or
a validity judgement. `shape-order-unkeyed-unicode-<family>-{forward,reversed}` (two
unreadable non-ASCII entries per family: a string and an object, `shape-type-mismatch`
in both orders), `shape-order-equal-key-unicode-{records,registries}-{forward,reversed}`
(two copies with identical typed keys, a legitimate non-ASCII value and one different
shape error each: `shape-unknown-field` in both orders) and
`positive-unicode-display-name-allowed` (control). `../resolve.test.mjs` asserts the
form's totality and injectivity against `canonicalize()` refusals and the permutation
stability of every pair.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More