altimate-receipts 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@ import {
7
7
  loadSession,
8
8
  selectSummary,
9
9
  upsertSection
10
- } from "./chunk-7LROPRL2.js";
10
+ } from "./chunk-Y53BB6HS.js";
11
11
 
12
12
  // src/report/sessions.ts
13
13
  async function deriveTargets(opts) {
@@ -259,4 +259,4 @@ export {
259
259
  renderTrends,
260
260
  upsertTrendsSection
261
261
  };
262
- //# sourceMappingURL=chunk-IRUM4XOY.js.map
262
+ //# sourceMappingURL=chunk-3QI3VEWL.js.map
@@ -170,7 +170,8 @@ var PRIVILEGED_PREFIXES = [
170
170
  "obfuscated-exec",
171
171
  "untrusted-dep-source",
172
172
  "persistence-write",
173
- "masked-failure"
173
+ "masked-failure",
174
+ "external-state-mismatch"
174
175
  ];
175
176
  var TEST_PATH = /(?:^|\/)(?:tests?|specs?|__tests__)(?:\/|$)|\.(?:test|spec)\.|_test\./;
176
177
  function privileged(id, filePath) {
@@ -229,4 +230,4 @@ export {
229
230
  regenerableReceiptRm,
230
231
  regenerableBuildArtifact
231
232
  };
232
- //# sourceMappingURL=chunk-RYOBPBGP.js.map
233
+ //# sourceMappingURL=chunk-KOPNMBLZ.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../policy/guardrails.policy.json","../src/policy/load.ts","../src/policy/classify.ts","../src/findings/surface.ts"],"sourcesContent":["{\n \"version\": 1,\n \"_doc\": \"CANONICAL guardrail policy — the single source of truth for the trust layer's surfaces: live hook (hooks/guard-policy.mjs), post-hoc detector/render (src/findings/surface.ts + action/verified-by.mjs), and fleet (receipts-cloud/app/guardrails.py). DATA only — each surface has a thin language-specific evaluator. Lists/messages/patterns live HERE ONLY so they cannot drift. See policy/README.md for the migration checklist.\",\n \"buildDirs\": [\n \"target\",\n \"dist\",\n \"build\",\n \"out\",\n \"node_modules\",\n \".next\",\n \".nuxt\",\n \".turbo\",\n \".parcel-cache\",\n \"__pycache__\",\n \".pytest_cache\",\n \".mypy_cache\",\n \".ruff_cache\",\n \"coverage\",\n \".gradle\"\n ],\n \"buildDirsExcluded\": {\n \"vendor\": \"committed in Go/PHP\",\n \".cache\": \"stateful / app-specific\",\n \".terraform\": \"stateful (provider plugins + lock)\"\n },\n \"protectedBranches\": \"main|master|release/|prod|production\",\n \"ephemeral\": \"(^|/)(tmp|\\\\.venv|venv)(/|$)|^/(private/)?tmp/|^/var/folders/|\\\\.(bak|tmp|temp|pyc|pyo|class|o|log)$\",\n \"guardrails\": {\n \"destructive-op\": {\n \"severity\": \"critical\",\n \"guardrail\": \"Require explicit confirmation before destructive ops (rm -rf, DROP/TRUNCATE, git reset --hard) outside a sandbox.\",\n \"fix\": \"Review the destructive op; confirm nothing needed was lost before merging.\"\n },\n \"force-push\": {\n \"severity\": \"critical\",\n \"guardrail\": \"Block force-push to shared/protected branches; confirm on feature branches.\",\n \"fix\": \"Verify no teammate work was overwritten by the force-push.\"\n },\n \"hook-bypass\": {\n \"severity\": \"high\",\n \"action\": \"ask\",\n \"guardrail\": \"Disallow --no-verify / git-hook bypass on protected branches; let the hooks run.\",\n \"fix\": \"Re-run the pre-commit/pre-push hooks that were skipped and address any failures.\"\n },\n \"pipe-from-internet\": {\n \"severity\": \"high\",\n \"action\": \"ask\",\n \"guardrail\": \"Never pipe a script straight from the internet into a shell; download, read, then run.\",\n \"fix\": \"Fetch the script to a file, review it, and run it explicitly if safe.\"\n },\n \"force-with-lease\": {\n \"severity\": \"medium\",\n \"action\": \"warn\",\n \"guardrail\": \"Force-with-lease is safer than --force but still rewrites shared history — confirm intent.\",\n \"fix\": \"Confirm the rewrite is intended and coordinated with anyone sharing the branch.\"\n },\n \"grader-harness-edit\": {\n \"severity\": \"high\",\n \"action\": \"warn\",\n \"guardrail\": \"Never edit the grader/test/harness/config to make a check pass.\",\n \"fix\": \"Revert the grader/test edit and fix the underlying issue instead.\"\n },\n \"cicd-edit\": {\n \"severity\": \"medium\",\n \"action\": \"warn\",\n \"guardrail\": \"Flag edits to CI/CD pipeline files for human review before merge.\",\n \"fix\": \"Review the CI/CD change; confirm it was intended and doesn't weaken checks.\"\n },\n \"secret-exposure\": {\n \"severity\": \"high\",\n \"action\": \"warn\",\n \"guardrail\": \"Never echo or commit secrets; redact tokens/keys in output and files.\",\n \"fix\": \"Rotate any exposed secret and remove it from history.\"\n }\n }\n}\n","// Canonical guardrails policy for the TS surfaces (post-hoc detector + the live-guard CLI command).\n//\n// The policy JSON (policy/guardrails.policy.json) is the CROSS-LANGUAGE canon — also read by the\n// live-guard .mjs (via policy/load.mjs) and receipts-cloud (Python). Here it is *imported*, not\n// file-read, so esbuild/tsup inlines it into `dist` and it ships in the published package (which\n// includes `dist` only). This file mirrors policy/load.mjs so the two stay byte-equivalent.\n//\n// This is THE single source of the guardrail data for TS: BUILD_DIRS, the ephemeral/protected\n// patterns, and the per-rule message metadata. The post-hoc detector imports from here instead of\n// re-declaring (it previously had its own BUILD_DIRS in surface.ts, a PROTECTED_REF in gitParse.ts,\n// and an inline ephemeral check) — eliminating the drift the dogfood audit found.\nimport policyJson from \"../../policy/guardrails.policy.json\";\n\ninterface GuardrailMeta {\n severity: string;\n guardrail: string;\n fix: string;\n action?: \"deny\" | \"ask\" | \"warn\";\n}\n\ninterface Policy {\n buildDirs: string[];\n protectedBranches: string;\n ephemeral: string;\n guardrails: Record<string, GuardrailMeta>;\n}\n\n/** The raw canonical policy object. */\nexport const policy: Policy = policyJson as Policy;\n\n/** Frozen regenerable build-output dir set (the formerly-duplicated list, now sourced here). */\nexport const BUILD_DIRS: ReadonlySet<string> = new Set(policy.buildDirs);\n\n/** Protected-branch matcher for force-push scoping (deny on protected, else confirm). */\nexport const PROTECTED_BRANCHES = new RegExp(\n `\\\\b(origin\\\\s+)?(${policy.protectedBranches})\\\\b`,\n \"i\",\n);\n\n/** Ephemeral scratch path/file matcher (`/tmp`, `.venv`, `*.bak`, …). */\nexport const EPHEMERAL = new RegExp(policy.ephemeral, \"i\");\n\n/** id → { severity, guardrail, fix, action? } — the shared message + tier metadata. */\nexport const G: Record<string, GuardrailMeta> = policy.guardrails;\n","// Canonical FP-aware classification PRIMITIVES, shared by the post-hoc detector (src/findings/*)\n// and the live-guard CLI command (src/hook/guard.ts). The \"ONE policy, three surfaces\" promise was\n// only true for the policy DATA (policy/guardrails.policy.json); the LOGIC was duplicated between\n// the live guard (hooks/guard-policy.mjs) and the post-hoc TS, and drifted — the dogfood audit\n// found the SAME force-push / scratch FP fixed twice (PR #190 live, PR #191 post-hoc). These are the\n// pure primitives both surfaces compose; each surface keeps its own input adapter (the live guard\n// works on a raw command string; the post-hoc on parsed spans + finding titles).\nimport { BUILD_DIRS, EPHEMERAL, PROTECTED_BRANCHES } from \"./load.js\";\n\n/**\n * Strip heredoc bodies (`<<TAG … TAG`) before classifying — they are DATA (commit messages, review\n * prompts, file content), not executed commands. A commit message that *describes* `DROP DATABASE`\n * or `rm -rf /` must not be read as if it ran one. Handles `<<-`, quoted/unquoted markers, multiple\n * heredocs, and an unclosed (truncated) heredoc (stripped to end). (Rare exception: `bash <<EOF`\n * executes the body; the post-hoc detector is the backstop there.)\n */\nexport function stripHeredocs(s: string): string {\n const closed = /<<-?\\s*(['\"]?)([A-Za-z_]\\w*)\\1[\\s\\S]*?\\n[ \\t]*\\2(?![A-Za-z0-9_])/g;\n const stripped = s.replace(closed, \"<<heredoc\");\n // An opener with no matching close (e.g. a truncated capture) → drop to end.\n return stripped.replace(/<<-?\\s*(['\"]?)[A-Za-z_]\\w*\\1[\\s\\S]*$/, \"<<heredoc\");\n}\n\n/**\n * Split a command into statements on `\\n ; && ||` — but NOT on a single `|` (a pipeline is one\n * statement). Quote-aware so separators inside strings don't fragment it. The core FP fix: each\n * statement is classified independently, so a `git push -f` to a feature branch is not denied\n * because `origin/main` appears in a *different* statement of the same merge-loop script.\n */\nexport function splitStatements(cmd: string): string[] {\n const out: string[] = [];\n let cur = \"\";\n let q: string | null = null;\n for (let i = 0; i < cmd.length; i++) {\n const c = cmd[i];\n if (q) {\n cur += c;\n if (c === q) q = null;\n continue;\n }\n if (c === \"'\" || c === '\"') {\n q = c;\n cur += c;\n continue;\n }\n if (c === \"\\n\" || c === \";\") {\n out.push(cur);\n cur = \"\";\n continue;\n }\n if ((c === \"&\" && cmd[i + 1] === \"&\") || (c === \"|\" && cmd[i + 1] === \"|\")) {\n out.push(cur);\n cur = \"\";\n i++;\n continue;\n }\n cur += c;\n }\n out.push(cur);\n return out.map((s) => s.trim()).filter(Boolean);\n}\n\n/** A path operand is a regenerable build artifact iff a path segment is a known build dir\n * (`node_modules`, `dist`, `target`, …). Deliberately EXCLUDES vendor/.cache/.terraform — see\n * guardrails.policy.json `buildDirsExcluded`. */\nexport function isRegenerableBuildDir(path: string): boolean {\n return path\n .replace(/^[\"']|[\"']$/g, \"\")\n .split(\"/\")\n .some((seg) => BUILD_DIRS.has(seg));\n}\n\n/** A path operand is ephemeral scratch (`/tmp`, `/var/folders`, `.venv`, `*.bak`, …) per the\n * canonical EPHEMERAL pattern — removing it is housekeeping, not a merge risk. */\nexport function isEphemeralTarget(path: string): boolean {\n return EPHEMERAL.test(path.replace(/^[\"']|[\"']$/g, \"\"));\n}\n\n/** A git ref (a force-push target) names a protected/shared branch (`main`, `master`, `release/`,\n * `prod`, `production`) — force-pushing one is a real merge risk; a feature branch is routine. */\nexport function isProtectedBranch(ref: string): boolean {\n return PROTECTED_BRANCHES.test(ref);\n}\n","/**\n * Which surface a finding belongs to. The **merge gate** (the PR comment) is about\n * the *merged artifact* — what a reviewer must check before merging. The **operator**\n * view (the `receipts` card and `receipts trends`) is about *how the agent worked* —\n * efficiency, cost, loops, retries. One source of truth, shared by the PR-comment\n * renderer and its standalone Action mirror.\n */\n\nimport { isRegenerableBuildDir } from \"../policy/classify.js\";\n\n/** Kinds that describe agent behaviour/efficiency, not the merged change. */\nconst OPERATOR_KINDS = new Set([\n \"loop\",\n \"bottleneck\",\n \"cost-concentration\",\n \"cache-opportunity\",\n \"model-downgrade\",\n]);\n\nexport type Surface = \"merge\" | \"operator\";\n\n/** Finding-id prefixes that touch a PRIVILEGED surface — CI/CD, lockfiles, hooks,\n * checker config, graders/evals, test integrity, git history (SPEC-0073 R5). These\n * render first in the Agent Work Record, ▲-marked, never collapsed: an edit here runs\n * with repository secrets or moves the ground truth reviewers stand on. */\nexport const PRIVILEGED_PREFIXES = [\n \"ci-cd-touch\",\n \"lockfile-edit\",\n \"hook-bypass\",\n \"config-weaken\",\n \"spec-tamper\",\n \"grader-edit\",\n \"eval-override\",\n \"test-focus\",\n \"test-skipped\",\n \"test-trivialised\",\n \"green-by-suppression\",\n \"untested-test\",\n \"history-rewrite\",\n \"force-push\",\n \"self-permission\",\n \"sensitive-access\",\n \"guard-blocked\",\n \"mcp-write\",\n \"mcp-config-change\",\n \"metadata-egress\",\n \"tls-verify-disabled\",\n \"setuid-create\",\n \"obfuscated-exec\",\n \"untrusted-dep-source\",\n \"persistence-write\",\n \"masked-failure\",\n];\n\n/** `file-shrink` is privileged only when the shrunk file is a test (assertion loss). */\nconst TEST_PATH = /(?:^|\\/)(?:tests?|specs?|__tests__)(?:\\/|$)|\\.(?:test|spec)\\.|_test\\./;\n\nexport function privileged(id: string, filePath?: string): boolean {\n if (PRIVILEGED_PREFIXES.some((p) => id.startsWith(p))) {\n return true;\n }\n return id.startsWith(\"file-shrink\") && !!filePath && TEST_PATH.test(filePath);\n}\n\nexport function findingSurface(id: string): Surface {\n // Classify on the BASE kind: strip a subagent suffix (`-sa-<agentId>`, M104) so a wrapped operator\n // id like `bottleneck-sa-1a2b3c4d` is still recognized as operator and can't leak onto the merge\n // surface. (`errcluster-<tool>` — \"X failed N× before succeeding\" — is also an efficiency signal.)\n const base = id.replace(/-sa-[0-9a-f]+$/i, \"\");\n if (OPERATOR_KINDS.has(base) || base.startsWith(\"errcluster-\")) {\n return \"operator\";\n }\n return \"merge\";\n}\n\n/**\n * Shared shape for the destructive-op carve-outs below. Parses the `rm`/`rmdir` clause from a\n * `Destructive op:` finding title and returns true only when EVERY removal operand satisfies\n * `safe`. A non-rm destructive (git/SQL/infra acts on the repo or an external system) or an empty\n * operand list returns false — i.e. the finding keeps flagging. Each carve-out below just supplies\n * the per-operand `safe` predicate.\n */\nfunction everyRemovalOperand(title: string, safe: (operand: string) => boolean): boolean {\n const clause = title.replace(/^Destructive op:\\s*/i, \"\").replace(/\\s*×\\d+\\s*$/, \"\");\n const m = clause.match(/^\\s*(rm|rmdir)\\b(.*)$/i);\n if (!m) {\n return false;\n }\n // Drop shell redirects and anything after them (`2>/dev/null`, `> log`, `&>f`, `2>&1`) and a\n // trailing `— via subagent …` attribution, so they aren't read as removal operands. Without this,\n // `rm -rf /tmp/x 2>/dev/null` keeps the `/tmp` carve-out from recognizing the scratch target (the\n // redirect token isn't a path), and the op false-flags as a merge risk.\n const args = m[2].replace(/\\s+(?:\\d*>>?|\\d*<|&>|>&)\\S*.*$/, \"\").replace(/\\s+—\\s.*$/, \"\");\n const operands = args.split(/\\s+/).filter((t) => t && !t.startsWith(\"-\"));\n return operands.length > 0 && operands.every(safe);\n}\n\n/** Strip surrounding quotes from a parsed operand before matching. */\nconst unquote = (operand: string): string => operand.replace(/^[\"']|[\"']$/g, \"\");\n\n/**\n * True when a destructive finding's target is a **scratch path** (a `$VAR`, `/tmp`,\n * `/var/folders`, or `~`), so it can't be a merge risk and shouldn't gate the PR. Conservative:\n * `rm -rf ./src`, `rm -rf build`, `git reset --hard`, `DROP TABLE` are all kept (return false).\n */\nexport function destructiveOutsideRepo(title: string): boolean {\n return everyRemovalOperand(title, (operand) => {\n const p = unquote(operand);\n return (\n /\\$/.test(p) ||\n p.startsWith(\"/tmp\") ||\n p.startsWith(\"/var/folders\") ||\n p.startsWith(\"/private/var/folders\") ||\n p.startsWith(\"~\")\n );\n });\n}\n\n/**\n * True when a destructive finding deletes only the tool's **own regenerable Receipt artifact**\n * (`rm .receipts/<branch>.json`) — a scope-layer carve-out: a `.receipts/*.json` is inside the repo\n * but is rewritten by `receipts pr`, so it isn't a merge risk. Only a flat `.receipts/<name>.json`\n * file matches; a mixed command or the whole dir (`rm -rf .receipts`) keeps flagging.\n */\nexport function regenerableReceiptRm(title: string): boolean {\n return everyRemovalOperand(title, (operand) =>\n /(?:^|\\/)\\.receipts\\/[^/]+\\.json$/.test(unquote(operand)),\n );\n}\n\n// BUILD_DIRS (well-known regenerable build/dependency output dirs — deleting them is housekeeping,\n// not data loss) now comes from the canonical policy via ../policy/load.js — single source shared\n// with the live guard. Deliberately excludes vendor/.cache/.terraform (see guardrails.policy.json).\n\n/**\n * True when a destructive op deletes only **regenerable build artifacts** (`rm -rf dist`, `rm -rf\n * node_modules`). Scope-layer carve-out like {@link destructiveOutsideRepo}: build output is inside\n * the repo but regenerable in one command. Conservative: every operand must have a path segment in\n * the frozen build-dir set — a mixed command (`rm -rf dist src`) or a real target keeps flagging.\n */\nexport function regenerableBuildArtifact(title: string): boolean {\n return everyRemovalOperand(title, isRegenerableBuildDir);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../policy/guardrails.policy.json","../src/policy/load.ts","../src/policy/classify.ts","../src/findings/surface.ts"],"sourcesContent":["{\n \"version\": 1,\n \"_doc\": \"CANONICAL guardrail policy — the single source of truth for the trust layer's surfaces: live hook (hooks/guard-policy.mjs), post-hoc detector/render (src/findings/surface.ts + action/verified-by.mjs), and fleet (receipts-cloud/app/guardrails.py). DATA only — each surface has a thin language-specific evaluator. Lists/messages/patterns live HERE ONLY so they cannot drift. See policy/README.md for the migration checklist.\",\n \"buildDirs\": [\n \"target\",\n \"dist\",\n \"build\",\n \"out\",\n \"node_modules\",\n \".next\",\n \".nuxt\",\n \".turbo\",\n \".parcel-cache\",\n \"__pycache__\",\n \".pytest_cache\",\n \".mypy_cache\",\n \".ruff_cache\",\n \"coverage\",\n \".gradle\"\n ],\n \"buildDirsExcluded\": {\n \"vendor\": \"committed in Go/PHP\",\n \".cache\": \"stateful / app-specific\",\n \".terraform\": \"stateful (provider plugins + lock)\"\n },\n \"protectedBranches\": \"main|master|release/|prod|production\",\n \"ephemeral\": \"(^|/)(tmp|\\\\.venv|venv)(/|$)|^/(private/)?tmp/|^/var/folders/|\\\\.(bak|tmp|temp|pyc|pyo|class|o|log)$\",\n \"guardrails\": {\n \"destructive-op\": {\n \"severity\": \"critical\",\n \"guardrail\": \"Require explicit confirmation before destructive ops (rm -rf, DROP/TRUNCATE, git reset --hard) outside a sandbox.\",\n \"fix\": \"Review the destructive op; confirm nothing needed was lost before merging.\"\n },\n \"force-push\": {\n \"severity\": \"critical\",\n \"guardrail\": \"Block force-push to shared/protected branches; confirm on feature branches.\",\n \"fix\": \"Verify no teammate work was overwritten by the force-push.\"\n },\n \"hook-bypass\": {\n \"severity\": \"high\",\n \"action\": \"ask\",\n \"guardrail\": \"Disallow --no-verify / git-hook bypass on protected branches; let the hooks run.\",\n \"fix\": \"Re-run the pre-commit/pre-push hooks that were skipped and address any failures.\"\n },\n \"pipe-from-internet\": {\n \"severity\": \"high\",\n \"action\": \"ask\",\n \"guardrail\": \"Never pipe a script straight from the internet into a shell; download, read, then run.\",\n \"fix\": \"Fetch the script to a file, review it, and run it explicitly if safe.\"\n },\n \"force-with-lease\": {\n \"severity\": \"medium\",\n \"action\": \"warn\",\n \"guardrail\": \"Force-with-lease is safer than --force but still rewrites shared history — confirm intent.\",\n \"fix\": \"Confirm the rewrite is intended and coordinated with anyone sharing the branch.\"\n },\n \"grader-harness-edit\": {\n \"severity\": \"high\",\n \"action\": \"warn\",\n \"guardrail\": \"Never edit the grader/test/harness/config to make a check pass.\",\n \"fix\": \"Revert the grader/test edit and fix the underlying issue instead.\"\n },\n \"cicd-edit\": {\n \"severity\": \"medium\",\n \"action\": \"warn\",\n \"guardrail\": \"Flag edits to CI/CD pipeline files for human review before merge.\",\n \"fix\": \"Review the CI/CD change; confirm it was intended and doesn't weaken checks.\"\n },\n \"secret-exposure\": {\n \"severity\": \"high\",\n \"action\": \"warn\",\n \"guardrail\": \"Never echo or commit secrets; redact tokens/keys in output and files.\",\n \"fix\": \"Rotate any exposed secret and remove it from history.\"\n }\n }\n}\n","// Canonical guardrails policy for the TS surfaces (post-hoc detector + the live-guard CLI command).\n//\n// The policy JSON (policy/guardrails.policy.json) is the CROSS-LANGUAGE canon — also read by the\n// live-guard .mjs (via policy/load.mjs) and receipts-cloud (Python). Here it is *imported*, not\n// file-read, so esbuild/tsup inlines it into `dist` and it ships in the published package (which\n// includes `dist` only). This file mirrors policy/load.mjs so the two stay byte-equivalent.\n//\n// This is THE single source of the guardrail data for TS: BUILD_DIRS, the ephemeral/protected\n// patterns, and the per-rule message metadata. The post-hoc detector imports from here instead of\n// re-declaring (it previously had its own BUILD_DIRS in surface.ts, a PROTECTED_REF in gitParse.ts,\n// and an inline ephemeral check) — eliminating the drift the dogfood audit found.\nimport policyJson from \"../../policy/guardrails.policy.json\";\n\ninterface GuardrailMeta {\n severity: string;\n guardrail: string;\n fix: string;\n action?: \"deny\" | \"ask\" | \"warn\";\n}\n\ninterface Policy {\n buildDirs: string[];\n protectedBranches: string;\n ephemeral: string;\n guardrails: Record<string, GuardrailMeta>;\n}\n\n/** The raw canonical policy object. */\nexport const policy: Policy = policyJson as Policy;\n\n/** Frozen regenerable build-output dir set (the formerly-duplicated list, now sourced here). */\nexport const BUILD_DIRS: ReadonlySet<string> = new Set(policy.buildDirs);\n\n/** Protected-branch matcher for force-push scoping (deny on protected, else confirm). */\nexport const PROTECTED_BRANCHES = new RegExp(\n `\\\\b(origin\\\\s+)?(${policy.protectedBranches})\\\\b`,\n \"i\",\n);\n\n/** Ephemeral scratch path/file matcher (`/tmp`, `.venv`, `*.bak`, …). */\nexport const EPHEMERAL = new RegExp(policy.ephemeral, \"i\");\n\n/** id → { severity, guardrail, fix, action? } — the shared message + tier metadata. */\nexport const G: Record<string, GuardrailMeta> = policy.guardrails;\n","// Canonical FP-aware classification PRIMITIVES, shared by the post-hoc detector (src/findings/*)\n// and the live-guard CLI command (src/hook/guard.ts). The \"ONE policy, three surfaces\" promise was\n// only true for the policy DATA (policy/guardrails.policy.json); the LOGIC was duplicated between\n// the live guard (hooks/guard-policy.mjs) and the post-hoc TS, and drifted — the dogfood audit\n// found the SAME force-push / scratch FP fixed twice (PR #190 live, PR #191 post-hoc). These are the\n// pure primitives both surfaces compose; each surface keeps its own input adapter (the live guard\n// works on a raw command string; the post-hoc on parsed spans + finding titles).\nimport { BUILD_DIRS, EPHEMERAL, PROTECTED_BRANCHES } from \"./load.js\";\n\n/**\n * Strip heredoc bodies (`<<TAG … TAG`) before classifying — they are DATA (commit messages, review\n * prompts, file content), not executed commands. A commit message that *describes* `DROP DATABASE`\n * or `rm -rf /` must not be read as if it ran one. Handles `<<-`, quoted/unquoted markers, multiple\n * heredocs, and an unclosed (truncated) heredoc (stripped to end). (Rare exception: `bash <<EOF`\n * executes the body; the post-hoc detector is the backstop there.)\n */\nexport function stripHeredocs(s: string): string {\n const closed = /<<-?\\s*(['\"]?)([A-Za-z_]\\w*)\\1[\\s\\S]*?\\n[ \\t]*\\2(?![A-Za-z0-9_])/g;\n const stripped = s.replace(closed, \"<<heredoc\");\n // An opener with no matching close (e.g. a truncated capture) → drop to end.\n return stripped.replace(/<<-?\\s*(['\"]?)[A-Za-z_]\\w*\\1[\\s\\S]*$/, \"<<heredoc\");\n}\n\n/**\n * Split a command into statements on `\\n ; && ||` — but NOT on a single `|` (a pipeline is one\n * statement). Quote-aware so separators inside strings don't fragment it. The core FP fix: each\n * statement is classified independently, so a `git push -f` to a feature branch is not denied\n * because `origin/main` appears in a *different* statement of the same merge-loop script.\n */\nexport function splitStatements(cmd: string): string[] {\n const out: string[] = [];\n let cur = \"\";\n let q: string | null = null;\n for (let i = 0; i < cmd.length; i++) {\n const c = cmd[i];\n if (q) {\n cur += c;\n if (c === q) q = null;\n continue;\n }\n if (c === \"'\" || c === '\"') {\n q = c;\n cur += c;\n continue;\n }\n if (c === \"\\n\" || c === \";\") {\n out.push(cur);\n cur = \"\";\n continue;\n }\n if ((c === \"&\" && cmd[i + 1] === \"&\") || (c === \"|\" && cmd[i + 1] === \"|\")) {\n out.push(cur);\n cur = \"\";\n i++;\n continue;\n }\n cur += c;\n }\n out.push(cur);\n return out.map((s) => s.trim()).filter(Boolean);\n}\n\n/** A path operand is a regenerable build artifact iff a path segment is a known build dir\n * (`node_modules`, `dist`, `target`, …). Deliberately EXCLUDES vendor/.cache/.terraform — see\n * guardrails.policy.json `buildDirsExcluded`. */\nexport function isRegenerableBuildDir(path: string): boolean {\n return path\n .replace(/^[\"']|[\"']$/g, \"\")\n .split(\"/\")\n .some((seg) => BUILD_DIRS.has(seg));\n}\n\n/** A path operand is ephemeral scratch (`/tmp`, `/var/folders`, `.venv`, `*.bak`, …) per the\n * canonical EPHEMERAL pattern — removing it is housekeeping, not a merge risk. */\nexport function isEphemeralTarget(path: string): boolean {\n return EPHEMERAL.test(path.replace(/^[\"']|[\"']$/g, \"\"));\n}\n\n/** A git ref (a force-push target) names a protected/shared branch (`main`, `master`, `release/`,\n * `prod`, `production`) — force-pushing one is a real merge risk; a feature branch is routine. */\nexport function isProtectedBranch(ref: string): boolean {\n return PROTECTED_BRANCHES.test(ref);\n}\n","/**\n * Which surface a finding belongs to. The **merge gate** (the PR comment) is about\n * the *merged artifact* — what a reviewer must check before merging. The **operator**\n * view (the `receipts` card and `receipts trends`) is about *how the agent worked* —\n * efficiency, cost, loops, retries. One source of truth, shared by the PR-comment\n * renderer and its standalone Action mirror.\n */\n\nimport { isRegenerableBuildDir } from \"../policy/classify.js\";\n\n/** Kinds that describe agent behaviour/efficiency, not the merged change. */\nconst OPERATOR_KINDS = new Set([\n \"loop\",\n \"bottleneck\",\n \"cost-concentration\",\n \"cache-opportunity\",\n \"model-downgrade\",\n]);\n\nexport type Surface = \"merge\" | \"operator\";\n\n/** Finding-id prefixes that touch a PRIVILEGED surface — CI/CD, lockfiles, hooks,\n * checker config, graders/evals, test integrity, git history (SPEC-0073 R5). These\n * render first in the Agent Work Record, ▲-marked, never collapsed: an edit here runs\n * with repository secrets or moves the ground truth reviewers stand on. */\nexport const PRIVILEGED_PREFIXES = [\n \"ci-cd-touch\",\n \"lockfile-edit\",\n \"hook-bypass\",\n \"config-weaken\",\n \"spec-tamper\",\n \"grader-edit\",\n \"eval-override\",\n \"test-focus\",\n \"test-skipped\",\n \"test-trivialised\",\n \"green-by-suppression\",\n \"untested-test\",\n \"history-rewrite\",\n \"force-push\",\n \"self-permission\",\n \"sensitive-access\",\n \"guard-blocked\",\n \"mcp-write\",\n \"mcp-config-change\",\n \"metadata-egress\",\n \"tls-verify-disabled\",\n \"setuid-create\",\n \"obfuscated-exec\",\n \"untrusted-dep-source\",\n \"persistence-write\",\n \"masked-failure\",\n \"external-state-mismatch\",\n];\n\n/** `file-shrink` is privileged only when the shrunk file is a test (assertion loss). */\nconst TEST_PATH = /(?:^|\\/)(?:tests?|specs?|__tests__)(?:\\/|$)|\\.(?:test|spec)\\.|_test\\./;\n\nexport function privileged(id: string, filePath?: string): boolean {\n if (PRIVILEGED_PREFIXES.some((p) => id.startsWith(p))) {\n return true;\n }\n return id.startsWith(\"file-shrink\") && !!filePath && TEST_PATH.test(filePath);\n}\n\nexport function findingSurface(id: string): Surface {\n // Classify on the BASE kind: strip a subagent suffix (`-sa-<agentId>`, M104) so a wrapped operator\n // id like `bottleneck-sa-1a2b3c4d` is still recognized as operator and can't leak onto the merge\n // surface. (`errcluster-<tool>` — \"X failed N× before succeeding\" — is also an efficiency signal.)\n const base = id.replace(/-sa-[0-9a-f]+$/i, \"\");\n if (OPERATOR_KINDS.has(base) || base.startsWith(\"errcluster-\")) {\n return \"operator\";\n }\n return \"merge\";\n}\n\n/**\n * Shared shape for the destructive-op carve-outs below. Parses the `rm`/`rmdir` clause from a\n * `Destructive op:` finding title and returns true only when EVERY removal operand satisfies\n * `safe`. A non-rm destructive (git/SQL/infra acts on the repo or an external system) or an empty\n * operand list returns false — i.e. the finding keeps flagging. Each carve-out below just supplies\n * the per-operand `safe` predicate.\n */\nfunction everyRemovalOperand(title: string, safe: (operand: string) => boolean): boolean {\n const clause = title.replace(/^Destructive op:\\s*/i, \"\").replace(/\\s*×\\d+\\s*$/, \"\");\n const m = clause.match(/^\\s*(rm|rmdir)\\b(.*)$/i);\n if (!m) {\n return false;\n }\n // Drop shell redirects and anything after them (`2>/dev/null`, `> log`, `&>f`, `2>&1`) and a\n // trailing `— via subagent …` attribution, so they aren't read as removal operands. Without this,\n // `rm -rf /tmp/x 2>/dev/null` keeps the `/tmp` carve-out from recognizing the scratch target (the\n // redirect token isn't a path), and the op false-flags as a merge risk.\n const args = m[2].replace(/\\s+(?:\\d*>>?|\\d*<|&>|>&)\\S*.*$/, \"\").replace(/\\s+—\\s.*$/, \"\");\n const operands = args.split(/\\s+/).filter((t) => t && !t.startsWith(\"-\"));\n return operands.length > 0 && operands.every(safe);\n}\n\n/** Strip surrounding quotes from a parsed operand before matching. */\nconst unquote = (operand: string): string => operand.replace(/^[\"']|[\"']$/g, \"\");\n\n/**\n * True when a destructive finding's target is a **scratch path** (a `$VAR`, `/tmp`,\n * `/var/folders`, or `~`), so it can't be a merge risk and shouldn't gate the PR. Conservative:\n * `rm -rf ./src`, `rm -rf build`, `git reset --hard`, `DROP TABLE` are all kept (return false).\n */\nexport function destructiveOutsideRepo(title: string): boolean {\n return everyRemovalOperand(title, (operand) => {\n const p = unquote(operand);\n return (\n /\\$/.test(p) ||\n p.startsWith(\"/tmp\") ||\n p.startsWith(\"/var/folders\") ||\n p.startsWith(\"/private/var/folders\") ||\n p.startsWith(\"~\")\n );\n });\n}\n\n/**\n * True when a destructive finding deletes only the tool's **own regenerable Receipt artifact**\n * (`rm .receipts/<branch>.json`) — a scope-layer carve-out: a `.receipts/*.json` is inside the repo\n * but is rewritten by `receipts pr`, so it isn't a merge risk. Only a flat `.receipts/<name>.json`\n * file matches; a mixed command or the whole dir (`rm -rf .receipts`) keeps flagging.\n */\nexport function regenerableReceiptRm(title: string): boolean {\n return everyRemovalOperand(title, (operand) =>\n /(?:^|\\/)\\.receipts\\/[^/]+\\.json$/.test(unquote(operand)),\n );\n}\n\n// BUILD_DIRS (well-known regenerable build/dependency output dirs — deleting them is housekeeping,\n// not data loss) now comes from the canonical policy via ../policy/load.js — single source shared\n// with the live guard. Deliberately excludes vendor/.cache/.terraform (see guardrails.policy.json).\n\n/**\n * True when a destructive op deletes only **regenerable build artifacts** (`rm -rf dist`, `rm -rf\n * node_modules`). Scope-layer carve-out like {@link destructiveOutsideRepo}: build output is inside\n * the repo but regenerable in one command. Conservative: every operand must have a path segment in\n * the frozen build-dir set — a mixed command (`rm -rf dist src`) or a real target keeps flagging.\n */\nexport function regenerableBuildArtifact(title: string): boolean {\n return everyRemovalOperand(title, isRegenerableBuildDir);\n}\n"],"mappings":"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","names":[]}
@@ -3,6 +3,7 @@ import {
3
3
  agentIds,
4
4
  applyDiffScope,
5
5
  buildReceipt,
6
+ computeCompletion,
6
7
  deriveEvidence,
7
8
  deriveFindings,
8
9
  deriveSpans,
@@ -16,18 +17,18 @@ import {
16
17
  redactReceipt,
17
18
  renderLedger,
18
19
  windowedEffort
19
- } from "./chunk-7LROPRL2.js";
20
+ } from "./chunk-Y53BB6HS.js";
20
21
  import {
21
22
  FIX_FOR,
22
23
  categorize,
23
24
  checkForId
24
- } from "./chunk-VKT2ZUCT.js";
25
+ } from "./chunk-SVTKMARA.js";
25
26
  import {
26
27
  destructiveOutsideRepo,
27
28
  findingSurface,
28
29
  privileged,
29
30
  regenerableBuildArtifact
30
- } from "./chunk-RYOBPBGP.js";
31
+ } from "./chunk-KOPNMBLZ.js";
31
32
 
32
33
  // src/report/sarif.ts
33
34
  var INFO_URI = "https://github.com/AltimateAI/altimate-receipts";
@@ -93,6 +94,44 @@ function toSarif(receipt) {
93
94
  }
94
95
  return result;
95
96
  });
97
+ for (const scanner of p.scanners ?? []) {
98
+ for (const sr of scanner.results ?? []) {
99
+ const ruleId = sr.ruleId || scanner.tool;
100
+ const level = sr.level;
101
+ const existingRule = rules.get(ruleId);
102
+ if (!existingRule) {
103
+ ruleOrder.push(ruleId);
104
+ rules.set(ruleId, {
105
+ id: ruleId,
106
+ name: `${scanner.tool}:${ruleId}`,
107
+ shortDescription: { text: `${scanner.category} (${scanner.tool})` },
108
+ defaultConfiguration: { level },
109
+ helpUri: INFO_URI
110
+ });
111
+ } else if (sevRank[level] < sevRank[existingRule.defaultConfiguration.level]) {
112
+ existingRule.defaultConfiguration.level = level;
113
+ }
114
+ const uri = sr.file;
115
+ const result = {
116
+ ruleId,
117
+ ruleIndex: ruleOrder.indexOf(ruleId),
118
+ level,
119
+ message: { text: sr.message ?? `${scanner.tool} ${ruleId}` },
120
+ partialFingerprints: { receiptsId: `${ruleId}::${uri ?? "scanner"}` }
121
+ };
122
+ if (uri) {
123
+ result.locations = [
124
+ {
125
+ physicalLocation: {
126
+ artifactLocation: { uri },
127
+ ...sr.line ? { region: { startLine: sr.line } } : {}
128
+ }
129
+ }
130
+ ];
131
+ }
132
+ results.push(result);
133
+ }
134
+ }
96
135
  const ruleList = ruleOrder.map((id) => rules.get(id));
97
136
  return {
98
137
  $schema: "https://json.schemastore.org/sarif-2.1.0.json",
@@ -263,6 +302,85 @@ function serialize(value) {
263
302
  // src/report/prComment.ts
264
303
  import { createHash as createHash2 } from "crypto";
265
304
 
305
+ // src/findings/externalState.ts
306
+ var TARGET = /\b([A-Za-z0-9][\w.-]*\/[\w.-]+)#(\d+)\b|(?:^|[\s(])#(\d+)\b/;
307
+ var PR_REF = /(?:[A-Za-z0-9][\w.-]*\/[\w.-]+)?#\d+/;
308
+ var MERGED_OBJECT = new RegExp(
309
+ `\\b(?:merged|landed)\\s+(?:(?:the\\s+)?(?:pr|pull\\s*request)\\s+)?${PR_REF.source}`,
310
+ "i"
311
+ );
312
+ var MERGED_SUBJECT = new RegExp(
313
+ `${PR_REF.source}\\s+(?:(?:has|have|is|was|been|got|now|successfully|finally)\\s+){0,4}(?:merged|landed)\\b`,
314
+ "i"
315
+ );
316
+ var isMergedClaim = (s) => (MERGED_OBJECT.test(s) || MERGED_SUBJECT.test(s)) && !LOCAL_MERGE.test(s);
317
+ var CI_GREEN = /\b(?:ci|checks?|pipeline|the build|all checks?)\b[^.!?\n]*\b(?:is|are|all)?\s*(?:green|passing|passed|pass)\b/i;
318
+ var HEDGE = /\b(should|will|going to|once|after|when|if|need to|needs to|ready to|about to|pending|waiting)\b/i;
319
+ var NEGATION = /\b(not|isn'?t|aren'?t|wasn'?t|weren'?t|never|no longer|failed to|couldn'?t|can'?t|cannot|won'?t|without)\b/i;
320
+ var LOCAL_MERGE = /\bmerged\s+\S+\s+into\b|\bmerged\s+(?:main|master|develop|trunk|origin|upstream|HEAD)\b/i;
321
+ var OTHER_PR_BACKDROP = /\b(?:from|in|like|see|per|via)\s+(?:pr\s+)?#\s*\d|\bapproach\s+(?:from|in)\b|\bbase\s+branch\b|\bduplicat\w*/i;
322
+ function targetOf(sentence) {
323
+ const m = TARGET.exec(sentence);
324
+ if (!m) {
325
+ return void 0;
326
+ }
327
+ if (m[1] && m[2]) {
328
+ return `${m[1]}#${m[2]}`;
329
+ }
330
+ return m[3] ? `#${m[3]}` : void 0;
331
+ }
332
+ function parseExternalClaims(finalText) {
333
+ const out = [];
334
+ const seen = /* @__PURE__ */ new Set();
335
+ for (const sentence of finalText.split(/[.!?\n]+/)) {
336
+ if (HEDGE.test(sentence) || NEGATION.test(sentence) || OTHER_PR_BACKDROP.test(sentence)) {
337
+ continue;
338
+ }
339
+ const target = targetOf(sentence);
340
+ if (!target) {
341
+ continue;
342
+ }
343
+ const kind = isMergedClaim(sentence) ? "merged" : CI_GREEN.test(sentence) ? "ci-green" : void 0;
344
+ if (!kind) {
345
+ continue;
346
+ }
347
+ const key = `${kind}|${target}`;
348
+ if (seen.has(key)) {
349
+ continue;
350
+ }
351
+ seen.add(key);
352
+ out.push({ kind, target, raw: sentence.trim().slice(0, 200) });
353
+ }
354
+ return out;
355
+ }
356
+ function label(kind) {
357
+ return kind === "merged" ? "merged" : "CI green";
358
+ }
359
+ function deriveExternalStateRows(rows) {
360
+ const out = [];
361
+ let i = 0;
362
+ for (const r of rows) {
363
+ if (r.match) {
364
+ continue;
365
+ }
366
+ const who = r.actor ? ` by \`${r.actor}\`` : "";
367
+ const when = r.ts ? ` (${r.ts})` : "";
368
+ const at = r.fetchedAt ? ` fetched ${r.fetchedAt}` : "";
369
+ out.push({
370
+ id: `external-state-mismatch-${i}`,
371
+ severity: "high",
372
+ title: `Claimed ${r.target} ${label(r.kind)}, API reports ${r.actual}`,
373
+ detail: `The session claimed ${r.target} was ${r.claimed}, but the authoritative API reports \`${r.actual}\`${who}${when}${at}. This is a recorded input \u2014 the reconciler fetched it in CI; the deterministic core never makes the call. Confirm the claim, or correct the summary. ${r.url ?? ""}`.trim(),
374
+ impactLabel: "external state",
375
+ confidence: 1,
376
+ score: 100,
377
+ guardrailRule: "Don't claim a PR is merged / CI is green until the authoritative API confirms it; reconcile claims about external state before reporting done."
378
+ });
379
+ i++;
380
+ }
381
+ return out;
382
+ }
383
+
266
384
  // src/findings/guardEvents.ts
267
385
  var SEV = {
268
386
  deny: "critical",
@@ -398,6 +516,32 @@ function subagentRows(p) {
398
516
  ...s.filePath ? { filePath: s.filePath } : {}
399
517
  }));
400
518
  }
519
+ function externalStateRows(p) {
520
+ if (!p.externalState?.length) {
521
+ return [];
522
+ }
523
+ return deriveExternalStateRows(p.externalState).map((f) => ({
524
+ id: f.id,
525
+ severity: f.severity,
526
+ title: f.title,
527
+ detail: f.detail,
528
+ confidence: f.confidence,
529
+ score: f.score,
530
+ impactLabel: f.impactLabel
531
+ }));
532
+ }
533
+ function externalStateCorroborations(p) {
534
+ const ok = (p.externalState ?? []).filter((r) => r.match);
535
+ if (ok.length === 0) {
536
+ return void 0;
537
+ }
538
+ const chips = ok.map((r) => {
539
+ const who = r.actor ? ` by \`${r.actor}\`` : "";
540
+ const when = r.ts ? ` \xB7 ${r.ts}` : "";
541
+ return `\u2714 ${r.target} ${r.kind === "merged" ? "merged" : "CI green"}${who}${when}`;
542
+ }).join(" \xB7 ");
543
+ return `<sub>\u{1F310} external state reconciled: ${chips} (authoritative API, attested).</sub>`;
544
+ }
401
545
  function subagentCountLine(p) {
402
546
  const sa = p.subagents;
403
547
  if (!sa || sa.total === 0) {
@@ -407,11 +551,27 @@ function subagentCountLine(p) {
407
551
  const tail = shown > 0 ? `, ${shown} surfaced above` : ", none merge-relevant";
408
552
  return `<sub>\u{1F9F7} ${sa.total} subagent${sa.total === 1 ? "" : "s"} ran (${sa.considered} did file/command work${tail}) \u2014 separate transcripts, attributed.</sub>`;
409
553
  }
554
+ function witnessLine(p) {
555
+ const w = p.witness;
556
+ if (!w) {
557
+ return void 0;
558
+ }
559
+ if (w.tier === "witness-divergence") {
560
+ return `<sub>\u{1F6F0}\uFE0F model-side: <b>diverges from altimate-router</b> \u2014 ${w.present}/${w.witnessed} model decisions match the independent record; the rest are missing from this transcript.</sub>`;
561
+ }
562
+ const sig = w.signed === "verified" ? ", signature verified" : w.signed === "unverified" ? ", signature unverified" : "";
563
+ return `<sub>\u{1F6F0}\uFE0F model-side: witnessed by altimate-router \u2014 ${w.present}/${w.witnessed} model decisions independently recorded${sig}.</sub>`;
564
+ }
410
565
  function renderPrComment(receipt, opts = {}) {
411
566
  const p = receipt.predicate;
412
567
  const ev = p.evidence;
413
568
  const operatorCount = p.findings.filter((f) => findingSurface(f.id) === "operator").length;
414
- const main = visibleMergeFindings([...guardRows(p), ...subagentRows(p), ...p.findings]);
569
+ const main = visibleMergeFindings([
570
+ ...guardRows(p),
571
+ ...subagentRows(p),
572
+ ...externalStateRows(p),
573
+ ...p.findings
574
+ ]);
415
575
  const cats = categorize(main);
416
576
  const files = p.scope?.kind === "diff" ? p.scope.files ?? [] : [];
417
577
  const priv = main.filter((f) => privileged(f.id, f.filePath));
@@ -675,6 +835,14 @@ function recordDetails(p, ev, cats, operatorCount, opts, feedback) {
675
835
  if (saLine) {
676
836
  inner.push(saLine);
677
837
  }
838
+ const wLine = witnessLine(p);
839
+ if (wLine) {
840
+ inner.push(wLine);
841
+ }
842
+ const xLine = externalStateCorroborations(p);
843
+ if (xLine) {
844
+ inner.push(xLine);
845
+ }
678
846
  const signed = opts.signed ? `Signed (Sigstore \u2192 Rekor)${opts.attestationUrl ? ` \xB7 [attestation](${opts.attestationUrl})` : ""} \xB7 ` : "";
679
847
  inner.push(
680
848
  `<sub>${signed}Re-derivable (L1: \`receipts verify <receipt> --transcript <t>\`) \xB7 pull any event's tape: \`npx altimate-receipts log --ref <evidence-id>\` \xB7 deterministic \xB7 0 model calls \xB7 evidence, not judgement \xB7 [trust model](${TRUST_DOC_URL}).</sub>`
@@ -1127,7 +1295,11 @@ async function rederiveFromTranscript(path, opts = {}) {
1127
1295
  const branch = scope?.kind === "branch" ? scope.branch : opts.branch;
1128
1296
  const session = branch ? sliceByBranch(loaded, branch) ?? loaded : loaded;
1129
1297
  let derived = deriveSpans(session);
1298
+ if (opts.criteria?.length) {
1299
+ derived.criteria = opts.criteria;
1300
+ }
1130
1301
  let findings = deriveFindings(derived);
1302
+ const completion = opts.criteria?.length ? computeCompletion(derived, [...findings.main, ...findings.minor]) : void 0;
1131
1303
  if (scope?.kind === "diff" && scope.files) {
1132
1304
  const scoped = applyDiffScope(derived, findings, scope.files, session.projectPath);
1133
1305
  derived = scoped.derived;
@@ -1151,7 +1323,7 @@ async function rederiveFromTranscript(path, opts = {}) {
1151
1323
  }
1152
1324
  }
1153
1325
  }
1154
- const receipt = await buildReceipt(session, derived, findings, { scope });
1326
+ const receipt = await buildReceipt(session, derived, findings, { scope, completion });
1155
1327
  return opts.redact ? redactReceipt(receipt) : receipt;
1156
1328
  }
1157
1329
  async function compareToTranscript(committed, transcriptPath, opts = {}) {
@@ -1162,13 +1334,16 @@ async function compareToTranscript(committed, transcriptPath, opts = {}) {
1162
1334
  // codex/cursor receipt isn't re-run through the Claude adapter. An explicit
1163
1335
  // opts.source still wins.
1164
1336
  source: opts.source ?? sourceFromReceipt(committed),
1165
- scope: committed.predicate.scope
1337
+ scope: committed.predicate.scope,
1338
+ // SPEC-0118 — re-apply the recorded criteria basis (like scope) so a receipt
1339
+ // carrying completion findings re-derives byte-identically with no asserts.json.
1340
+ criteria: committed.predicate.evidence.completion?.criteria
1166
1341
  });
1167
1342
  if (!fresh) {
1168
1343
  return { rederived: false, matches: false };
1169
1344
  }
1170
1345
  const strip = (r) => {
1171
- if (!r.predicate.prEffort && !r.predicate.prHistory && !r.predicate.prCleared && !r.predicate.guardEvents && !r.predicate.subagents) {
1346
+ if (!r.predicate.prEffort && !r.predicate.prHistory && !r.predicate.prCleared && !r.predicate.guardEvents && !r.predicate.subagents && !r.predicate.scanners && !r.predicate.externalState && !r.predicate.witness) {
1172
1347
  return r;
1173
1348
  }
1174
1349
  const {
@@ -1177,6 +1352,9 @@ async function compareToTranscript(committed, transcriptPath, opts = {}) {
1177
1352
  prCleared: _z,
1178
1353
  guardEvents: _g,
1179
1354
  subagents: _s,
1355
+ scanners: _sc,
1356
+ externalState: _e,
1357
+ witness: _w,
1180
1358
  ...rest
1181
1359
  } = r.predicate;
1182
1360
  return { ...r, predicate: rest };
@@ -1194,6 +1372,7 @@ export {
1194
1372
  exporterIds,
1195
1373
  gitInvocations,
1196
1374
  canonicalize,
1375
+ parseExternalClaims,
1197
1376
  PR_COMMENT_MARKER,
1198
1377
  visibleMergeFindings,
1199
1378
  eventKey,
@@ -1212,4 +1391,4 @@ export {
1212
1391
  rederiveFromTranscript,
1213
1392
  compareToTranscript
1214
1393
  };
1215
- //# sourceMappingURL=chunk-7QTTBFY3.js.map
1394
+ //# sourceMappingURL=chunk-OW3O53BD.js.map