/* Library emission mode — the K-fixture conformance suite over the * ratified design. Every fixture runs TWICE, once per emission, with the * profile's `emission` field flipped by the harness, and outputs (sink * message text included) must be identical across the two runs — the * "reference/differential emission" posture; library mode has no fallback * concept. * * K1 symbols-exact nm over the archive: prefix-carrying external * definitions equal the declared set, both * directions; no prefix-carrying undefineds * K2 scalar-roundtrip f64 (-0, NaN, MAX_SAFE_INTEGER), bool, and the * u8/u32/i32 plumbing classes * K3 buffer-roundtrip string/bytes in/out, NUL-termination, NULL with * len 0, BOTH arena postures over one library * K4 init-rerun three identical sessions (globals, refcounted * state, run-once guards all reset) * K5 trap-to-sink-once the range trap's message + address exactly * once, no process termination from compiled * code — structured (SC4014, the trapping * export's symbol, default text, no remediation * on this no-teachings profile); a trap during * init routes the same with the init symbol * K6 pre-registration a trap before sink registration aborts * K7 escaped-throw "Uncaught ..." reaches the sink as the * structured message's text (SC4013); the * poisoned library aborts every later entry * deterministically * K8 ambient-audit no undefined refs to sigaction/signal/ * pthread_create/atexit anywhere in the archive * K9 refusals SC4002/SC4003/SC4004/SC4005/SC4007 with the * profile-teaching rider (SC4001 has its own * suite in library-profile.test.ts) * K10 sanitized-lane K4/K5/K7 re-run under ASan + the RC audit * (arming the per-session zero-live-heap seam) * K11 trap-teaching the structured sink-message encoding: a * host-contract trap arrives as 0x01 text 0x1F * code 0x1F symbol 0x1F remediation (exact byte * layout through the spec's parse rule), an * 0x01-led throw rides the sink verbatim (no * "Uncaught " prefix), and the human text always * LEADS the buffer with a printable byte — the * plain-text-degradation pin the marker's * unambiguity rests on * K12 runtime-trap family runtime-DETECTED traps arrive structured * unconditionally: a teachings-declared profile * overlays its text/remediation for the trap * kind's code (SC4014 here); an undeclared code * keeps the baseline human line as field 0 and * carries NO remediation field (fields=3) — the * no-teachings default is pinned by K5/K7 over * the teachings-free traps profile * K13 npm-posture bare npm specifiers are static-or-refuse: an * eligible package (own .d.ts, unminified JS, * no transform markers) compiles statically * into the library graph, its own npm deps * judged by the same bar; every ineligible one * refuses SC4020 naming the failed bar — never * a generic SC1010/SC2013 — and builtins keep * the SC4005 async_free story * K14 determinism-fences the ask-5 deny-by-manifest-id surface: a * reached fenced static surface refuses SC4008 * (id + name + attributed teaching note, both * selector forms), unreached fences emit * byte-identical code to a fence-free build, * the generalized teachings map decorates any * refusal by code or manifest id, and fencing * an already-refusing surface only adds the * note (its own code survives) */ import { execFileSync, spawnSync } from "node:child_process"; import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; import { compileLibrary } from "@scriptc/compiler"; const repoRoot = join(import.meta.dirname, "../.."); const fixtureRoot = join(repoRoot, "tests/library-mode"); const platformTest = process.env["SCRIPTC_PORTABLE_ONLY"] === "1" ? test.skip : test; /* The plain and SCRIPTC_SAN=1 suites may run concurrently by design (the * suite lock is per flavor) and this suite runs the same ordinary builds in * both, so the cache path carries the SUITE flavor — otherwise the two runs * race ar/ranlib in shared build dirs. The per-build `-san` tag below is a * different axis: it distinguishes explicitly-sanitized BUILDS within one * suite (K10). */ const flavor = process.env["SCRIPTC_SAN"] === "1" ? "san" : "plain"; const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests/library-mode", flavor); type Emission = "llvm"; const EMISSIONS: Emission[] = ["llvm"]; interface BuildOpts { sanitize?: boolean; /** Patch a declared result-reset symbol into the profile (K3's second * arena posture). */ declaredReset?: string; } /** Build one fixture's library archive for one emission: the fixture's * profile.json is patched (emission flipped, entry made absolute, posture * overridden when asked) into the build dir, then compiled through the * real compileLibrary pipeline. */ async function buildLibrary( fixture: string, emission: Emission, opts: BuildOpts = {}, ): Promise<{ archive: string; outDir: string }> { const dir = join(fixtureRoot, fixture); const tag = `${fixture}-${emission}${opts.sanitize ? "-san" : ""}${opts.declaredReset !== undefined ? "-reset" : ""}`; const outDir = join(cacheDir, tag); mkdirSync(outDir, { recursive: true }); const profile = JSON.parse(readFileSync(join(dir, "profile.json"), "utf8")) as { entry: string; emission: string; abi: { result_reset_symbol: string | null }; }; profile.emission = emission; profile.entry = join(dir, profile.entry); if (opts.declaredReset !== undefined) profile.abi.result_reset_symbol = opts.declaredReset; const profilePath = join(outDir, "profile.json"); writeFileSync(profilePath, JSON.stringify(profile, null, 2)); const result = await compileLibrary({ profilePath, outDir, sanitize: opts.sanitize ?? false, }); if (!result.ok) { throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); } expect(result.backend).toBe(emission); return { archive: result.archivePath, outDir }; } function buildProbe( fixture: string, archive: string, outDir: string, opts: { sanitize?: boolean; defines?: string[] } = {}, ): string { const bin = join(outDir, "probe"); execFileSync("clang", [ "-std=c11", ...(opts.sanitize ? ["-fsanitize=address"] : []), ...(opts.defines ?? []).map((d) => `-D${d}`), join(fixtureRoot, fixture, "probe.c"), archive, "-lm", "-o", bin, ]); return bin; } function runProbe(bin: string, args: string[] = []): { stdout: string; status: number | null; signal: string | null } { const r = spawnSync(bin, args, { encoding: "utf8", timeout: 60_000 }); return { stdout: r.stdout ?? "", status: r.status, signal: r.signal }; } /** nm over the archive: [definedExternal, undefined] symbol sets, macOS/ * Linux leading-underscore normalized away. */ function nmSymbols(archive: string): { defined: Set; undef: Set } { const parse = (out: string): Set => { const set = new Set(); for (const line of out.split("\n")) { const sym = line.trim().split(/\s+/).pop(); if (sym === undefined || sym === "" || sym.endsWith(":")) continue; set.add(sym.replace(/^_/, "")); } return set; }; const defined = parse(execFileSync("nm", ["-gU", archive], { encoding: "utf8" })); const undef = parse(execFileSync("nm", ["-u", archive], { encoding: "utf8" })); return { defined, undef }; } /* ── K2 + K1 + K8: scalars ─────────────────────────────────────────────── */ const SCALARS_EXPECTED = `scalars ready add: 0.30000000000000004 max-safe exact: 1 nan passthrough: 1 neg zero sign: 1 is_nan(NaN): 1 is_nan(1): 0 invert(0): 1 invert(7): 0 plumb: 40000254995 `; const SCALARS_SYMBOLS = [ "kt_add", "kt_passthrough", "kt_neg_zero", "kt_is_nan", "kt_invert", "kt_plumb", "kt_init", "kt_set_panic_sink", "kt_collect", ]; describe.each(EMISSIONS)("library mode, %s emission", (emission) => { test("K1/K2/K8: scalar round-trips, symbol exactness, ambient audit", async () => { const { archive, outDir } = await buildLibrary("scalars", emission); // K2: the scripted call sequence. const probe = buildProbe("scalars", archive, outDir); const run = runProbe(probe); expect(run.signal).toBeNull(); expect(run.status).toBe(0); expect(run.stdout).toBe(SCALARS_EXPECTED); // K1: prefix-carrying external definitions equal the declared set // exactly, both directions; no prefix-carrying undefineds. const { defined, undef } = nmSymbols(archive); const prefixDefined = [...defined].filter((s) => s.startsWith("kt_")).sort(); expect(prefixDefined).toEqual([...SCALARS_SYMBOLS].sort()); expect([...undef].filter((s) => s.startsWith("kt_"))).toEqual([]); // K8: the mechanical ambient audit — the archive references none of // the process-disposition or threading surface, and registers no // atexit handlers (library teardown is the reset registry). for (const banned of ["sigaction", "signal", "pthread_create", "atexit", "setvbuf"]) { expect(undef.has(banned), `undefined reference to ${banned}`).toBe(false); } // The structural async_free consequence: no fiber/event-loop/timer/ // child-process symbol is DEFINED in the archive (scr_async.c and // scr_child.c never joined the link) and none is REFERENCED by a // linked unit (a missed inter-unit reference would surface here, the // backstop the design asks for instead of a hand-maintained map). const loopish = /^scr_(loop|fiber|on_fiber|timer|set_timeout|set_interval|set_immediate|next_tick|child|spawn)/; expect([...defined].filter((s) => loopish.test(s))).toEqual([]); expect([...undef].filter((s) => loopish.test(s))).toEqual([]); }); /* ── K12: the npm posture's compile side ─────────────────────────────── */ test("K12: an eligible npm package (and its own dep) compiles statically", async () => { const { archive, outDir } = await buildLibrary("npm-static", emission); const probe = buildProbe("npm-static", archive, outDir); const run = runProbe(probe); expect(run.signal).toBeNull(); expect(run.status).toBe(0); // scaled: mathkit.scale (which itself calls mathdep.twice) + OFFSET — // values computed by the packages' shipped JS, statically compiled; // tail: the node:path builtin riding the same graph (async_free, so // SC4005 has nothing to say). expect(run.stdout).toBe( `npm-static ready scaled: 37 tail: c.txt (len 5) `, ); // K1 discipline holds with npm code in the graph: prefix-carrying // definitions are exactly the declared set, no prefix undefineds. const { defined, undef } = nmSymbols(archive); expect([...defined].filter((s) => s.startsWith("kn_")).sort()).toEqual([ "kn_collect", "kn_init", "kn_scaled", "kn_set_panic_sink", "kn_tail", ]); expect([...undef].filter((s) => s.startsWith("kn_"))).toEqual([]); }); /* ── K3: buffers, both arena postures over one library ──────────────────── */ const BUFFERS_EXPECTED = `buffers ready shout: ABC! (len 4, nul 1) both live: ABC! / a-b-c strlen empty (NULL, 0): 0 strlen utf8: 4 wrap: len 4 bytes 60 1 2 62 wrap empty: len 2 bytes 60 62 `; test("K3: buffer round-trips + lifetime, auto-reset posture", async () => { const { archive, outDir } = await buildLibrary("buffers", emission); const probe = buildProbe("buffers", archive, outDir); const run = runProbe(probe); expect(run.signal).toBeNull(); expect(run.status).toBe(0); expect(run.stdout).toBe(BUFFERS_EXPECTED); }); test("K3: buffer lifetime, declared-reset posture (results accumulate)", async () => { const { archive, outDir } = await buildLibrary("buffers", emission, { declaredReset: "kb_reset" }); const probe = buildProbe("buffers", archive, outDir, { defines: ["DECLARED_RESET"] }); const run = runProbe(probe); expect(run.signal).toBeNull(); expect(run.status).toBe(0); expect(run.stdout).toBe(BUFFERS_EXPECTED); }); /* ── K4: init re-run determinism ─────────────────────────────────────── */ const SESSION = `session start counter=0 bump: 1 2 note: 1 2 indexed: 72100 recall: a,b `; test("K4: three init sessions are byte-identical", async () => { const { archive, outDir } = await buildLibrary("reinit", emission); const probe = buildProbe("reinit", archive, outDir); const run = runProbe(probe); expect(run.signal).toBeNull(); expect(run.status).toBe(0); expect(run.stdout).toBe(SESSION + SESSION + SESSION); }); /* ── K5/K6/K7: the trap channel ──────────────────────────────────────── */ test("K5: a trap delivers to the sink exactly once, host survives", async () => { const { archive, outDir } = await buildLibrary("traps", emission); const probe = buildProbe("traps", archive, outDir); const run = runProbe(probe, ["trap"]); expect(run.signal).toBeNull(); expect(run.status).toBe(0); // The runtime-detected trap arrives structured (K12's no-teachings // default): field 0 is the runtime's own line unchanged (its newline // intact), the code names the range kind, the symbol names the export // the host called, and no remediation field exists on this // teachings-free profile. expect(run.stdout).toBe( `traps ready sink[1]: text=[scriptc: RangeError: array index 9 out of bounds (length 3) ] code=[SC4014] symbol=[kp_boom] fields=3 text_printable=1 addr: nonzero survived, sink_calls=1 `, ); }); test("K5: a trap during init routes to the sink the same way", async () => { const { archive, outDir } = await buildLibrary("init-trap", emission); const probe = buildProbe("init-trap", archive, outDir); const run = runProbe(probe); expect(run.signal).toBeNull(); expect(run.status).toBe(0); // Init counts as an entry: the structured message names the init // symbol itself as the trapping entry. expect(run.stdout).toBe( `about to trap sink[1]: text=[scriptc: RangeError: array index 7 out of bounds (length 3) ] code=[SC4014] symbol=[ki_init] fields=3 text_printable=1 survived init trap, sink_calls=1 `, ); }); test("K7: an escaped throw reaches the sink as 'Uncaught ...'", async () => { const { archive, outDir } = await buildLibrary("traps", emission); const probe = buildProbe("traps", archive, outDir); const run = runProbe(probe, ["throw"]); expect(run.signal).toBeNull(); expect(run.status).toBe(0); // The escaped-exception renderer's "Uncaught ..." line is the // structured message's text; the code names the escaped-exception // kind and the symbol the entry the throw escaped through. expect(run.stdout).toBe( `traps ready sink[1]: text=[Uncaught Error: kaput ] code=[SC4013] symbol=[kp_fail] fields=3 text_printable=1 addr: nonzero survived, sink_calls=1 `, ); }); test("K7: the poisoned library aborts every later entry", async () => { const { archive, outDir } = await buildLibrary("traps", emission); const probe = buildProbe("traps", archive, outDir); const run = runProbe(probe, ["poisoned"]); expect(run.signal).toBe("SIGABRT"); expect(run.stdout).toContain("poisoned now"); expect(run.stdout).not.toContain("UNREACHABLE"); }); test("K6: a trap before sink registration aborts", async () => { const { archive, outDir } = await buildLibrary("traps", emission); const probe = buildProbe("traps", archive, outDir); const run = runProbe(probe, ["preregister"]); expect(run.signal).toBe("SIGABRT"); expect(run.stdout).not.toContain("UNREACHABLE"); }); /* ── K11: the structured trap-teaching encoding ──────────────────────── */ test("K11: a host-contract trap arrives structured, exact byte layout", async () => { const { archive, outDir } = await buildLibrary("teach", emission); const probe = buildProbe("teach", archive, outDir); const run = runProbe(probe, ["structured"]); expect(run.signal).toBeNull(); expect(run.status).toBe(0); // The probe implements the spec's parse rule: byte 0 is the 0x01 // marker, the bytes after it split on 0x1F into exactly four fields — // the profile's teaching text (its authored newline intact), the // compiler-threaded code and trapping symbol, and the profile's // remediation. fields=4 pins that the emitter appends nothing past // the remediation. expect(run.stdout).toBe( `teach ready sink[1]: text=[inbound bytes length does not fit the marshalling class — the host and this library disagree about the call contract ] code=[SC4012] symbol=[kv_wrap] remediation=[pass the buffer's true byte length; lengths must stay below 2^53] fields=4 text_printable=1 survived, sink_calls=1 `, ); }); test("K11: an 0x01-led thrown Error message rides the sink verbatim", async () => { const { archive, outDir } = await buildLibrary("teach", emission); const probe = buildProbe("teach", archive, outDir); const run = runProbe(probe, ["verbatim"]); expect(run.signal).toBeNull(); expect(run.status).toBe(0); // The facade-authored structured teaching (embedder-prefixed NS code) // arrives byte-for-byte: no "Uncaught " prefix ahead of the marker, // no added trailing newline inside the remediation. expect(run.stdout).toBe( `teach ready sink[1]: text=[tag 99 does not name a bare message arm of this core] code=[NS1207] symbol=[kv_dispatch] remediation=[rebuild the app so the compiled core and the host shim come from one build] fields=4 text_printable=1 survived, sink_calls=1 `, ); }); test("K11: an 0x01-led thrown string rides the sink verbatim", async () => { const { archive, outDir } = await buildLibrary("teach", emission); const probe = buildProbe("teach", archive, outDir); const run = runProbe(probe, ["verbatim-str"]); expect(run.signal).toBeNull(); expect(run.status).toBe(0); // Three fields, no remediation: the whole fourth field is absent, so // the parser counts exactly three. expect(run.stdout).toBe( `teach ready sink[1]: text=[string-thrown teaching] code=[NS0002] symbol=[kv_teach_str] fields=3 text_printable=1 survived, sink_calls=1 `, ); }); /* ── K12: the runtime-detected trap family arrives structured ────────── */ test("K12: a runtime trap overlays the profile's teaching and remediation", async () => { const { archive, outDir } = await buildLibrary("teach", emission); const probe = buildProbe("teach", archive, outDir); // The profile declares SC4014 (range) teaching + remediation: the // funnel-assembled message carries the overlay text as field 0 (its // authored newline intact), the range code, the trapping export's // symbol, and the remediation as the fourth field. text_printable=1 // is the plain-text-degradation pin: the human text still LEADS the // buffer with a printable byte. const run = runProbe(probe, ["runtime-trap"]); expect(run.signal).toBeNull(); expect(run.status).toBe(0); expect(run.stdout).toBe( `teach ready sink[1]: text=[this core read past the end of a collection — an index the embedding fed it does not exist ] code=[SC4014] symbol=[kv_boom_runtime] remediation=[clamp indices to the collection's length before calling into the core] fields=4 text_printable=1 survived, sink_calls=1 `, ); }); test("K12: an undeclared code keeps the default text and no remediation", async () => { const { archive, outDir } = await buildLibrary("teach", emission); const probe = buildProbe("teach", archive, outDir); // Nothing is declared for the escaped-exception code (SC4013): field 0 // is the baseline "Uncaught ..." line unchanged and the whole // remediation field is absent — exactly the spec's worked example 2. const run = runProbe(probe, ["runtime-throw"]); expect(run.signal).toBeNull(); expect(run.status).toBe(0); expect(run.stdout).toBe( `teach ready sink[1]: text=[Uncaught Error: kaput ] code=[SC4013] symbol=[kv_fail_runtime] fields=3 text_printable=1 survived, sink_calls=1 `, ); }); /* ── K10: the sanitized lane (ASan + the RC audit's re-init seam) ────── */ platformTest("K10: K4 under ASan + RC audit (zero live heap across re-init)", async () => { const { archive, outDir } = await buildLibrary("reinit", emission, { sanitize: true }); const probe = buildProbe("reinit", archive, outDir, { sanitize: true }); const run = runProbe(probe); expect(run.signal).toBeNull(); expect(run.status).toBe(0); expect(run.stdout).toBe(SESSION + SESSION + SESSION); }); platformTest("K10: K5/K7 under ASan", async () => { const { archive, outDir } = await buildLibrary("traps", emission, { sanitize: true }); const probe = buildProbe("traps", archive, outDir, { sanitize: true }); const trap = runProbe(probe, ["trap"]); expect(trap.status).toBe(0); expect(trap.stdout).toContain("survived, sink_calls=1"); const thrown = runProbe(probe, ["throw"]); expect(thrown.status).toBe(0); expect(thrown.stdout).toContain("text=[Uncaught Error: kaput"); }); }); /* ── K9: the SC4xxx refusal family, end to end through compileLibrary ─────── */ let refusalCounter = 0; async function refusal( source: string, profilePatch: Record, emission: Emission = "llvm", ): Promise<{ code: string; message: string; hint?: string; note?: string; file: string }[]> { const outDir = join(cacheDir, `refusal-${refusalCounter++}`); mkdirSync(outDir, { recursive: true }); const entry = join(outDir, "lib.ts"); writeFileSync(entry, source); const profile = { profile_format: 1, name: "refusal-fixture", entry, emission, abi: { prefix: "kx_", init_symbol: "kx_init", sink_register_symbol: "kx_set_panic_sink", collect_symbol: null, result_reset_symbol: null, }, exports: [] as unknown[], ...profilePatch, }; const profilePath = join(outDir, "profile.json"); writeFileSync(profilePath, JSON.stringify(profile)); const result = await compileLibrary({ profilePath, outDir }); expect(result.ok).toBe(false); if (result.ok) throw new Error("unreachable"); return result.diagnostics.map((d) => ({ code: d.code, message: d.message, ...(d.hint !== undefined ? { hint: d.hint } : {}), ...(d.note !== undefined ? { note: d.note } : {}), file: d.loc.file, })); } async function acceptance( source: string, profilePatch: Record, emission: Emission = "llvm", ): Promise { const outDir = join(cacheDir, `acceptance-${refusalCounter++}`); mkdirSync(outDir, { recursive: true }); const entry = join(outDir, "lib.ts"); writeFileSync(entry, source); const profilePath = join(outDir, "profile.json"); writeFileSync( profilePath, JSON.stringify({ profile_format: 1, name: "acceptance-fixture", entry, emission, abi: { prefix: "kx_", init_symbol: "kx_init", sink_register_symbol: "kx_set_panic_sink", collect_symbol: null, result_reset_symbol: null, }, exports: [], ...profilePatch, }), ); const result = await compileLibrary({ profilePath, outDir }); expect(result.ok, result.ok ? undefined : result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")).toBe(true); } describe("K9: library-mode refusals", () => { test("SC4002: unmapped export name", async () => { const diags = await refusal(`export function real(): number { return 1; }\n`, { exports: [{ export: "nope", symbol: "kx_nope", params: [], returns: "f64" }], }); expect(diags[0]!.code).toBe("SC4002"); expect(diags[0]!.message).toContain("'nope'"); expect(diags[0]!.hint).toContain("facade"); }); test("SC4003: a parameter outside the marshalling classes", async () => { const diags = await refusal( `export function take(r: { a: number }): number { return r.a; }\n`, { exports: [{ export: "take", symbol: "kx_take", params: ["f64"], returns: "f64" }] }, ); expect(diags[0]!.code).toBe("SC4003"); expect(diags[0]!.message).toContain("parameter 1"); expect(diags[0]!.hint).toContain("asks 2/3"); }); test("SC4003: arity mismatch between profile and signature", async () => { const diags = await refusal(`export function two(a: number, b: number): number { return a + b; }\n`, { exports: [{ export: "two", symbol: "kx_two", params: ["f64"], returns: "f64" }], }); expect(diags[0]!.code).toBe("SC4003"); expect(diags[0]!.message).toContain("2 parameter(s)"); }); test("SC4004: a mapped async export, with the profile teaching as the attributed note", async () => { const diags = await refusal(`export async function tick(): Promise { return 1; }\n`, { exports: [{ export: "tick", symbol: "kx_tick", params: [], returns: "f64" }], determinism: { teachings: { async: "use the host scheduler entry instead" } }, }); expect(diags[0]!.code).toBe("SC4004"); expect(diags[0]!.hint).toContain("synchronous facade"); // The teaching arrives as its own visibly-attributed note — the // tool's hint stays the tool's. expect(diags[0]!.note).toBe("from the 'refusal-fixture' profile: use the host scheduler entry instead"); }); test("SC4005: a timer anywhere in the graph, teaching as the attributed note", async () => { const diags = await refusal(`setTimeout(() => {}, 1);\nexport function f(): number { return 1; }\n`, { exports: [{ export: "f", symbol: "kx_f", params: [], returns: "f64" }], determinism: { teachings: { SC4005: "schedule through the embedder frame loop" } }, }); expect(diags[0]!.code).toBe("SC4005"); expect(diags[0]!.message).toContain("timers"); expect(diags[0]!.note).toBe("from the 'refusal-fixture' profile: schedule through the embedder frame loop"); }); test("SC4007: a generic export", async () => { const diags = await refusal( `export function id(x: T): T { return x; }\nconsole.log(id(1));\n`, { exports: [{ export: "id", symbol: "kx_id", params: ["f64"], returns: "f64" }] }, ); expect(diags[0]!.code).toBe("SC4007"); expect(diags[0]!.hint).toContain("concrete instantiation"); }); }); /* ── K12: npm static-or-refuse, the refusal side ───────────────────────── * The entries live in the npm-refuse fixture dir (its vendored * node_modules is what the specifiers resolve against); the profile is * synthesized per test like K9's. Emission never engages — SC4020 is a * frontend verdict — so one lane suffices. */ let npmRefusalCounter = 0; async function npmRefusal( entryFile: string, exports: unknown[], profilePatch: Record = {}, ): Promise<{ code: string; message: string; hint?: string; note?: string; file: string }[]> { const outDir = join(cacheDir, `npm-refusal-${npmRefusalCounter++}`); mkdirSync(outDir, { recursive: true }); const profile = { profile_format: 1, name: "npm-refusal-fixture", entry: join(fixtureRoot, "npm-refuse", entryFile), emission: "llvm", abi: { prefix: "kx_", init_symbol: "kx_init", sink_register_symbol: "kx_set_panic_sink", collect_symbol: null, result_reset_symbol: null, }, exports, ...profilePatch, }; const profilePath = join(outDir, "profile.json"); writeFileSync(profilePath, JSON.stringify(profile)); const result = await compileLibrary({ profilePath, outDir }); expect(result.ok).toBe(false); if (result.ok) throw new Error("unreachable"); return result.diagnostics.map((d) => ({ code: d.code, message: d.message, ...(d.hint !== undefined ? { hint: d.hint } : {}), ...(d.note !== undefined ? { note: d.note } : {}), file: d.loc.file, })); } describe("K13: library-mode npm refusals (SC4020, static-or-refuse)", () => { test("build-transform markers refuse with the failed bar named", async () => { const diags = await npmRefusal("lib-markers.ts", [{ export: "f", symbol: "kx_f", params: [], returns: "string" }]); expect(diags.map((d) => d.code)).toEqual(["SC4020"]); expect(diags[0]!.message).toContain("'webbundle'"); expect(diags[0]!.message).toContain("build-transform markers"); expect(diags[0]!.hint).toContain("vendor"); // Anchored at the import site in the entry, not a whole-file blur. expect(diags[0]!.file.endsWith("lib-markers.ts")).toBe(true); }); test("minified shipped JS refuses", async () => { const diags = await npmRefusal("lib-minified.ts", [ { export: "f", symbol: "kx_f", params: ["f64", "f64"], returns: "f64" }, ]); expect(diags.map((d) => d.code)).toEqual(["SC4020"]); expect(diags[0]!.message).toContain("'slimmed'"); expect(diags[0]!.message).toContain("looks minified"); }); test("no own .d.ts refuses by name — never the generic import fence", async () => { const diags = await npmRefusal("lib-untyped.ts", [ { export: "f", symbol: "kx_f", params: ["f64", "f64"], returns: "f64" }, ]); // The package resolves at runtime (Node would load it) but ships no // types: the library answer is the bar's, not SC1010's "nothing // installed resolves it" nor SC2013's --dynamic teaching. expect(diags.map((d) => d.code)).toEqual(["SC4020"]); expect(diags[0]!.message).toContain("'untyped'"); expect(diags[0]!.message).toContain("no own .d.ts"); }); test("the profile teaching rides SC4020 as the attributed note", async () => { const diags = await npmRefusal( "lib-markers.ts", [{ export: "f", symbol: "kx_f", params: [], returns: "string" }], { determinism: { teachings: { SC4020: "request the vendored copy from the embedder SDK" } } }, ); expect(diags[0]!.code).toBe("SC4020"); expect(diags[0]!.note).toBe("from the 'npm-refusal-fixture' profile: request the vendored copy from the embedder SDK"); }); test("a builtin pulling the event loop keeps the SC4005 story, not a new fence", async () => { const diags = await refusal( `import { setTimeout as later } from "node:timers";\nexport function f(): number { later(() => {}, 1); return 1; }\n`, { exports: [{ export: "f", symbol: "kx_f", params: [], returns: "f64" }] }, ); expect(diags[0]!.code).toBe("SC4005"); expect(diags[0]!.message).toContain("timers"); }); }); /* ── K14: the ask-5 determinism fences (spec §1–§2) ─────────────────────── * The profile's `determinism.fences` deny surfaces by surface-manifest id * at compile time. Reached STATIC surfaces refuse SC4008 with the fence's * teaching as the attributed note; fenced surfaces the static tier * refuses anyway keep their own code with the teaching riding it; and the * generalized `teachings` map attaches text to any refusal by code or * manifest id. Both emissions run the refusal cases too — the refusal is * pre-emission, but the profile pins the emission and conformance asserts * the answer is emission-invariant. */ const WORKED_FENCES = [ { id: "stdlib.math.random", teaching: "randomness is an effect: request it from the host and receive it as a Msg." }, { prefix: "node-builtin.fs.", teaching: "files are effects: declare reads and writes as commands the host executes." }, { prefix: "node-builtin.net.", teaching: "network is an effect: declare requests as commands; responses arrive as Msgs." }, { prefix: "node-builtin.os.", teaching: "machine identity is an effect: ask the host." }, { prefix: "node-builtin.crypto.", teaching: "randomness and digests come from the host." }, // The remaining ambient families the determinism attestation demotes on // — with these, the fence set covers every attestation-known surface // (the ask-5 §4 invariant's full-fence profile). { prefix: "stdlib.date.", teaching: "time is an effect: the host passes the clock in as a Msg." }, { prefix: "node-builtin.process.", teaching: "process ambient state and authority belong to the host." }, { prefix: "node-builtin.perf_hooks.", teaching: "the monotonic clock is the host's." }, ]; describe.each(EMISSIONS)("K14: determinism fences, %s emission", (emission) => { test("a reached id-fenced surface refuses SC4008 with code, id, and attributed teaching", async () => { const diags = await refusal( `export function roll(): number { return Math.random(); }\n`, { exports: [{ export: "roll", symbol: "kx_roll", params: [], returns: "f64" }], determinism: { fences: WORKED_FENCES }, }, emission, ); expect(diags.map((d) => d.code)).toEqual(["SC4008"]); expect(diags[0]!.message).toContain("'stdlib.math.random'"); expect(diags[0]!.message).toContain("Math.random"); expect(diags[0]!.note).toBe( "from the 'refusal-fixture' profile: randomness is an effect: request it from the host and receive it as a Msg.", ); // Anchored at the reaching construct, not the profile file. expect(diags[0]!.file.endsWith("lib.ts")).toBe(true); }); test("Date.now under the full-fence profile refuses SC4008 naming the Date entry", async () => { // The §4 invariant's teeth: before the Date family had manifest // entries, Date.now() compiled under every declarable fence set and // attested `deterministic: false` post hoc — now the full-fence // profile refuses it at compile time. const diags = await refusal( `export function stamp(): number { return Date.now(); }\n`, { exports: [{ export: "stamp", symbol: "kx_stamp", params: [], returns: "f64" }], determinism: { fences: WORKED_FENCES }, }, emission, ); expect(diags.map((d) => d.code)).toEqual(["SC4008"]); expect(diags[0]!.message).toContain("'stdlib.date.now'"); expect(diags[0]!.message).toContain("Date.now"); expect(diags[0]!.note).toContain("time is an effect"); expect(diags[0]!.file.endsWith("lib.ts")).toBe(true); }); test("Date.parse fence denies the call but permits date construction and getTime", async () => { const profile = { exports: [{ export: "stamp", symbol: "kx_stamp", params: [], returns: "f64" }], determinism: { fences: [{ id: "stdlib.date.parse" }] }, }; await acceptance( `export function stamp(): number { return new Date("2026-07-17").getTime(); }\n`, profile, emission, ); const diags = await refusal( `export function stamp(): number { return Date.parse("2026-07-17"); }\n`, profile, emission, ); expect(diags.map((d) => d.code)).toEqual(["SC4008"]); expect(diags[0]!.message).toContain("'stdlib.date.parse'"); }); test("an exact Date.valueOf fence does not also fence getTime", async () => { const exportProfile = { exports: [{ export: "read", symbol: "kx_read", params: [], returns: "f64" }], determinism: { fences: [{ id: "stdlib.date.valueOf" }] }, }; await acceptance( `export function read(): number { return new Date(0).getTime(); }\n`, exportProfile, emission, ); const diags = await refusal( `export function read(): number { return new Date(0).valueOf(); }\n`, exportProfile, emission, ); expect(diags.map((d) => d.code)).toEqual(["SC4008"]); expect(diags[0]!.message).toContain("'stdlib.date.valueOf'"); }); test("a process.env read under the full-fence profile refuses SC4008 naming the process entry", async () => { const diags = await refusal( `export function home(): number { return (process.env["HOME"] ?? "").length; }\n`, { exports: [{ export: "home", symbol: "kx_home", params: [], returns: "f64" }], determinism: { fences: WORKED_FENCES }, }, emission, ); expect(diags.map((d) => d.code)).toEqual(["SC4008"]); expect(diags[0]!.message).toContain("'node-builtin.process.env'"); expect(diags[0]!.note).toContain("process ambient state"); }); test("a live machine-state read under the full-fence profile refuses SC4008 naming its entry", async () => { // process.stdout.isTTY joined the attestation's demote set as a // correction (a live terminal-attachment read); the family fence must // deny it or the §4 invariant would leak through it. const diags = await refusal( `export function tty(): number { return process.stdout.isTTY ? 1 : 0; }\n`, { exports: [{ export: "tty", symbol: "kx_tty", params: [], returns: "f64" }], determinism: { fences: WORKED_FENCES }, }, emission, ); expect(diags.map((d) => d.code)).toEqual(["SC4008"]); expect(diags[0]!.message).toContain("'node-builtin.process.isTTY'"); }); test("a reached prefix-fenced surface refuses SC4008 naming the covered member id", async () => { const diags = await refusal( `import { readFileSync } from "node:fs";\nexport function f(): number { return readFileSync("x", "utf8").length; }\n`, { exports: [{ export: "f", symbol: "kx_f", params: [], returns: "f64" }], determinism: { fences: WORKED_FENCES }, }, emission, ); expect(diags.map((d) => d.code)).toEqual(["SC4008"]); expect(diags[0]!.message).toContain("'node-builtin.fs.readFileSync'"); expect(diags[0]!.note).toContain("files are effects"); }); test("unreached fences compile at zero cost: the emitted code is byte-identical", async () => { // ONE entry file feeds both builds (emitted LLVM embeds the entry's // path in provenance comments; the fence's cost is what's measured, // not the build directory's spelling). const entryDir = join(cacheDir, `fence-zerocost-${emission}`); mkdirSync(entryDir, { recursive: true }); const entry = join(entryDir, "lib.ts"); writeFileSync(entry, `export function f(): number { return 41 + 1; }\n`); const emitted: string[] = []; for (const fenced of [false, true]) { const outDir = join(cacheDir, `fence-zerocost-${emission}-${fenced ? "fenced" : "plain"}`); mkdirSync(outDir, { recursive: true }); const profilePath = join(outDir, "profile.json"); writeFileSync( profilePath, JSON.stringify({ profile_format: 1, name: "fence-zerocost", entry, emission, abi: { prefix: "kx_", init_symbol: "kx_init", sink_register_symbol: "kx_set_panic_sink", collect_symbol: null, result_reset_symbol: null, }, exports: [{ export: "f", symbol: "kx_f", params: [], returns: "f64" }], ...(fenced ? { determinism: { fences: WORKED_FENCES } } : {}), }), ); const result = await compileLibrary({ profilePath, outDir }); if (!result.ok) throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); emitted.push(readFileSync(result.llvmPath, "utf8")); } expect(emitted[1]).toBe(emitted[0]); }); test("a code-keyed teachings entry rides a non-fence refusal as the attributed note", async () => { const diags = await refusal( `export function f(): number { return (1234.5).toLocaleString().length; }\n`, { exports: [{ export: "f", symbol: "kx_f", params: [], returns: "f64" }], determinism: { teachings: { SC2020: "locale comes from the host; never infer it in a core" } }, }, emission, ); expect(diags[0]!.code).toBe("SC2020"); expect(diags[0]!.note).toBe( "from the 'refusal-fixture' profile: locale comes from the host; never infer it in a core", ); }); test("a manifest-id-keyed teachings entry attaches to that surface's own refusal", async () => { const diags = await refusal( `export function f(): number { return (1.234).toPrecision(2).length; }\n`, { exports: [{ export: "f", symbol: "kx_f", params: [], returns: "f64" }], determinism: { teachings: { "stdlib.number.toPrecision": "formatting runs in the host; request it as an effect" } }, }, emission, ); // The surface's own code, not a fence code: the id key attaches text // to the refusal that already fires. expect(diags[0]!.code).toBe("SC2012"); expect(diags[0]!.message).toContain(".toPrecision()"); expect(diags[0]!.note).toBe("from the 'refusal-fixture' profile: formatting runs in the host; request it as an effect"); }); test("fencing a surface the static tier refuses anyway changes only the message", async () => { const diags = await refusal( `export function f(): number { return (1.234).toPrecision(2).length; }\n`, { exports: [{ export: "f", symbol: "kx_f", params: [], returns: "f64" }], determinism: { fences: [{ id: "stdlib.number.toPrecision", teaching: "formatting is host work" }] }, }, emission, ); // The existing refusal's code survives — the fence never re-codes a // surface that already refuses; its teaching rides as the note. expect(diags[0]!.code).toBe("SC2012"); expect(diags[0]!.note).toBe("from the 'refusal-fixture' profile: formatting is host work"); }); });