'use strict'; /** * Emitted-artifact provenance table + totality guard (ADR-2719 §2, issue #2722). * * Maps every EMITTED path (a key in any tests/fixtures/golden-install-parity/*.json * manifest) to the REPO SOURCE path(s) whose change can legitimately explain a * change to it. Phase 3 (#2723) consumes this to turn "these emitted hashes moved" * into "…and nothing in this diff explains them". * * This module resolves provenance ONLY. It never reads a git diff, never builds a * manifest, and never re-derives a byte — ADR-2719 §1 is explicit that this design * constrains which keys may move, rather than asserting emitted == transform(source) * (the tautology ADR-2264's Amendment rejected). * * ── Totality ──────────────────────────────────────────────────────────────── * Every emitted path must match EXACTLY ONE rule. Zero matches, two matches, and * a rule that matches nothing are all hard failures. A hand-maintained table's * characteristic risk is rotting into a silent gap; totality converts that into a * loud one, so a new emitted family fails the build instead of passing through * unattributed. * * ── Derived vs. hard-coded (deliberate split) ─────────────────────────────── * Emitted SHAPES (roots + patterns) are hard-coded on purpose. The guard's whole * value is failing when the installer starts emitting something new; a table that * derived its shapes from the installer could never fail that way — it would follow * the installer anywhere, silently, which is the tautology above rebuilt. * Source PATHS may read a first-party descriptor when the descriptor is the only * declaration of that source (`hostBehaviors.nativePlugin.source`). The emitted dest * stays hard-coded, so a dest change still fails loud. * * ── The trap this table exists to avoid ───────────────────────────────────── * The repo contains `skills/msd-/SKILL.md` (71 dirs) that LOOK like the source * of the emitted `skills/` family. They are not: scripts/gen-plugin-skills.cjs * GENERATES them from commands/msd/*.md, and the installer stages from commands/msd/ * directly (src/install-profiles.cts:637-708). Attributing emitted skills to repo * skills/ would be false attribution that still passes totality — the exact residual * risk ADR-2719 records. The spot-check tests pin this pair. */ const path = require('node:path'); const { cleanup } = require('../helpers.cjs'); const { MANIFEST_FAMILIES, runMinimalInstall, buildParityManifest } = require('./install-shared.cjs'); /** * Number of runtime manifests the guard expects to cover. Asserted, so a glob that * silently matches fewer files can never report a vacuous pass. * * DERIVED, not a literal (#2723). It was `19`, and that same literal was also asserted * against the baseline built at the base ref — two trees that legitimately differ by one * family whenever a PR adds or removes a runtime, which made every such PR unpassable at * any value. Deriving it from the single `MANIFEST_FAMILIES` source keeps the * anti-vacuity property here (this tree's glob must match this tree's registry) while * leaving the cross-tree question to `reconcileFamilies`, which is set-based and * direction-aware. The absolute floor that a shrunken universe cannot satisfy lives with * the derivation as `MINIMUM_MANIFEST_FAMILIES`. */ const EXPECTED_MANIFEST_COUNT = MANIFEST_FAMILIES.length; // ─── Emitted roots ──────────────────────────────────────────────────────────── // Longest-first: `skills/msd` (hermes category dir) must win over `skills` for // `skills/msd/...`, and `.agents/skills` must win over `.agents`. const SKILLS_ROOTS = ['.agents/skills', 'skills']; const HOOKS_ROOTS = ['hooks']; /** Source-of-truth command dir every skill/command surface converts from. */ const COMMANDS_SRC = 'commands/msd'; /** Installer source file that emits the Cline/AGENTS.md instruction bodies as * code literals (buildClineRulesBody / buildClineAgentsMdBody / * buildClinePreToolUseHook). */ const CLINE_BODY_SRC = 'src/runtime-hooks-surface.cts'; /** * Installer source file that GENERATES the Windows-only `hooks/.cmd` shim * wrapping a Codex hook's `.js` script (#3426). Same physical file as * CLINE_BODY_SRC — kept as its own named constant because the two constants * attribute unrelated transform code that happens to live in one file: * buildCodexHookWindowsShimIR / ensureCodexHooksJsonSessionStart / * ensureCodexHooksJsonEvent (verified via Memtrace: these are the ONLY writers * of a `.cmd` file anywhere in the installer — no other runtime's hook surface * builds one). */ const HOOKS_WINDOWS_SHIM_SRC = 'src/runtime-hooks-surface.cts'; /** Installer source file that emits the Hermes skill-category DESCRIPTION.md * (writeHermesCategoryDescription) as a code literal. */ const INSTALLER_SRC = 'bin/install.js'; /** Module owning the #2544 `{"type":"commonjs"}` marker literal and the * write/remove ownership predicate behind it. */ const COMMONJS_MARKER_SRC = 'src/commonjs-marker.cts'; /** Engine module that stages the native plugin adapter and writes the marker * beside it (_installNativePluginIfDeclared). */ const INSTALL_ENGINE_SRC = 'src/install-engine.cts'; /** Source file holding the Kimi root-agent literal (runtime-artifact-layout.cts:303). */ const KIMI_ROOT_AGENT_SRC = 'src/runtime-artifact-layout.cts'; /** * Transform sources for the per-runtime agent-content pipeline (#2757). * * `runtime-artifact-conversion.cts` rewrites frontmatter (quoting, dropping `tools:`/ * `color:`), reformats the tools list, and rewrites the hardcoded `.claude/` self- * reference to each runtime's own home (`applyAgentPathRewrites`, * `normalizeAgentBodyForRuntime`, the `convertClaudeAgentTo*Agent` family). * `install-effort-resolver.cts` (+ the `model-catalog.cts` primitives it calls) * resolves the `reasoning_effort` value that both Claude's `.md` (`effort:` frontmatter, * injected by `injectEffortFrontmatter` in bin/install.js) and Codex's `.toml` * (`model_reasoning_effort`) embed — "the same config-driven precedence chain" per * bin/install.js's own #443 comment. * * Verified empirically (#2757), not assumed: installing every runtime for a sample * agent and diffing the output against the raw repo `.md` (after normalizing the * install-time HOME/version substitutions the golden fixtures already normalize) shows * NO runtime — including Claude — emits a byte-identical copy. Every one rewrites at * least the frontmatter and the `.claude/skills` self-reference; Claude additionally * gets `effort:` injected. This is why `agents-verbatim` below is `derived`, not * `identity`, despite its (retained, historical) id. * * Deliberately excludes `bin/install.js`: that file also implements the final splice * (`injectEffortFrontmatter`, `generateCodexAgentToml`), but at 13k+ lines spanning * hooks, MCP config, uninstall, and every other installer concern, declaring it here * would be the blanket escape hatch ADR-2719 warns against — almost any PR touches SOME * line of it. A change localized to those two functions and not reachable through the * three files below stays unattributable and falls to the drift-ack file, which is the * documented escape hatch for exactly that case. * * ── Known follow-up, NOT included here on purpose (#2757 review) ──────────── * The issue text also named `src/agent-tools-contract.cts` and * `src/agent-install-check.cts` (both touched by PR #2566, "derive Codex agent sandbox * from the tool"). Verified against THIS tree and excluded on the evidence: * - `src/agent-tools-contract.cts` does not exist on `next` — #2566 ADDS it (+119/-0). * A nonexistent path here would immediately fail the * "every declared transform path exists in the repo" hygiene test in * emitted-provenance.test.cjs, which exists precisely to catch a rule (or a * suggested transform, as here) that cites a file that doesn't back real content. * - `src/agent-install-check.cts` exists today and is READ-ONLY (`getAgentsDir`, * `checkAgentsInstalled` — no fs.writeFileSync, no content transform); #2566 * nearly doubles it (+112/-1). Whether the addition becomes a real content-writer, * a larger read-only diagnostic surface, or a helper called BY an already-declared * file cannot be determined without reading #2566's actual diff, which review is * explicit should not be fetched here. Declaring it on a line-count guess risks * the exact false-attribution failure mode this whole table exists to prevent — a * rule that looks fixed while resolving to the wrong causal story. * Interim safety net: if #2566 lands and either file becomes a real transform without * this list being updated, the differential (Phase 3) will correctly flag the moved * agent artifact as unattributable — that is the guard working, not a regression — and * unblocks via `tests/emitted-drift-ack.json` until this list is verified and extended * using the SAME method used for the three files above: build, install every runtime, * diff the output against the raw repo source, confirm which file's absence/presence * changes the bytes. */ const AGENT_TRANSFORM_SRCS = [ 'src/runtime-artifact-conversion.cts', 'src/install-effort-resolver.cts', 'src/model-catalog.cts', // #4770: the Codex .toml family's sandbox_mode is derived through // src/codex-agent-toml.cts (deriveCodexSandboxMode — the single owner of the // derivation), so a change there moves every emitted agents/*.toml without // touching any agents/*.md source. 'src/codex-agent-toml.cts', ]; // #3738: antigravity's global skills pass through the antigravity converter // (src/runtime-artifact-conversion.cts, mirrored hand-authored in bin/install.js // per ADR-1508), whose rewrites — e.g. ~/.claude/skills/ → ~/.gemini/config/skills/ // — can move emitted bytes with NO commands/msd source changing. Declaring the // transform here attributes that ripple class permanently, the same way // AGENT_TRANSFORM_SRCS does for agents; scoped to runtime 'antigravity' so a // converter change never blankets the other skills runtimes' attribution. const ANTIGRAVITY_SKILL_TRANSFORM_SRCS = [ 'src/runtime-artifact-conversion.cts', 'bin/install.js', ]; // #4002: zcode's command AND skill bodies flow through `_applyRuntimeRewrites` // (converter: null — the rewrite pass is their only path-rewriting step), so a // converter change moves emitted bytes with no commands/msd source changing. // Same permanent-attribution shape as ANTIGRAVITY_SKILL_TRANSFORM_SRCS (#3738), // scoped to runtime 'zcode' for the same reason. const ZCODE_BODY_TRANSFORM_SRCS = [ 'src/runtime-artifact-conversion.cts', 'bin/install.js', ]; // #4482: every non-Copilot runtime filters audience-specific notes through the // conversion module and the published installer copy path. const RUNTIME_NOTE_FILTER_TRANSFORM_SRCS = [ 'src/runtime-artifact-conversion.cts', 'bin/install.js', ]; const RUNTIME_NOTE_FILTERED_FAMILIES = new Set( MANIFEST_FAMILIES.map(({ name }) => name), ); /** * A `sources` entry ending in `/` is a PREFIX, not a file: it means "any repo path * under this directory legitimately explains this emitted path". Used where an * emitted artifact aggregates a whole directory (a root agent enumerating every * staged agent). Phase 3 must honor the trailing slash when testing a changed-path * set against these sources; a plain string is an exact path. */ const SOURCE_PREFIX_SUFFIX = '/'; /** * Strip the runtime skill prefix from a staged skill directory name. * Router/flat skill dirs are ``; nested CHILD dirs are the bare * stem (src/install-profiles.cts:696 joins `stem`, not `prefix + stem`). */ function stripSkillPrefix(dirName) { return dirName.startsWith('msd-') ? dirName.slice(4) : dirName; } /** * Resolve a runtime's declared native plugin/extension source from the compiled * capability registry — the only place that mapping is declared. * Returns null when the runtime declares none. */ function nativePluginDescriptor(runtime) { // Required lazily so a missing build surfaces at call time with a clear message // rather than at module load for callers that never touch this family. let registry; try { registry = require('../../msd-core/bin/lib/capability-registry.cjs'); } catch (err) { throw new Error( 'emitted-provenance: cannot load msd-core/bin/lib/capability-registry.cjs ' + `(run \`npm run build\` first): ${err.message}`, ); } const entry = registry && registry.runtimes && registry.runtimes[runtime] && registry.runtimes[runtime].runtime && registry.runtimes[runtime].runtime.hostBehaviors; return (entry && entry.nativePlugin) || null; } // ─── The table ──────────────────────────────────────────────────────────────── // // kind: // identity — emitted path IS the repo path // rewrite — emitted path maps to a differently-named repo path // derived — emitted file is generated from another repo file // descriptor — source declared by a first-party runtime descriptor // code-derived — content is a literal inside a repo source file (attributable) // synthesized — install-time/environment state, no repo content source (EXEMPT) // // `roots` — emitted prefixes this rule applies under (null = match `rel` whole) // `pattern` — matched against the root-stripped tail (or whole `rel` when roots is null) // `sources` — (match, ctx) => string[] of repo-relative paths; [] only for `synthesized` // `transforms` — OPTIONAL repo paths implementing the TRANSFORM that produces this // rule's emitted bytes (#2757). A `derived`/`code-derived` artifact's // bytes can move for a second reason `sources` alone cannot express: // the transform code changed, not the source it derives from. // Phase 3 (emitted-diff.cjs) attributes a moved path if the diff // satisfies EITHER `sources` OR `transforms`, reusing the same // `sourceSatisfiedBy` matcher for both so exact/prefix semantics stay // identical. `kind: 'identity'` rules MUST NOT declare a non-empty // `transforms` — enforced by `assertNoIdentityTransforms` below — because // an identity copy's bytes can only move when its source moves; that is // what makes it an identity. // May be a plain `string[]` (the common case) OR a `(match, ctx) => string[]` // function, mirroring `sources`, for a rule whose transform attribution // depends on WHICH emitted path matched — e.g. `hooks-built` below, where // only the `.cmd` shim sub-family has transform code at all; a static // array would either miss that or (worse) falsely blanket-attribute every // plain hook file to the shim generator that cannot move its bytes. A // dedicated rule for the `.cmd` sub-family was considered and rejected: it // is emitted ONLY on win32 (see `hooks-built` below), so on every non- // Windows CI lane it would match zero paths and fail as a "dead" rule. // // Rule ORDER CARRIES NO SEMANTICS. Exactly-one matching is enforced, so rules are // mutually exclusive by construction and the table reads correctly in any order. const PROVENANCE_RULES = [ // ── Verbatim engine payload ──────────────────────────────────────────────── { id: 'msd-core-verbatim', // Markdown payloads pass through copyWithPathReplacement's audience // filter for non-Copilot runtimes. Non-Markdown payloads remain byte-for- // byte copies, but one rule has one kind; the match-specific transform // list below keeps the causal attribution precise. kind: 'derived', roots: ['msd-core'], // Enumerated subdirs, NOT `.+`: a new msd-core/ must fail totality // loudly rather than being absorbed silently. Also keeps this mutually // exclusive with the two synthesized msd-core top-level files below. pattern: /^(workflows|references|templates|contexts|bin)\/.+$/, sources: (m) => [`msd-core/${m[0]}`], transforms: (m, ctx) => ( m[0].endsWith('.md') && RUNTIME_NOTE_FILTERED_FAMILIES.has(ctx.runtime) ? RUNTIME_NOTE_FILTER_TRANSFORM_SRCS : [] ), }, { id: 'msd-core-commands-corpus', kind: 'rewrite', roots: ['msd-core'], pattern: /^commands\/msd\/(.+)$/, sources: (m) => [`commands/msd/${m[1]}`], transforms: [INSTALL_ENGINE_SRC, INSTALLER_SRC], }, { id: 'msd-core-agents-corpus', kind: 'rewrite', roots: ['msd-core'], pattern: /^agents\/(.+)$/, sources: (m) => [`agents/${m[1]}`], transforms: [INSTALL_ENGINE_SRC, INSTALLER_SRC], }, { id: 'scripts-verbatim', kind: 'identity', roots: ['scripts'], pattern: /^.+$/, sources: (m) => [`scripts/${m[0]}`], }, { // Historical id — retained even though, per #2757, this is no longer identity. // Nothing else in the repo keys off this string (checked), and the `sources` // shape below is unchanged, so renaming it would only widen the diff. id: 'agents-verbatim', // #2757 (was `identity`): measured against origin/next's own committed fixtures, // the SAME emitted agents/.md hashes DIFFERENTLY per runtime (e.g. codex vs // claude for msd-nyquist-auditor.md) — impossible for a true verbatim copy. // Verified empirically by installing every runtime and diffing the output against // the raw repo source: no runtime reproduces it byte-for-byte. Every one rewrites // frontmatter quoting and the hardcoded `.claude/` self-reference // (src/runtime-artifact-conversion.cts); Claude additionally gets an `effort:` // line injected (src/install-effort-resolver.cts + src/model-catalog.cts). See // the #2757 design doc for the alternatives considered (per-runtime split, // per-runtime `kind`) and why this wholesale reclassification was chosen instead. kind: 'derived', roots: ['agents'], // Excludes `msd.md`: that is a ROOT agent built from a code literal, NOT a // repo agent file. Without the exclusion it matched here and resolved to // `agents/msd.md`, which does not exist — a false attribution that still passed // totality, i.e. the exact residual ADR-2719 records. Every repo agent is // `msd-.md`, so excluding the bare `msd.md` is precise. // `.agent.md` is likewise excluded — Copilot emits a RENAMED copy // (`.agent.md`) whose source is `agents/.md`; matching it here // resolved to a file that does not exist. Same false-attribution class. pattern: /^(?!msd\.md$)(?!.*\.agent\.md$)[^/]+\.md$/, sources: (m) => [`agents/${m[0]}`], transforms: AGENT_TRANSFORM_SRCS, }, // ── Derived from another repo file ───────────────────────────────────────── { id: 'agents-toml-derived', kind: 'derived', roots: ['agents'], // Codex emits a .toml agent descriptor alongside/instead of the .md, generated // from the same agents/.md source. pattern: /^([^/]+)\.toml$/, sources: (m) => [`agents/${m[1]}.md`], // #2757 (issue text, PR #2566): a change to the conversion/effort code can move // every emitted .toml without touching any agents/*.md. See AGENT_TRANSFORM_SRCS. transforms: AGENT_TRANSFORM_SRCS, }, { id: 'hooks-built', // `kind` is a single scalar per rule, and this rule's majority sub-family // (plain built hook files) is genuinely `derived` from hooks/ via // scripts/build-hooks.js. The `.cmd` shim sub-family is code-derived (see // the pattern-comment and `sources` comment below), but that does not // change this rule's `kind` — a per-match `kind` would need a dedicated // rule, which is exactly what was rejected above (dead on non-Windows CI // lanes). The `.cmd` branch's real (code-derived) provenance is instead // carried precisely by `sources`/`transforms` both pointing at // HOOKS_WINDOWS_SHIM_SRC — `assertNoIdentityTransforms` only constrains // `kind: 'identity'` rules, so a `derived` rule with a non-empty // `transforms` here is unaffected by that guard. kind: 'derived', roots: HOOKS_ROOTS, // Emitted from hooks/dist/, which scripts/build-hooks.js builds from hooks/. // Attribute to the REPO source a PR actually edits, not the build artifact. // Excludes Copilot's hook-registration JSON (next rule) — that is a code // literal, not a built script, and attributing it here resolved to a // nonexistent `hooks/msd-session.json`. `package.json` is excluded for the // same reason (the #2544 `commonjs-marker` rule below): there is no // `hooks/package.json` in the repo to attribute to. // // `.cmd` shims are a SEPARATE, Windows-only emission path folded into this // SAME rule rather than a dedicated one (see the `transforms` doc above for // why a standalone rule would go dead on non-Windows CI lanes). Verified via // Memtrace: `hooks/.cmd` is written at install time by // ensureCodexHooksJsonSessionStart / ensureCodexHooksJsonEvent (both gated // on `platform === 'win32'`) via buildCodexHookWindowsShimIR // (HOOKS_WINDOWS_SHIM_SRC, src/runtime-hooks-surface.cts). The `.cmd` bytes // are `@ECHO OFF\r\n@SETLOCAL\r\n@