omk-protocol 0.98.3 → 0.98.5

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +44 -0
  3. package/dist/claims/claim-blocking-cut.d.ts +8 -15
  4. package/dist/claims/claim-blocking-cut.d.ts.map +1 -1
  5. package/dist/claims/claim-blocking-cut.js +161 -41
  6. package/dist/claims/claim-blocking-cut.js.map +1 -1
  7. package/dist/claims/claim-closure.d.ts.map +1 -1
  8. package/dist/claims/claim-closure.js +27 -6
  9. package/dist/claims/claim-closure.js.map +1 -1
  10. package/dist/claims/claim-types.d.ts +20 -2
  11. package/dist/claims/claim-types.d.ts.map +1 -1
  12. package/dist/claims/claim-types.js.map +1 -1
  13. package/dist/index.d.ts +7 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +6 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/run-contract.d.ts +58 -0
  18. package/dist/run-contract.d.ts.map +1 -0
  19. package/dist/run-contract.js +112 -0
  20. package/dist/run-contract.js.map +1 -0
  21. package/dist/run-dag.d.ts +18 -0
  22. package/dist/run-dag.d.ts.map +1 -0
  23. package/dist/run-dag.js +76 -0
  24. package/dist/run-dag.js.map +1 -0
  25. package/dist/run-parsing.d.ts +13 -0
  26. package/dist/run-parsing.d.ts.map +1 -0
  27. package/dist/run-parsing.js +81 -0
  28. package/dist/run-parsing.js.map +1 -0
  29. package/dist/run-resume.d.ts +15 -0
  30. package/dist/run-resume.d.ts.map +1 -0
  31. package/dist/run-resume.js +28 -0
  32. package/dist/run-resume.js.map +1 -0
  33. package/dist/run-task-retry.d.ts +8 -0
  34. package/dist/run-task-retry.d.ts.map +1 -0
  35. package/dist/run-task-retry.js +25 -0
  36. package/dist/run-task-retry.js.map +1 -0
  37. package/dist/run-writer-restart.d.ts +14 -0
  38. package/dist/run-writer-restart.d.ts.map +1 -0
  39. package/dist/run-writer-restart.js +27 -0
  40. package/dist/run-writer-restart.js.map +1 -0
  41. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [0.98.5] - 2026-09-12
6
+
7
+ ### Added
8
+
9
+ - Added immutable verified-run contracts and validated start, candidate-resume, writer-restart and task-retry commands, plus bounded static DAG types and ordering helpers. Protocol data does not execute or approve work by itself.
10
+
11
+ ## [0.98.4] - 2026-09-10
12
+
13
+ ### Added
14
+
15
+ - Added bounded `explainBlockingCut()` metadata and opt-in `witnessIndependence: "explicit-groups"`. The compatibility default remains observation-ID grouping.
16
+
17
+ ### Fixed
18
+
19
+ - Shared DAG branches can share a repair instead of choosing incompatible local minima. Composite-local obligations remain visible. A bounded-search greedy fallback explicitly does not claim minimum cardinality.
20
+
3
21
  ## [0.98.3] - 2026-09-06
4
22
 
5
23
  ### Added
package/README.md CHANGED
@@ -12,6 +12,21 @@ TaskSpec -> ExecutionAttempt -> Observation -> EvaluationResult -> RuntimeDecisi
12
12
  - `TaskSpec`, `ExecutionAttempt`, `Observation`, `ClaimPredicate`
13
13
  - `ClaimEvaluation`, `EvaluationResult`, `RuntimeDecision`, `WaiverRecord`
14
14
  - Runtime parsers for every top-level record
15
+ - Opt-in command/scripted-agent profiles: immutable `RunContract`, `RunScriptedWriter`,
16
+ `RunStartCommand`, `parseRunContract()` and `parseRunStartCommand()`
17
+ (`omk.verified-run.v1` and `omk.verified-command.v1`). The scripted profile bounds
18
+ approved steps and offline logical requests. Parsing never grants approval or executes work.
19
+ - `RunResumeCommand`, `parseRunResumeCommand()` and `MAX_VERIFIED_RUN_GENERATIONS`:
20
+ candidate/contract/revision/generation-bound intent for host-governed recovery.
21
+ The runtime must still verify owner, clock, budget, process termination and evidence.
22
+ - `RunWriterRestartCommand` / `parseRunWriterRestartCommand()` separately bind a local
23
+ writer restart to an immutable input checkpoint. They grant no remote replay authority.
24
+ - Bounded `linux-command-dag-v1`: `RunDagWriter`, `RunDagTask`, `orderRunDag()`,
25
+ `runDagAncestors()`, `MAX_RUN_DAG_TASKS` and `MAX_RUN_TASK_ATTEMPTS`. Pure graph
26
+ ordering/closure does not schedule or authorize a process.
27
+ - `RunTaskRetryCommand` / `parseRunTaskRetryCommand()` bind a failed/interrupted task
28
+ selection to the original input and exact revision/generation. Empty selections
29
+ request pending-only continuation, not permission to omit a failed dependency.
15
30
  - `evaluateTask()` and `reduceRuntimeDecision()`
16
31
  - Claim Closure Graph v1: `evaluateProofClosure()`, `validateClaimGraph()`,
17
32
  `minimalBlockingCut()` and the readonly claim/observation/waiver vocabulary
@@ -21,6 +36,35 @@ trust floors, witness groups, expiry, workspace completeness and unresolved-effe
21
36
  boundaries; those supplied facts remain caller trust boundaries. It does not attest a
22
37
  runner or automatically decide an ordinary chat turn.
23
38
 
39
+ ## Claim closure and blocking explanations
40
+
41
+ `evaluateProofClosure()` evaluates qualified witnesses on an all/any claim DAG.
42
+ Set `witnessIndependence: "explicit-groups"` for a strict profile: multi-witness
43
+ claims (`requiredWitnesses > 1`) then require explicit, nonempty `independenceGroup`
44
+ values. Merely changing an observation ID does not create another independent
45
+ witness in that profile. Single-witness claims keep their existing behavior.
46
+
47
+ The compatibility default remains `legacy-observation-id`; the result reports the
48
+ resolved policy. Existing callers must opt into the strict profile deliberately.
49
+ Group identities still need binding to real evidence origins by a trusted caller;
50
+ the reducer cannot authenticate an arbitrary caller-supplied label.
51
+
52
+ `explainBlockingCut()` returns a bounded antichain repair explanation. Shared DAG
53
+ branches can share one repair. Composite-local counterexamples and scope obligations
54
+ are retained alongside child obligations rather than being hidden by them.
55
+
56
+ - `blockingCut.optimality: "minimum"` means minimum cardinality in this snapshot's
57
+ repair model, not semantic correctness after a real change.
58
+ - At the candidate/operation limit, `algorithm: "greedy"`, `truncated: true`, and
59
+ `optimality: "not-proven"` identify a fallback with no minimality guarantee.
60
+ - The legacy `minimalBlockingCut` array remains as a compatibility projection.
61
+ Consumers needing optimality must read the explanation metadata. Older results
62
+ without that metadata do not establish a minimum.
63
+
64
+ Repair explanations never close claims, reconcile effects, issue waivers, or authorize
65
+ merges. Re-evaluate evidence after a change. Set operations depend on candidate and
66
+ set sizes; the shared-DAG optimization is not a linear-time minimum-cut algorithm.
67
+
24
68
  The package does not execute tools, persist records, schedule work, choose providers, or infer topology. Retry and failover counts are derived from attempt records rather than stored counters.
25
69
 
26
70
  See [OMK Run Protocol v1](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/run-protocol.md) for evaluation rules, receipt bridging, migration status, and authority boundaries.
@@ -1,17 +1,10 @@
1
- /**
2
- * Minimal blocking cut: the smallest set of leaf claims whose closure would
3
- * unblock every blocking root, so a user sees "fix these two" instead of a
4
- * dump of every intermediate claim on the path.
5
- *
6
- * The graph is an all/any DAG, so the cut is computed by one memoized pass:
7
- * an `all` node needs every blocking required child's cut, an `any` node
8
- * needs only the cheapest child's cut (ties broken by claim id, so the
9
- * explanation is stable across runs), and a node that is blocked without any
10
- * blocking required child — a composite with its own counterexample — is its
11
- * own cut. Advisory children never enter a cut: they cannot block a parent.
12
- * Bounded by O(V + E); no general minimum-hitting-set search.
13
- */
14
- import type { ClaimClosureEvaluation, ClaimNode, ClaimVerdict } from "./claim-types.ts";
1
+ import { type BlockingCutExplanation, type ClaimClosureEvaluation, type ClaimNode, type ClaimVerdict } from "./claim-types.ts";
2
+ export declare const MAX_BLOCKING_CUT_CANDIDATES = 128;
3
+ type Cut = readonly string[];
15
4
  export declare function isBlockingVerdict(verdict: ClaimVerdict): boolean;
16
- export declare function minimalBlockingCut(claims: ReadonlyMap<string, ClaimNode>, evaluations: ReadonlyMap<string, ClaimClosureEvaluation>, rootIds: readonly string[]): readonly string[];
5
+ /** Prefer this API over the legacy array-only projection when optimality matters. */
6
+ export declare function explainBlockingCut(claims: ReadonlyMap<string, ClaimNode>, evaluations: ReadonlyMap<string, ClaimClosureEvaluation>, rootIds: readonly string[]): BlockingCutExplanation;
7
+ /** Compatibility name: on a bounded-search fallback this array is not guaranteed minimal. */
8
+ export declare function minimalBlockingCut(claims: ReadonlyMap<string, ClaimNode>, evaluations: ReadonlyMap<string, ClaimClosureEvaluation>, rootIds: readonly string[]): Cut;
9
+ export {};
17
10
  //# sourceMappingURL=claim-blocking-cut.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"claim-blocking-cut.d.ts","sourceRoot":"","sources":["../../src/claims/claim-blocking-cut.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAUxF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAEhE;AAoBD,wBAAgB,kBAAkB,CACjC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,EACtC,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,sBAAsB,CAAC,EACxD,OAAO,EAAE,SAAS,MAAM,EAAE,GACxB,SAAS,MAAM,EAAE,CAqBnB","sourcesContent":["/**\n * Minimal blocking cut: the smallest set of leaf claims whose closure would\n * unblock every blocking root, so a user sees \"fix these two\" instead of a\n * dump of every intermediate claim on the path.\n *\n * The graph is an all/any DAG, so the cut is computed by one memoized pass:\n * an `all` node needs every blocking required child's cut, an `any` node\n * needs only the cheapest child's cut (ties broken by claim id, so the\n * explanation is stable across runs), and a node that is blocked without any\n * blocking required child — a composite with its own counterexample — is its\n * own cut. Advisory children never enter a cut: they cannot block a parent.\n * Bounded by O(V + E); no general minimum-hitting-set search.\n */\n\nimport type { ClaimClosureEvaluation, ClaimNode, ClaimVerdict } from \"./claim-types.ts\";\n\nconst BLOCKING_VERDICTS: readonly ClaimVerdict[] = [\n\t\"violated\",\n\t\"stale\",\n\t\"incomplete_scope\",\n\t\"insufficient_trust\",\n\t\"missing\",\n];\n\nexport function isBlockingVerdict(verdict: ClaimVerdict): boolean {\n\treturn BLOCKING_VERDICTS.includes(verdict);\n}\n\nfunction compareCodeUnits(left: string, right: string): number {\n\tif (left === right) return 0;\n\treturn left < right ? -1 : 1;\n}\n\nfunction compareCuts(left: readonly string[], right: readonly string[]): number {\n\tif (left.length !== right.length) return left.length - right.length;\n\tfor (let index = 0; index < left.length; index++) {\n\t\tconst order = compareCodeUnits(left[index], right[index]);\n\t\tif (order !== 0) return order;\n\t}\n\treturn 0;\n}\n\nfunction unionSorted(sets: readonly (readonly string[])[]): string[] {\n\treturn [...new Set(sets.flat())].sort(compareCodeUnits);\n}\n\nexport function minimalBlockingCut(\n\tclaims: ReadonlyMap<string, ClaimNode>,\n\tevaluations: ReadonlyMap<string, ClaimClosureEvaluation>,\n\trootIds: readonly string[],\n): readonly string[] {\n\tconst memo = new Map<string, readonly string[]>();\n\tconst cutFor = (claimId: string): readonly string[] => {\n\t\tconst cached = memo.get(claimId);\n\t\tif (cached !== undefined) return cached;\n\t\tconst evaluation = evaluations.get(claimId);\n\t\tconst claim = claims.get(claimId);\n\t\tlet cut: readonly string[] = [];\n\t\tif (claim !== undefined && evaluation !== undefined && isBlockingVerdict(evaluation.verdict)) {\n\t\t\tconst childCuts = claim.satisfaction.inputs\n\t\t\t\t.filter((input) => claims.get(input)?.severity === \"required\")\n\t\t\t\t.map(cutFor)\n\t\t\t\t.filter((childCut) => childCut.length > 0);\n\t\t\tif (childCuts.length === 0) cut = [claimId];\n\t\t\telse if (claim.satisfaction.rule === \"all\") cut = unionSorted(childCuts);\n\t\t\telse cut = [...childCuts].sort(compareCuts)[0];\n\t\t}\n\t\tmemo.set(claimId, cut);\n\t\treturn cut;\n\t};\n\treturn unionSorted(rootIds.map(cutFor));\n}\n"]}
1
+ {"version":3,"file":"claim-blocking-cut.d.ts","sourceRoot":"","sources":["../../src/claims/claim-blocking-cut.ts"],"names":[],"mappings":"AAQA,OAAO,EACN,KAAK,sBAAsB,EAE3B,KAAK,sBAAsB,EAC3B,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,MAAM,kBAAkB,CAAC;AAS1B,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAG/C,KAAK,GAAG,GAAG,SAAS,MAAM,EAAE,CAAC;AAc7B,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAEhE;AAyID,qFAAqF;AACrF,wBAAgB,kBAAkB,CACjC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,EACtC,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,sBAAsB,CAAC,EACxD,OAAO,EAAE,SAAS,MAAM,EAAE,GACxB,sBAAsB,CA2BxB;AAED,6FAA6F;AAC7F,wBAAgB,kBAAkB,CACjC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,EACtC,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,sBAAsB,CAAC,EACxD,OAAO,EAAE,SAAS,MAAM,EAAE,GACxB,GAAG,CAEL","sourcesContent":["/**\n * Bounded blocking explanations for a validated all/any claim DAG.\n * Shared subgraphs require alternative repair sets, not a local cheapest branch.\n * Exact antichains can grow exponentially; bounds fall back to a deterministic,\n * not-proven explanation. Set-operation cost also depends on output-set size.\n * A repair is an explanatory obligation, never permission to satisfy a claim.\n */\nimport { ClaimGraphError, topologicalClaimOrder } from \"./claim-graph.ts\";\nimport {\n\ttype BlockingCutExplanation,\n\tCLAIM_GRAPH_SCHEMA_VERSION,\n\ttype ClaimClosureEvaluation,\n\ttype ClaimNode,\n\ttype ClaimVerdict,\n} from \"./claim-types.ts\";\n\nconst BLOCKING_VERDICTS: readonly ClaimVerdict[] = [\n\t\"violated\",\n\t\"stale\",\n\t\"incomplete_scope\",\n\t\"insufficient_trust\",\n\t\"missing\",\n];\nexport const MAX_BLOCKING_CUT_CANDIDATES = 128;\nconst MAX_CUT_OPERATIONS = 65536;\nclass CutSearchLimit extends Error {}\ntype Cut = readonly string[];\ntype Family = readonly Cut[];\ninterface CutNode {\n\treadonly id: string;\n\treadonly rule: \"all\" | \"any\";\n\treadonly inputs: readonly string[];\n\treadonly closed: boolean;\n\treadonly local: boolean;\n}\n/** Search-owned mutable accounting, including work before a limit is reached. */\ninterface CutSearchBudget {\n\toperations: number;\n}\n\nexport function isBlockingVerdict(verdict: ClaimVerdict): boolean {\n\treturn BLOCKING_VERDICTS.includes(verdict);\n}\nfunction compareIds(left: string, right: string): number {\n\tif (left === right) return 0;\n\treturn left < right ? -1 : 1;\n}\nfunction compareCuts(left: Cut, right: Cut): number {\n\tif (left.length !== right.length) return left.length - right.length;\n\tfor (let i = 0; i < left.length; i++) {\n\t\tconst order = compareIds(left[i], right[i]);\n\t\tif (order) return order;\n\t}\n\treturn 0;\n}\nfunction union(sets: readonly Cut[]): Cut {\n\treturn [...new Set(sets.flat())].sort(compareIds);\n}\nfunction* unions(left: Family, right: Family): Generator<Cut> {\n\tfor (const a of left) for (const b of right) yield union([a, b]);\n}\nfunction* alternatives(families: readonly Family[]): Generator<Cut> {\n\tfor (const family of families) yield* family;\n}\n\nfunction cutNodes(\n\tclaims: ReadonlyMap<string, ClaimNode>,\n\tevaluations: ReadonlyMap<string, ClaimClosureEvaluation>,\n): CutNode[] {\n\tconst ordered = topologicalClaimOrder(\n\t\t{ schemaVersion: CLAIM_GRAPH_SCHEMA_VERSION, claims: [...claims.values()] },\n\t\tclaims,\n\t);\n\treturn ordered.map((claim) => {\n\t\tconst evaluation = evaluations.get(claim.claimId);\n\t\tif (\n\t\t\t!evaluation ||\n\t\t\t(!isBlockingVerdict(evaluation.verdict) &&\n\t\t\t\tevaluation.verdict !== \"satisfied\" &&\n\t\t\t\tevaluation.verdict !== \"waived\")\n\t\t) {\n\t\t\tthrow new ClaimGraphError(\"invalid_input\", \"Blocking explanation requires a valid evaluation for every claim\");\n\t\t}\n\t\tconst inputs = claim.satisfaction.inputs.filter((id) => claims.get(id)?.severity === \"required\").sort(compareIds);\n\t\tconst inferredLocal =\n\t\t\t(evaluation.verdict === \"violated\" && evaluation.observationIds.length > 0) ||\n\t\t\t(evaluation.verdict === \"incomplete_scope\" && claim.scopeSensitive === true);\n\t\treturn {\n\t\t\tid: claim.claimId,\n\t\t\trule: claim.satisfaction.rule,\n\t\t\tinputs,\n\t\t\tclosed: !isBlockingVerdict(evaluation.verdict),\n\t\t\tlocal:\n\t\t\t\tinputs.length === 0 ||\n\t\t\t\tevaluation.blockingOrigin === \"local\" ||\n\t\t\t\t(evaluation.blockingOrigin === undefined && inferredLocal),\n\t\t};\n\t});\n}\nfunction reachableNodes(ordered: CutNode[], roots: readonly string[]): CutNode[] {\n\tconst byId = new Map(ordered.map((node) => [node.id, node]));\n\tconst reachable = new Set<string>();\n\tconst pending = [...roots];\n\twhile (pending.length) {\n\t\tconst id = pending.pop();\n\t\tif (id === undefined || reachable.has(id)) continue;\n\t\treachable.add(id);\n\t\tconst node = byId.get(id);\n\t\tif (node && !node.closed) pending.push(...node.inputs);\n\t}\n\treturn ordered.filter((node) => reachable.has(node.id));\n}\nfunction step(budget: CutSearchBudget): void {\n\tif (budget.operations >= MAX_CUT_OPERATIONS) throw new CutSearchLimit();\n\tbudget.operations++;\n}\nfunction prune(candidates: Iterable<Cut>, budget: CutSearchBudget): Family {\n\tlet kept: Cut[] = [];\n\tfor (const candidate of candidates) {\n\t\tstep(budget);\n\t\tconst set = new Set(candidate);\n\t\tif (\n\t\t\tkept.some((other) => {\n\t\t\t\tstep(budget);\n\t\t\t\treturn other.every((id) => set.has(id));\n\t\t\t})\n\t\t)\n\t\t\tcontinue;\n\t\tkept = kept.filter((other) => {\n\t\t\tstep(budget);\n\t\t\tconst existing = new Set(other);\n\t\t\treturn !candidate.every((id) => existing.has(id));\n\t\t});\n\t\tkept.push(candidate);\n\t\tif (kept.length > MAX_BLOCKING_CUT_CANDIDATES) throw new CutSearchLimit();\n\t}\n\treturn kept.sort(compareCuts);\n}\nfunction combineAll(families: readonly Family[], budget: CutSearchBudget): Family {\n\tlet result: Family = [[]];\n\tfor (const family of families) result = prune(unions(result, family), budget);\n\treturn result;\n}\nfunction exactCut(nodes: readonly CutNode[], roots: readonly string[], budget: CutSearchBudget): Cut {\n\tconst families = new Map<string, Family>();\n\tfor (const node of nodes) {\n\t\tif (node.closed) {\n\t\t\tfamilies.set(node.id, [[]]);\n\t\t\tcontinue;\n\t\t}\n\t\tconst children = node.inputs.map((id) => families.get(id) ?? []);\n\t\tlet family: Family = [[]];\n\t\tif (children.length) {\n\t\t\tfamily = node.rule === \"all\" ? combineAll(children, budget) : prune(alternatives(children), budget);\n\t\t}\n\t\tif (node.local) family = prune(unions(family, [[node.id]]), budget);\n\t\tfamilies.set(node.id, family);\n\t}\n\treturn (\n\t\tcombineAll(\n\t\t\troots.map((id) => families.get(id) ?? []),\n\t\t\tbudget,\n\t\t)[0] ?? []\n\t);\n}\nfunction greedyCut(nodes: readonly CutNode[], roots: readonly string[]): Cut {\n\tconst cuts = new Map<string, Cut>();\n\tfor (const node of nodes) {\n\t\tif (node.closed) {\n\t\t\tcuts.set(node.id, []);\n\t\t\tcontinue;\n\t\t}\n\t\tconst children = node.inputs.map((id) => cuts.get(id) ?? []);\n\t\tconst childCut = node.rule === \"all\" ? union(children) : ([...children].sort(compareCuts)[0] ?? []);\n\t\tcuts.set(node.id, node.local ? union([childCut, [node.id]]) : childCut);\n\t}\n\treturn union(roots.map((id) => cuts.get(id) ?? []));\n}\n\n/** Prefer this API over the legacy array-only projection when optimality matters. */\nexport function explainBlockingCut(\n\tclaims: ReadonlyMap<string, ClaimNode>,\n\tevaluations: ReadonlyMap<string, ClaimClosureEvaluation>,\n\trootIds: readonly string[],\n): BlockingCutExplanation {\n\tconst roots = [...new Set(rootIds)].sort(compareIds);\n\tfor (const id of roots) if (!claims.has(id)) throw new ClaimGraphError(\"unknown_input\", \"Unknown blocking root\");\n\tconst nodes = reachableNodes(cutNodes(claims, evaluations), roots);\n\tconst budget: CutSearchBudget = { operations: 0 };\n\tlet claimIds: Cut;\n\tlet truncated = false;\n\ttry {\n\t\tclaimIds = exactCut(nodes, roots, budget);\n\t} catch (error) {\n\t\tif (!(error instanceof CutSearchLimit)) throw error;\n\t\ttruncated = true;\n\t\tclaimIds = greedyCut(nodes, roots);\n\t}\n\tconst selected = new Set(claimIds);\n\treturn Object.freeze({\n\t\tclaimIds: Object.freeze([...claimIds]),\n\t\talgorithm: truncated ? \"greedy\" : \"exact-antichain\",\n\t\toptimality: truncated ? \"not-proven\" : \"minimum\",\n\t\ttruncated,\n\t\texploredStates: budget.operations,\n\t\tlocalClaimIds: Object.freeze(\n\t\t\tnodes\n\t\t\t\t.flatMap((node) => (node.local && node.inputs.length > 0 && selected.has(node.id) ? [node.id] : []))\n\t\t\t\t.sort(compareIds),\n\t\t),\n\t});\n}\n\n/** Compatibility name: on a bounded-search fallback this array is not guaranteed minimal. */\nexport function minimalBlockingCut(\n\tclaims: ReadonlyMap<string, ClaimNode>,\n\tevaluations: ReadonlyMap<string, ClaimClosureEvaluation>,\n\trootIds: readonly string[],\n): Cut {\n\treturn explainBlockingCut(claims, evaluations, rootIds).claimIds;\n}\n"]}
@@ -1,16 +1,12 @@
1
1
  /**
2
- * Minimal blocking cut: the smallest set of leaf claims whose closure would
3
- * unblock every blocking root, so a user sees "fix these two" instead of a
4
- * dump of every intermediate claim on the path.
5
- *
6
- * The graph is an all/any DAG, so the cut is computed by one memoized pass:
7
- * an `all` node needs every blocking required child's cut, an `any` node
8
- * needs only the cheapest child's cut (ties broken by claim id, so the
9
- * explanation is stable across runs), and a node that is blocked without any
10
- * blocking required child — a composite with its own counterexample — is its
11
- * own cut. Advisory children never enter a cut: they cannot block a parent.
12
- * Bounded by O(V + E); no general minimum-hitting-set search.
2
+ * Bounded blocking explanations for a validated all/any claim DAG.
3
+ * Shared subgraphs require alternative repair sets, not a local cheapest branch.
4
+ * Exact antichains can grow exponentially; bounds fall back to a deterministic,
5
+ * not-proven explanation. Set-operation cost also depends on output-set size.
6
+ * A repair is an explanatory obligation, never permission to satisfy a claim.
13
7
  */
8
+ import { ClaimGraphError, topologicalClaimOrder } from "./claim-graph.js";
9
+ import { CLAIM_GRAPH_SCHEMA_VERSION, } from "./claim-types.js";
14
10
  const BLOCKING_VERDICTS = [
15
11
  "violated",
16
12
  "stale",
@@ -18,10 +14,14 @@ const BLOCKING_VERDICTS = [
18
14
  "insufficient_trust",
19
15
  "missing",
20
16
  ];
17
+ export const MAX_BLOCKING_CUT_CANDIDATES = 128;
18
+ const MAX_CUT_OPERATIONS = 65536;
19
+ class CutSearchLimit extends Error {
20
+ }
21
21
  export function isBlockingVerdict(verdict) {
22
22
  return BLOCKING_VERDICTS.includes(verdict);
23
23
  }
24
- function compareCodeUnits(left, right) {
24
+ function compareIds(left, right) {
25
25
  if (left === right)
26
26
  return 0;
27
27
  return left < right ? -1 : 1;
@@ -29,40 +29,160 @@ function compareCodeUnits(left, right) {
29
29
  function compareCuts(left, right) {
30
30
  if (left.length !== right.length)
31
31
  return left.length - right.length;
32
- for (let index = 0; index < left.length; index++) {
33
- const order = compareCodeUnits(left[index], right[index]);
34
- if (order !== 0)
32
+ for (let i = 0; i < left.length; i++) {
33
+ const order = compareIds(left[i], right[i]);
34
+ if (order)
35
35
  return order;
36
36
  }
37
37
  return 0;
38
38
  }
39
- function unionSorted(sets) {
40
- return [...new Set(sets.flat())].sort(compareCodeUnits);
39
+ function union(sets) {
40
+ return [...new Set(sets.flat())].sort(compareIds);
41
41
  }
42
- export function minimalBlockingCut(claims, evaluations, rootIds) {
43
- const memo = new Map();
44
- const cutFor = (claimId) => {
45
- const cached = memo.get(claimId);
46
- if (cached !== undefined)
47
- return cached;
48
- const evaluation = evaluations.get(claimId);
49
- const claim = claims.get(claimId);
50
- let cut = [];
51
- if (claim !== undefined && evaluation !== undefined && isBlockingVerdict(evaluation.verdict)) {
52
- const childCuts = claim.satisfaction.inputs
53
- .filter((input) => claims.get(input)?.severity === "required")
54
- .map(cutFor)
55
- .filter((childCut) => childCut.length > 0);
56
- if (childCuts.length === 0)
57
- cut = [claimId];
58
- else if (claim.satisfaction.rule === "all")
59
- cut = unionSorted(childCuts);
60
- else
61
- cut = [...childCuts].sort(compareCuts)[0];
42
+ function* unions(left, right) {
43
+ for (const a of left)
44
+ for (const b of right)
45
+ yield union([a, b]);
46
+ }
47
+ function* alternatives(families) {
48
+ for (const family of families)
49
+ yield* family;
50
+ }
51
+ function cutNodes(claims, evaluations) {
52
+ const ordered = topologicalClaimOrder({ schemaVersion: CLAIM_GRAPH_SCHEMA_VERSION, claims: [...claims.values()] }, claims);
53
+ return ordered.map((claim) => {
54
+ const evaluation = evaluations.get(claim.claimId);
55
+ if (!evaluation ||
56
+ (!isBlockingVerdict(evaluation.verdict) &&
57
+ evaluation.verdict !== "satisfied" &&
58
+ evaluation.verdict !== "waived")) {
59
+ throw new ClaimGraphError("invalid_input", "Blocking explanation requires a valid evaluation for every claim");
60
+ }
61
+ const inputs = claim.satisfaction.inputs.filter((id) => claims.get(id)?.severity === "required").sort(compareIds);
62
+ const inferredLocal = (evaluation.verdict === "violated" && evaluation.observationIds.length > 0) ||
63
+ (evaluation.verdict === "incomplete_scope" && claim.scopeSensitive === true);
64
+ return {
65
+ id: claim.claimId,
66
+ rule: claim.satisfaction.rule,
67
+ inputs,
68
+ closed: !isBlockingVerdict(evaluation.verdict),
69
+ local: inputs.length === 0 ||
70
+ evaluation.blockingOrigin === "local" ||
71
+ (evaluation.blockingOrigin === undefined && inferredLocal),
72
+ };
73
+ });
74
+ }
75
+ function reachableNodes(ordered, roots) {
76
+ const byId = new Map(ordered.map((node) => [node.id, node]));
77
+ const reachable = new Set();
78
+ const pending = [...roots];
79
+ while (pending.length) {
80
+ const id = pending.pop();
81
+ if (id === undefined || reachable.has(id))
82
+ continue;
83
+ reachable.add(id);
84
+ const node = byId.get(id);
85
+ if (node && !node.closed)
86
+ pending.push(...node.inputs);
87
+ }
88
+ return ordered.filter((node) => reachable.has(node.id));
89
+ }
90
+ function step(budget) {
91
+ if (budget.operations >= MAX_CUT_OPERATIONS)
92
+ throw new CutSearchLimit();
93
+ budget.operations++;
94
+ }
95
+ function prune(candidates, budget) {
96
+ let kept = [];
97
+ for (const candidate of candidates) {
98
+ step(budget);
99
+ const set = new Set(candidate);
100
+ if (kept.some((other) => {
101
+ step(budget);
102
+ return other.every((id) => set.has(id));
103
+ }))
104
+ continue;
105
+ kept = kept.filter((other) => {
106
+ step(budget);
107
+ const existing = new Set(other);
108
+ return !candidate.every((id) => existing.has(id));
109
+ });
110
+ kept.push(candidate);
111
+ if (kept.length > MAX_BLOCKING_CUT_CANDIDATES)
112
+ throw new CutSearchLimit();
113
+ }
114
+ return kept.sort(compareCuts);
115
+ }
116
+ function combineAll(families, budget) {
117
+ let result = [[]];
118
+ for (const family of families)
119
+ result = prune(unions(result, family), budget);
120
+ return result;
121
+ }
122
+ function exactCut(nodes, roots, budget) {
123
+ const families = new Map();
124
+ for (const node of nodes) {
125
+ if (node.closed) {
126
+ families.set(node.id, [[]]);
127
+ continue;
128
+ }
129
+ const children = node.inputs.map((id) => families.get(id) ?? []);
130
+ let family = [[]];
131
+ if (children.length) {
132
+ family = node.rule === "all" ? combineAll(children, budget) : prune(alternatives(children), budget);
133
+ }
134
+ if (node.local)
135
+ family = prune(unions(family, [[node.id]]), budget);
136
+ families.set(node.id, family);
137
+ }
138
+ return (combineAll(roots.map((id) => families.get(id) ?? []), budget)[0] ?? []);
139
+ }
140
+ function greedyCut(nodes, roots) {
141
+ const cuts = new Map();
142
+ for (const node of nodes) {
143
+ if (node.closed) {
144
+ cuts.set(node.id, []);
145
+ continue;
62
146
  }
63
- memo.set(claimId, cut);
64
- return cut;
65
- };
66
- return unionSorted(rootIds.map(cutFor));
147
+ const children = node.inputs.map((id) => cuts.get(id) ?? []);
148
+ const childCut = node.rule === "all" ? union(children) : ([...children].sort(compareCuts)[0] ?? []);
149
+ cuts.set(node.id, node.local ? union([childCut, [node.id]]) : childCut);
150
+ }
151
+ return union(roots.map((id) => cuts.get(id) ?? []));
152
+ }
153
+ /** Prefer this API over the legacy array-only projection when optimality matters. */
154
+ export function explainBlockingCut(claims, evaluations, rootIds) {
155
+ const roots = [...new Set(rootIds)].sort(compareIds);
156
+ for (const id of roots)
157
+ if (!claims.has(id))
158
+ throw new ClaimGraphError("unknown_input", "Unknown blocking root");
159
+ const nodes = reachableNodes(cutNodes(claims, evaluations), roots);
160
+ const budget = { operations: 0 };
161
+ let claimIds;
162
+ let truncated = false;
163
+ try {
164
+ claimIds = exactCut(nodes, roots, budget);
165
+ }
166
+ catch (error) {
167
+ if (!(error instanceof CutSearchLimit))
168
+ throw error;
169
+ truncated = true;
170
+ claimIds = greedyCut(nodes, roots);
171
+ }
172
+ const selected = new Set(claimIds);
173
+ return Object.freeze({
174
+ claimIds: Object.freeze([...claimIds]),
175
+ algorithm: truncated ? "greedy" : "exact-antichain",
176
+ optimality: truncated ? "not-proven" : "minimum",
177
+ truncated,
178
+ exploredStates: budget.operations,
179
+ localClaimIds: Object.freeze(nodes
180
+ .flatMap((node) => (node.local && node.inputs.length > 0 && selected.has(node.id) ? [node.id] : []))
181
+ .sort(compareIds)),
182
+ });
183
+ }
184
+ /** Compatibility name: on a bounded-search fallback this array is not guaranteed minimal. */
185
+ export function minimalBlockingCut(claims, evaluations, rootIds) {
186
+ return explainBlockingCut(claims, evaluations, rootIds).claimIds;
67
187
  }
68
188
  //# sourceMappingURL=claim-blocking-cut.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"claim-blocking-cut.js","sourceRoot":"","sources":["../../src/claims/claim-blocking-cut.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal blocking cut: the smallest set of leaf claims whose closure would\n * unblock every blocking root, so a user sees \"fix these two\" instead of a\n * dump of every intermediate claim on the path.\n *\n * The graph is an all/any DAG, so the cut is computed by one memoized pass:\n * an `all` node needs every blocking required child's cut, an `any` node\n * needs only the cheapest child's cut (ties broken by claim id, so the\n * explanation is stable across runs), and a node that is blocked without any\n * blocking required child — a composite with its own counterexample — is its\n * own cut. Advisory children never enter a cut: they cannot block a parent.\n * Bounded by O(V + E); no general minimum-hitting-set search.\n */\n\nimport type { ClaimClosureEvaluation, ClaimNode, ClaimVerdict } from \"./claim-types.ts\";\n\nconst BLOCKING_VERDICTS: readonly ClaimVerdict[] = [\n\t\"violated\",\n\t\"stale\",\n\t\"incomplete_scope\",\n\t\"insufficient_trust\",\n\t\"missing\",\n];\n\nexport function isBlockingVerdict(verdict: ClaimVerdict): boolean {\n\treturn BLOCKING_VERDICTS.includes(verdict);\n}\n\nfunction compareCodeUnits(left: string, right: string): number {\n\tif (left === right) return 0;\n\treturn left < right ? -1 : 1;\n}\n\nfunction compareCuts(left: readonly string[], right: readonly string[]): number {\n\tif (left.length !== right.length) return left.length - right.length;\n\tfor (let index = 0; index < left.length; index++) {\n\t\tconst order = compareCodeUnits(left[index], right[index]);\n\t\tif (order !== 0) return order;\n\t}\n\treturn 0;\n}\n\nfunction unionSorted(sets: readonly (readonly string[])[]): string[] {\n\treturn [...new Set(sets.flat())].sort(compareCodeUnits);\n}\n\nexport function minimalBlockingCut(\n\tclaims: ReadonlyMap<string, ClaimNode>,\n\tevaluations: ReadonlyMap<string, ClaimClosureEvaluation>,\n\trootIds: readonly string[],\n): readonly string[] {\n\tconst memo = new Map<string, readonly string[]>();\n\tconst cutFor = (claimId: string): readonly string[] => {\n\t\tconst cached = memo.get(claimId);\n\t\tif (cached !== undefined) return cached;\n\t\tconst evaluation = evaluations.get(claimId);\n\t\tconst claim = claims.get(claimId);\n\t\tlet cut: readonly string[] = [];\n\t\tif (claim !== undefined && evaluation !== undefined && isBlockingVerdict(evaluation.verdict)) {\n\t\t\tconst childCuts = claim.satisfaction.inputs\n\t\t\t\t.filter((input) => claims.get(input)?.severity === \"required\")\n\t\t\t\t.map(cutFor)\n\t\t\t\t.filter((childCut) => childCut.length > 0);\n\t\t\tif (childCuts.length === 0) cut = [claimId];\n\t\t\telse if (claim.satisfaction.rule === \"all\") cut = unionSorted(childCuts);\n\t\t\telse cut = [...childCuts].sort(compareCuts)[0];\n\t\t}\n\t\tmemo.set(claimId, cut);\n\t\treturn cut;\n\t};\n\treturn unionSorted(rootIds.map(cutFor));\n}\n"]}
1
+ {"version":3,"file":"claim-blocking-cut.js","sourceRoot":"","sources":["../../src/claims/claim-blocking-cut.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Bounded blocking explanations for a validated all/any claim DAG.\n * Shared subgraphs require alternative repair sets, not a local cheapest branch.\n * Exact antichains can grow exponentially; bounds fall back to a deterministic,\n * not-proven explanation. Set-operation cost also depends on output-set size.\n * A repair is an explanatory obligation, never permission to satisfy a claim.\n */\nimport { ClaimGraphError, topologicalClaimOrder } from \"./claim-graph.ts\";\nimport {\n\ttype BlockingCutExplanation,\n\tCLAIM_GRAPH_SCHEMA_VERSION,\n\ttype ClaimClosureEvaluation,\n\ttype ClaimNode,\n\ttype ClaimVerdict,\n} from \"./claim-types.ts\";\n\nconst BLOCKING_VERDICTS: readonly ClaimVerdict[] = [\n\t\"violated\",\n\t\"stale\",\n\t\"incomplete_scope\",\n\t\"insufficient_trust\",\n\t\"missing\",\n];\nexport const MAX_BLOCKING_CUT_CANDIDATES = 128;\nconst MAX_CUT_OPERATIONS = 65536;\nclass CutSearchLimit extends Error {}\ntype Cut = readonly string[];\ntype Family = readonly Cut[];\ninterface CutNode {\n\treadonly id: string;\n\treadonly rule: \"all\" | \"any\";\n\treadonly inputs: readonly string[];\n\treadonly closed: boolean;\n\treadonly local: boolean;\n}\n/** Search-owned mutable accounting, including work before a limit is reached. */\ninterface CutSearchBudget {\n\toperations: number;\n}\n\nexport function isBlockingVerdict(verdict: ClaimVerdict): boolean {\n\treturn BLOCKING_VERDICTS.includes(verdict);\n}\nfunction compareIds(left: string, right: string): number {\n\tif (left === right) return 0;\n\treturn left < right ? -1 : 1;\n}\nfunction compareCuts(left: Cut, right: Cut): number {\n\tif (left.length !== right.length) return left.length - right.length;\n\tfor (let i = 0; i < left.length; i++) {\n\t\tconst order = compareIds(left[i], right[i]);\n\t\tif (order) return order;\n\t}\n\treturn 0;\n}\nfunction union(sets: readonly Cut[]): Cut {\n\treturn [...new Set(sets.flat())].sort(compareIds);\n}\nfunction* unions(left: Family, right: Family): Generator<Cut> {\n\tfor (const a of left) for (const b of right) yield union([a, b]);\n}\nfunction* alternatives(families: readonly Family[]): Generator<Cut> {\n\tfor (const family of families) yield* family;\n}\n\nfunction cutNodes(\n\tclaims: ReadonlyMap<string, ClaimNode>,\n\tevaluations: ReadonlyMap<string, ClaimClosureEvaluation>,\n): CutNode[] {\n\tconst ordered = topologicalClaimOrder(\n\t\t{ schemaVersion: CLAIM_GRAPH_SCHEMA_VERSION, claims: [...claims.values()] },\n\t\tclaims,\n\t);\n\treturn ordered.map((claim) => {\n\t\tconst evaluation = evaluations.get(claim.claimId);\n\t\tif (\n\t\t\t!evaluation ||\n\t\t\t(!isBlockingVerdict(evaluation.verdict) &&\n\t\t\t\tevaluation.verdict !== \"satisfied\" &&\n\t\t\t\tevaluation.verdict !== \"waived\")\n\t\t) {\n\t\t\tthrow new ClaimGraphError(\"invalid_input\", \"Blocking explanation requires a valid evaluation for every claim\");\n\t\t}\n\t\tconst inputs = claim.satisfaction.inputs.filter((id) => claims.get(id)?.severity === \"required\").sort(compareIds);\n\t\tconst inferredLocal =\n\t\t\t(evaluation.verdict === \"violated\" && evaluation.observationIds.length > 0) ||\n\t\t\t(evaluation.verdict === \"incomplete_scope\" && claim.scopeSensitive === true);\n\t\treturn {\n\t\t\tid: claim.claimId,\n\t\t\trule: claim.satisfaction.rule,\n\t\t\tinputs,\n\t\t\tclosed: !isBlockingVerdict(evaluation.verdict),\n\t\t\tlocal:\n\t\t\t\tinputs.length === 0 ||\n\t\t\t\tevaluation.blockingOrigin === \"local\" ||\n\t\t\t\t(evaluation.blockingOrigin === undefined && inferredLocal),\n\t\t};\n\t});\n}\nfunction reachableNodes(ordered: CutNode[], roots: readonly string[]): CutNode[] {\n\tconst byId = new Map(ordered.map((node) => [node.id, node]));\n\tconst reachable = new Set<string>();\n\tconst pending = [...roots];\n\twhile (pending.length) {\n\t\tconst id = pending.pop();\n\t\tif (id === undefined || reachable.has(id)) continue;\n\t\treachable.add(id);\n\t\tconst node = byId.get(id);\n\t\tif (node && !node.closed) pending.push(...node.inputs);\n\t}\n\treturn ordered.filter((node) => reachable.has(node.id));\n}\nfunction step(budget: CutSearchBudget): void {\n\tif (budget.operations >= MAX_CUT_OPERATIONS) throw new CutSearchLimit();\n\tbudget.operations++;\n}\nfunction prune(candidates: Iterable<Cut>, budget: CutSearchBudget): Family {\n\tlet kept: Cut[] = [];\n\tfor (const candidate of candidates) {\n\t\tstep(budget);\n\t\tconst set = new Set(candidate);\n\t\tif (\n\t\t\tkept.some((other) => {\n\t\t\t\tstep(budget);\n\t\t\t\treturn other.every((id) => set.has(id));\n\t\t\t})\n\t\t)\n\t\t\tcontinue;\n\t\tkept = kept.filter((other) => {\n\t\t\tstep(budget);\n\t\t\tconst existing = new Set(other);\n\t\t\treturn !candidate.every((id) => existing.has(id));\n\t\t});\n\t\tkept.push(candidate);\n\t\tif (kept.length > MAX_BLOCKING_CUT_CANDIDATES) throw new CutSearchLimit();\n\t}\n\treturn kept.sort(compareCuts);\n}\nfunction combineAll(families: readonly Family[], budget: CutSearchBudget): Family {\n\tlet result: Family = [[]];\n\tfor (const family of families) result = prune(unions(result, family), budget);\n\treturn result;\n}\nfunction exactCut(nodes: readonly CutNode[], roots: readonly string[], budget: CutSearchBudget): Cut {\n\tconst families = new Map<string, Family>();\n\tfor (const node of nodes) {\n\t\tif (node.closed) {\n\t\t\tfamilies.set(node.id, [[]]);\n\t\t\tcontinue;\n\t\t}\n\t\tconst children = node.inputs.map((id) => families.get(id) ?? []);\n\t\tlet family: Family = [[]];\n\t\tif (children.length) {\n\t\t\tfamily = node.rule === \"all\" ? combineAll(children, budget) : prune(alternatives(children), budget);\n\t\t}\n\t\tif (node.local) family = prune(unions(family, [[node.id]]), budget);\n\t\tfamilies.set(node.id, family);\n\t}\n\treturn (\n\t\tcombineAll(\n\t\t\troots.map((id) => families.get(id) ?? []),\n\t\t\tbudget,\n\t\t)[0] ?? []\n\t);\n}\nfunction greedyCut(nodes: readonly CutNode[], roots: readonly string[]): Cut {\n\tconst cuts = new Map<string, Cut>();\n\tfor (const node of nodes) {\n\t\tif (node.closed) {\n\t\t\tcuts.set(node.id, []);\n\t\t\tcontinue;\n\t\t}\n\t\tconst children = node.inputs.map((id) => cuts.get(id) ?? []);\n\t\tconst childCut = node.rule === \"all\" ? union(children) : ([...children].sort(compareCuts)[0] ?? []);\n\t\tcuts.set(node.id, node.local ? union([childCut, [node.id]]) : childCut);\n\t}\n\treturn union(roots.map((id) => cuts.get(id) ?? []));\n}\n\n/** Prefer this API over the legacy array-only projection when optimality matters. */\nexport function explainBlockingCut(\n\tclaims: ReadonlyMap<string, ClaimNode>,\n\tevaluations: ReadonlyMap<string, ClaimClosureEvaluation>,\n\trootIds: readonly string[],\n): BlockingCutExplanation {\n\tconst roots = [...new Set(rootIds)].sort(compareIds);\n\tfor (const id of roots) if (!claims.has(id)) throw new ClaimGraphError(\"unknown_input\", \"Unknown blocking root\");\n\tconst nodes = reachableNodes(cutNodes(claims, evaluations), roots);\n\tconst budget: CutSearchBudget = { operations: 0 };\n\tlet claimIds: Cut;\n\tlet truncated = false;\n\ttry {\n\t\tclaimIds = exactCut(nodes, roots, budget);\n\t} catch (error) {\n\t\tif (!(error instanceof CutSearchLimit)) throw error;\n\t\ttruncated = true;\n\t\tclaimIds = greedyCut(nodes, roots);\n\t}\n\tconst selected = new Set(claimIds);\n\treturn Object.freeze({\n\t\tclaimIds: Object.freeze([...claimIds]),\n\t\talgorithm: truncated ? \"greedy\" : \"exact-antichain\",\n\t\toptimality: truncated ? \"not-proven\" : \"minimum\",\n\t\ttruncated,\n\t\texploredStates: budget.operations,\n\t\tlocalClaimIds: Object.freeze(\n\t\t\tnodes\n\t\t\t\t.flatMap((node) => (node.local && node.inputs.length > 0 && selected.has(node.id) ? [node.id] : []))\n\t\t\t\t.sort(compareIds),\n\t\t),\n\t});\n}\n\n/** Compatibility name: on a bounded-search fallback this array is not guaranteed minimal. */\nexport function minimalBlockingCut(\n\tclaims: ReadonlyMap<string, ClaimNode>,\n\tevaluations: ReadonlyMap<string, ClaimClosureEvaluation>,\n\trootIds: readonly string[],\n): Cut {\n\treturn explainBlockingCut(claims, evaluations, rootIds).claimIds;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"claim-closure.d.ts","sourceRoot":"","sources":["../../src/claims/claim-closure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,EAQN,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EAGvB,MAAM,kBAAkB,CAAC;AAqJ1B,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,iBAAiB,GAAG,kBAAkB,CAiDjF","sourcesContent":["/**\n * Proof closure evaluation over a claim graph (plan §5.5).\n *\n * Order of operations, each step pure and deterministic:\n *\n * 1. Drop observations bound to another source root or environment.\n * 2. Classify expired observations and observations missing one of a claim's\n * invalidation keys as stale for that claim.\n * 3. A qualified counterexample decides `violated` before any witness counts.\n * 4. Leaf witnesses below the claim's trust floor never count; distinct\n * independence groups must reach `requiredWitnesses`.\n * 5. Composite claims fold their *required* children (`all` = worst child,\n * `any` = best child) in topological order, so a parent is never read\n * before its inputs. Advisory children are reported but never fold into a\n * parent, and a composite with no required children is witnessed like a leaf.\n * 6. A scope-sensitive claim is blocked while the workspace witness is not\n * complete; an unresolved effect blocks the whole closure.\n * 7. A waiver removes an unclosed claim from the required set. It never\n * removes a violated one: a counterexample beats a waiver.\n *\n * The public verdict is decided by the required *roots* (claims nobody lists\n * as an input): `verified` only when every required root is closed, no effect\n * is unresolved, and the workspace witness is complete. A required leaf under\n * an `any` parent therefore blocks nothing once a sibling branch closes — that\n * is what `any` means — while `blockingClaimIds` still lists every required\n * claim on a blocking path so the cause is explainable.\n */\n\nimport { isBlockingVerdict, minimalBlockingCut } from \"./claim-blocking-cut.ts\";\nimport { ClaimGraphError, rootClaimIds, topologicalClaimOrder, validateClaimGraph } from \"./claim-graph.ts\";\nimport {\n\tCLAIM_VERDICT_PRECEDENCE,\n\ttype ClaimClosureEvaluation,\n\ttype ClaimNode,\n\ttype ClaimReasonCode,\n\ttype ClaimVerdict,\n\tOBSERVATION_TRUST_RANK,\n\ttype ObservationNode,\n\ttype ProofClosureInput,\n\ttype ProofClosureResult,\n\ttype VerificationVerdict,\n\ttype WaiverNode,\n} from \"./claim-types.ts\";\n\nconst ISO_TIMESTAMP = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/;\n\ninterface Witnesses {\n\treadonly supporting: ObservationNode[];\n\treadonly violating: ObservationNode[];\n\treadonly stale: ObservationNode[];\n}\n\nfunction compareCodeUnits(left: string, right: string): number {\n\tif (left === right) return 0;\n\treturn left < right ? -1 : 1;\n}\n\nfunction requireTimestamp(value: string, label: string): string {\n\tif (!ISO_TIMESTAMP.test(value))\n\t\tthrow new ClaimGraphError(\"invalid_input\", `${label} must be an ISO-8601 UTC instant`);\n\treturn value;\n}\n\nfunction reasonFor(verdict: ClaimVerdict): ClaimReasonCode {\n\treturn `claim.${verdict}` as ClaimReasonCode;\n}\n\nfunction worst(verdicts: readonly ClaimVerdict[]): ClaimVerdict {\n\treturn [...verdicts].sort((a, b) => CLAIM_VERDICT_PRECEDENCE.indexOf(a) - CLAIM_VERDICT_PRECEDENCE.indexOf(b))[0];\n}\n\nfunction best(verdicts: readonly ClaimVerdict[]): ClaimVerdict {\n\treturn [...verdicts].sort((a, b) => CLAIM_VERDICT_PRECEDENCE.indexOf(b) - CLAIM_VERDICT_PRECEDENCE.indexOf(a))[0];\n}\n\n/** Steps 1–2: bind observations to claims, dropping foreign ones and marking stale ones. */\nfunction indexWitnesses(input: ProofClosureInput, claims: ReadonlyMap<string, ClaimNode>): Map<string, Witnesses> {\n\tconst index = new Map<string, Witnesses>();\n\tfor (const claim of claims.values()) index.set(claim.claimId, { supporting: [], violating: [], stale: [] });\n\tfor (const observation of input.observations) {\n\t\tif (observation.sourceRoot !== input.sourceRoot || observation.environmentDigest !== input.environmentDigest)\n\t\t\tcontinue;\n\t\tconst expired =\n\t\t\tobservation.validUntil !== undefined && requireTimestamp(observation.validUntil, \"validUntil\") <= input.now;\n\t\tconst attested = observation.invalidationKeys ?? [];\n\t\tfor (const claimId of observation.claimIds) {\n\t\t\tconst claim = claims.get(claimId);\n\t\t\tconst witnesses = index.get(claimId);\n\t\t\tif (claim === undefined || witnesses === undefined) continue;\n\t\t\tconst missingKey = claim.invalidationKeys.some((key) => !attested.includes(key));\n\t\t\tif (expired || missingKey) witnesses.stale.push(observation);\n\t\t\telse if (observation.polarity === \"violates\") witnesses.violating.push(observation);\n\t\t\telse witnesses.supporting.push(observation);\n\t\t}\n\t}\n\treturn index;\n}\n\nfunction ids(observations: readonly ObservationNode[]): readonly string[] {\n\treturn observations.map((observation) => observation.observationId).sort(compareCodeUnits);\n}\n\n/** Steps 3–4 for a leaf claim. */\nfunction evaluateLeaf(\n\tclaim: ClaimNode,\n\twitnesses: Witnesses,\n): { verdict: ClaimVerdict; observationIds: readonly string[] } {\n\tif (witnesses.violating.length > 0) return { verdict: \"violated\", observationIds: ids(witnesses.violating) };\n\tconst floor = OBSERVATION_TRUST_RANK[claim.trustFloor];\n\tconst trusted = witnesses.supporting.filter((observation) => OBSERVATION_TRUST_RANK[observation.source] >= floor);\n\tconst groups = new Set(trusted.map((observation) => observation.independenceGroup ?? observation.observationId));\n\tif (groups.size >= (claim.requiredWitnesses ?? 1)) return { verdict: \"satisfied\", observationIds: ids(trusted) };\n\tif (trusted.length > 0) return { verdict: \"missing\", observationIds: ids(trusted) };\n\tif (witnesses.supporting.length > 0)\n\t\treturn { verdict: \"insufficient_trust\", observationIds: ids(witnesses.supporting) };\n\tif (witnesses.stale.length > 0) return { verdict: \"stale\", observationIds: ids(witnesses.stale) };\n\treturn { verdict: \"missing\", observationIds: [] };\n}\n\n/** Inputs that take part in a parent's fold: advisory children are informational only. */\nfunction requiredInputs(claim: ClaimNode, claims: ReadonlyMap<string, ClaimNode>): readonly string[] {\n\treturn claim.satisfaction.inputs.filter((input) => claims.get(input)?.severity === \"required\");\n}\n\nfunction isClosed(verdict: ClaimVerdict): boolean {\n\treturn verdict === \"satisfied\" || verdict === \"waived\";\n}\n\n/** Step 5 for a composite claim; its required children are already evaluated. */\nfunction evaluateComposite(\n\tclaim: ClaimNode,\n\tinputs: readonly string[],\n\twitnesses: Witnesses,\n\tevaluations: ReadonlyMap<string, ClaimClosureEvaluation>,\n): { verdict: ClaimVerdict; observationIds: readonly string[] } {\n\tif (witnesses.violating.length > 0) return { verdict: \"violated\", observationIds: ids(witnesses.violating) };\n\tconst childVerdicts = inputs.map((input) => evaluations.get(input)?.verdict ?? \"missing\");\n\tif (claim.satisfaction.rule === \"all\") {\n\t\tconst open = childVerdicts.filter((verdict) => !isClosed(verdict));\n\t\treturn { verdict: open.length === 0 ? \"satisfied\" : worst(open), observationIds: [] };\n\t}\n\tif (childVerdicts.some(isClosed)) return { verdict: \"satisfied\", observationIds: [] };\n\treturn { verdict: best(childVerdicts), observationIds: [] };\n}\n\n/** Step 7: the lexically first valid waiver for a claim, or none. */\nfunction indexWaivers(input: ProofClosureInput, claims: ReadonlyMap<string, ClaimNode>): Map<string, WaiverNode> {\n\tconst byClaim = new Map<string, WaiverNode>();\n\tfor (const waiver of [...input.waivers].sort((a, b) => compareCodeUnits(a.waiverId, b.waiverId))) {\n\t\tif (!claims.has(waiver.claimId))\n\t\t\tthrow new ClaimGraphError(\"unknown_input\", `${waiver.waiverId} waives unknown ${waiver.claimId}`);\n\t\tif (waiver.sourceRoot !== input.sourceRoot) continue;\n\t\tif (requireTimestamp(waiver.expiresAt, \"expiresAt\") <= input.now) continue;\n\t\tif (!byClaim.has(waiver.claimId)) byClaim.set(waiver.claimId, waiver);\n\t}\n\treturn byClaim;\n}\n\n/** Required claims on a blocking path from a blocking required root, in graph order. */\nfunction blockingPathClaimIds(\n\tclaims: ReadonlyMap<string, ClaimNode>,\n\tevaluations: ReadonlyMap<string, ClaimClosureEvaluation>,\n\tblockingRoots: readonly string[],\n\tgraphOrder: readonly string[],\n): readonly string[] {\n\tconst onPath = new Set<string>();\n\tconst visit = (claimId: string): void => {\n\t\tif (onPath.has(claimId)) return;\n\t\tconst claim = claims.get(claimId);\n\t\tconst verdict = evaluations.get(claimId)?.verdict;\n\t\tif (claim === undefined || verdict === undefined || !isBlockingVerdict(verdict)) return;\n\t\tonPath.add(claimId);\n\t\tfor (const input of requiredInputs(claim, claims)) visit(input);\n\t};\n\tfor (const root of blockingRoots) visit(root);\n\treturn graphOrder.filter((claimId) => onPath.has(claimId));\n}\n\nfunction globalVerdict(\n\trequiredRoots: readonly ClaimClosureEvaluation[],\n\tinput: ProofClosureInput,\n): VerificationVerdict {\n\tif (requiredRoots.length === 0) return \"unverified\";\n\tif (requiredRoots.some((root) => root.verdict === \"violated\")) return \"violated\";\n\tconst closure =\n\t\trequiredRoots.every((root) => isClosed(root.verdict)) &&\n\t\tinput.unresolvedEffectIds.length === 0 &&\n\t\tinput.workspaceCompleteness === \"complete\";\n\treturn closure ? \"verified\" : \"inconclusive\";\n}\n\nexport function evaluateProofClosure(input: ProofClosureInput): ProofClosureResult {\n\tconst claims = validateClaimGraph(input.graph);\n\trequireTimestamp(input.now, \"now\");\n\tconst witnesses = indexWitnesses(input, claims);\n\tconst waivers = indexWaivers(input, claims);\n\tconst evaluations = new Map<string, ClaimClosureEvaluation>();\n\tfor (const claim of topologicalClaimOrder(input.graph, claims)) {\n\t\tconst claimWitnesses = witnesses.get(claim.claimId) as Witnesses;\n\t\tconst inputs = requiredInputs(claim, claims);\n\t\tconst local =\n\t\t\tinputs.length === 0\n\t\t\t\t? evaluateLeaf(claim, claimWitnesses)\n\t\t\t\t: evaluateComposite(claim, inputs, claimWitnesses, evaluations);\n\t\tlet verdict = local.verdict;\n\t\tif (verdict !== \"violated\" && claim.scopeSensitive === true && input.workspaceCompleteness !== \"complete\") {\n\t\t\tverdict = \"incomplete_scope\";\n\t\t}\n\t\tconst waiver = waivers.get(claim.claimId);\n\t\tconst waived = waiver !== undefined && verdict !== \"violated\" && verdict !== \"satisfied\";\n\t\tif (waived) verdict = \"waived\";\n\t\tevaluations.set(\n\t\t\tclaim.claimId,\n\t\t\tObject.freeze({\n\t\t\t\tclaimId: claim.claimId,\n\t\t\t\tseverity: claim.severity,\n\t\t\t\tverdict,\n\t\t\t\treasonCode: reasonFor(verdict),\n\t\t\t\tobservationIds: Object.freeze([...local.observationIds]),\n\t\t\t\t...(waived ? { waiverId: waiver.waiverId } : {}),\n\t\t\t}),\n\t\t);\n\t}\n\tconst graphOrder = input.graph.claims.map((claim) => claim.claimId);\n\tconst ordered = graphOrder.map((claimId) => evaluations.get(claimId) as ClaimClosureEvaluation);\n\tconst requiredRoots = rootClaimIds(input.graph)\n\t\t.map((claimId) => evaluations.get(claimId) as ClaimClosureEvaluation)\n\t\t.filter((evaluation) => evaluation.severity === \"required\");\n\tconst blockingRoots = requiredRoots\n\t\t.filter((evaluation) => isBlockingVerdict(evaluation.verdict))\n\t\t.map((evaluation) => evaluation.claimId);\n\tconst blockingClaimIds = blockingPathClaimIds(claims, evaluations, blockingRoots, graphOrder);\n\treturn Object.freeze({\n\t\tverdict: globalVerdict(requiredRoots, input),\n\t\tclaimEvaluations: Object.freeze(ordered),\n\t\tblockingClaimIds: Object.freeze(blockingClaimIds),\n\t\tminimalBlockingCut: Object.freeze([...minimalBlockingCut(claims, evaluations, blockingRoots)]),\n\t\tunresolvedEffectIds: Object.freeze([...input.unresolvedEffectIds].sort(compareCodeUnits)),\n\t\tworkspaceCompleteness: input.workspaceCompleteness,\n\t});\n}\n"]}
1
+ {"version":3,"file":"claim-closure.d.ts","sourceRoot":"","sources":["../../src/claims/claim-closure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,EAQN,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EAIvB,MAAM,kBAAkB,CAAC;AAmK1B,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,iBAAiB,GAAG,kBAAkB,CAmEjF","sourcesContent":["/**\n * Proof closure evaluation over a claim graph (plan §5.5).\n *\n * Order of operations, each step pure and deterministic:\n *\n * 1. Drop observations bound to another source root or environment.\n * 2. Classify expired observations and observations missing one of a claim's\n * invalidation keys as stale for that claim.\n * 3. A qualified counterexample decides `violated` before any witness counts.\n * 4. Leaf witnesses below the claim's trust floor never count; distinct\n * independence groups must reach `requiredWitnesses`.\n * 5. Composite claims fold their *required* children (`all` = worst child,\n * `any` = best child) in topological order, so a parent is never read\n * before its inputs. Advisory children are reported but never fold into a\n * parent, and a composite with no required children is witnessed like a leaf.\n * 6. A scope-sensitive claim is blocked while the workspace witness is not\n * complete; an unresolved effect blocks the whole closure.\n * 7. A waiver removes an unclosed claim from the required set. It never\n * removes a violated one: a counterexample beats a waiver.\n *\n * The public verdict is decided by the required *roots* (claims nobody lists\n * as an input): `verified` only when every required root is closed, no effect\n * is unresolved, and the workspace witness is complete. A required leaf under\n * an `any` parent therefore blocks nothing once a sibling branch closes — that\n * is what `any` means — while `blockingClaimIds` still lists every required\n * claim on a blocking path so the cause is explainable.\n */\n\nimport { explainBlockingCut, isBlockingVerdict } from \"./claim-blocking-cut.ts\";\nimport { ClaimGraphError, rootClaimIds, topologicalClaimOrder, validateClaimGraph } from \"./claim-graph.ts\";\nimport {\n\tCLAIM_VERDICT_PRECEDENCE,\n\ttype ClaimClosureEvaluation,\n\ttype ClaimNode,\n\ttype ClaimReasonCode,\n\ttype ClaimVerdict,\n\tOBSERVATION_TRUST_RANK,\n\ttype ObservationNode,\n\ttype ProofClosureInput,\n\ttype ProofClosureResult,\n\ttype VerificationVerdict,\n\ttype WaiverNode,\n\ttype WitnessIndependencePolicy,\n} from \"./claim-types.ts\";\n\nconst ISO_TIMESTAMP = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/;\n\ninterface Witnesses {\n\treadonly supporting: ObservationNode[];\n\treadonly violating: ObservationNode[];\n\treadonly stale: ObservationNode[];\n}\n\nfunction compareCodeUnits(left: string, right: string): number {\n\tif (left === right) return 0;\n\treturn left < right ? -1 : 1;\n}\n\nfunction requireTimestamp(value: string, label: string): string {\n\tif (!ISO_TIMESTAMP.test(value))\n\t\tthrow new ClaimGraphError(\"invalid_input\", `${label} must be an ISO-8601 UTC instant`);\n\treturn value;\n}\n\nfunction reasonFor(verdict: ClaimVerdict): ClaimReasonCode {\n\treturn `claim.${verdict}` as ClaimReasonCode;\n}\n\nfunction worst(verdicts: readonly ClaimVerdict[]): ClaimVerdict {\n\treturn [...verdicts].sort((a, b) => CLAIM_VERDICT_PRECEDENCE.indexOf(a) - CLAIM_VERDICT_PRECEDENCE.indexOf(b))[0];\n}\n\nfunction best(verdicts: readonly ClaimVerdict[]): ClaimVerdict {\n\treturn [...verdicts].sort((a, b) => CLAIM_VERDICT_PRECEDENCE.indexOf(b) - CLAIM_VERDICT_PRECEDENCE.indexOf(a))[0];\n}\n\n/** Steps 1–2: bind observations to claims, dropping foreign ones and marking stale ones. */\nfunction indexWitnesses(input: ProofClosureInput, claims: ReadonlyMap<string, ClaimNode>): Map<string, Witnesses> {\n\tconst index = new Map<string, Witnesses>();\n\tfor (const claim of claims.values()) index.set(claim.claimId, { supporting: [], violating: [], stale: [] });\n\tfor (const observation of input.observations) {\n\t\tif (observation.sourceRoot !== input.sourceRoot || observation.environmentDigest !== input.environmentDigest)\n\t\t\tcontinue;\n\t\tconst expired =\n\t\t\tobservation.validUntil !== undefined && requireTimestamp(observation.validUntil, \"validUntil\") <= input.now;\n\t\tconst attested = observation.invalidationKeys ?? [];\n\t\tfor (const claimId of observation.claimIds) {\n\t\t\tconst claim = claims.get(claimId);\n\t\t\tconst witnesses = index.get(claimId);\n\t\t\tif (claim === undefined || witnesses === undefined) continue;\n\t\t\tconst missingKey = claim.invalidationKeys.some((key) => !attested.includes(key));\n\t\t\tif (expired || missingKey) witnesses.stale.push(observation);\n\t\t\telse if (observation.polarity === \"violates\") witnesses.violating.push(observation);\n\t\t\telse witnesses.supporting.push(observation);\n\t\t}\n\t}\n\treturn index;\n}\n\nfunction ids(observations: readonly ObservationNode[]): readonly string[] {\n\treturn observations.map((observation) => observation.observationId).sort(compareCodeUnits);\n}\n\n/** Steps 3–4 for a leaf claim. */\nfunction evaluateLeaf(\n\tclaim: ClaimNode,\n\twitnesses: Witnesses,\n\tindependence: WitnessIndependencePolicy,\n): { verdict: ClaimVerdict; observationIds: readonly string[] } {\n\tif (witnesses.violating.length > 0) return { verdict: \"violated\", observationIds: ids(witnesses.violating) };\n\tconst floor = OBSERVATION_TRUST_RANK[claim.trustFloor];\n\tconst trusted = witnesses.supporting.filter((observation) => OBSERVATION_TRUST_RANK[observation.source] >= floor);\n\tconst requiredWitnesses = claim.requiredWitnesses ?? 1;\n\tconst attributable = trusted.filter(\n\t\t(observation) =>\n\t\t\tindependence === \"legacy-observation-id\" ||\n\t\t\trequiredWitnesses === 1 ||\n\t\t\tBoolean(observation.independenceGroup?.trim()),\n\t);\n\tconst groups = new Set(\n\t\tattributable.map(\n\t\t\t(observation) =>\n\t\t\t\tobservation.independenceGroup ??\n\t\t\t\t(independence === \"legacy-observation-id\" ? observation.observationId : undefined),\n\t\t),\n\t);\n\tif (groups.size >= requiredWitnesses) return { verdict: \"satisfied\", observationIds: ids(trusted) };\n\tif (trusted.length > 0) return { verdict: \"missing\", observationIds: ids(trusted) };\n\tif (witnesses.supporting.length > 0)\n\t\treturn { verdict: \"insufficient_trust\", observationIds: ids(witnesses.supporting) };\n\tif (witnesses.stale.length > 0) return { verdict: \"stale\", observationIds: ids(witnesses.stale) };\n\treturn { verdict: \"missing\", observationIds: [] };\n}\n\n/** Inputs that take part in a parent's fold: advisory children are informational only. */\nfunction requiredInputs(claim: ClaimNode, claims: ReadonlyMap<string, ClaimNode>): readonly string[] {\n\treturn claim.satisfaction.inputs.filter((input) => claims.get(input)?.severity === \"required\");\n}\n\nfunction isClosed(verdict: ClaimVerdict): boolean {\n\treturn verdict === \"satisfied\" || verdict === \"waived\";\n}\n\n/** Step 5 for a composite claim; its required children are already evaluated. */\nfunction evaluateComposite(\n\tclaim: ClaimNode,\n\tinputs: readonly string[],\n\twitnesses: Witnesses,\n\tevaluations: ReadonlyMap<string, ClaimClosureEvaluation>,\n): { verdict: ClaimVerdict; observationIds: readonly string[] } {\n\tif (witnesses.violating.length > 0) return { verdict: \"violated\", observationIds: ids(witnesses.violating) };\n\tconst childVerdicts = inputs.map((input) => evaluations.get(input)?.verdict ?? \"missing\");\n\tif (claim.satisfaction.rule === \"all\") {\n\t\tconst open = childVerdicts.filter((verdict) => !isClosed(verdict));\n\t\treturn { verdict: open.length === 0 ? \"satisfied\" : worst(open), observationIds: [] };\n\t}\n\tif (childVerdicts.some(isClosed)) return { verdict: \"satisfied\", observationIds: [] };\n\treturn { verdict: best(childVerdicts), observationIds: [] };\n}\n\n/** Step 7: the lexically first valid waiver for a claim, or none. */\nfunction indexWaivers(input: ProofClosureInput, claims: ReadonlyMap<string, ClaimNode>): Map<string, WaiverNode> {\n\tconst byClaim = new Map<string, WaiverNode>();\n\tfor (const waiver of [...input.waivers].sort((a, b) => compareCodeUnits(a.waiverId, b.waiverId))) {\n\t\tif (!claims.has(waiver.claimId))\n\t\t\tthrow new ClaimGraphError(\"unknown_input\", `${waiver.waiverId} waives unknown ${waiver.claimId}`);\n\t\tif (waiver.sourceRoot !== input.sourceRoot) continue;\n\t\tif (requireTimestamp(waiver.expiresAt, \"expiresAt\") <= input.now) continue;\n\t\tif (!byClaim.has(waiver.claimId)) byClaim.set(waiver.claimId, waiver);\n\t}\n\treturn byClaim;\n}\n\n/** Required claims on a blocking path from a blocking required root, in graph order. */\nfunction blockingPathClaimIds(\n\tclaims: ReadonlyMap<string, ClaimNode>,\n\tevaluations: ReadonlyMap<string, ClaimClosureEvaluation>,\n\tblockingRoots: readonly string[],\n\tgraphOrder: readonly string[],\n): readonly string[] {\n\tconst onPath = new Set<string>();\n\tconst visit = (claimId: string): void => {\n\t\tif (onPath.has(claimId)) return;\n\t\tconst claim = claims.get(claimId);\n\t\tconst verdict = evaluations.get(claimId)?.verdict;\n\t\tif (claim === undefined || verdict === undefined || !isBlockingVerdict(verdict)) return;\n\t\tonPath.add(claimId);\n\t\tfor (const input of requiredInputs(claim, claims)) visit(input);\n\t};\n\tfor (const root of blockingRoots) visit(root);\n\treturn graphOrder.filter((claimId) => onPath.has(claimId));\n}\n\nfunction globalVerdict(\n\trequiredRoots: readonly ClaimClosureEvaluation[],\n\tinput: ProofClosureInput,\n): VerificationVerdict {\n\tif (requiredRoots.length === 0) return \"unverified\";\n\tif (requiredRoots.some((root) => root.verdict === \"violated\")) return \"violated\";\n\tconst closure =\n\t\trequiredRoots.every((root) => isClosed(root.verdict)) &&\n\t\tinput.unresolvedEffectIds.length === 0 &&\n\t\tinput.workspaceCompleteness === \"complete\";\n\treturn closure ? \"verified\" : \"inconclusive\";\n}\n\nexport function evaluateProofClosure(input: ProofClosureInput): ProofClosureResult {\n\tconst claims = validateClaimGraph(input.graph);\n\tconst witnessIndependence =\n\t\tinput.witnessIndependence === undefined ? \"legacy-observation-id\" : input.witnessIndependence;\n\tif (witnessIndependence !== \"legacy-observation-id\" && witnessIndependence !== \"explicit-groups\") {\n\t\tthrow new ClaimGraphError(\"invalid_input\", \"Unknown witness independence policy\");\n\t}\n\trequireTimestamp(input.now, \"now\");\n\tconst witnesses = indexWitnesses(input, claims);\n\tconst waivers = indexWaivers(input, claims);\n\tconst evaluations = new Map<string, ClaimClosureEvaluation>();\n\tfor (const claim of topologicalClaimOrder(input.graph, claims)) {\n\t\tconst claimWitnesses = witnesses.get(claim.claimId) as Witnesses;\n\t\tconst inputs = requiredInputs(claim, claims);\n\t\tconst local =\n\t\t\tinputs.length === 0\n\t\t\t\t? evaluateLeaf(claim, claimWitnesses, witnessIndependence)\n\t\t\t\t: evaluateComposite(claim, inputs, claimWitnesses, evaluations);\n\t\tlet verdict = local.verdict;\n\t\tif (verdict !== \"violated\" && claim.scopeSensitive === true && input.workspaceCompleteness !== \"complete\") {\n\t\t\tverdict = \"incomplete_scope\";\n\t\t}\n\t\tconst waiver = waivers.get(claim.claimId);\n\t\tconst waived = waiver !== undefined && verdict !== \"violated\" && verdict !== \"satisfied\";\n\t\tif (waived) verdict = \"waived\";\n\t\tevaluations.set(\n\t\t\tclaim.claimId,\n\t\t\tObject.freeze({\n\t\t\t\tclaimId: claim.claimId,\n\t\t\t\tseverity: claim.severity,\n\t\t\t\tverdict,\n\t\t\t\treasonCode: reasonFor(verdict),\n\t\t\t\tobservationIds: Object.freeze([...local.observationIds]),\n\t\t\t\t...(waived ? { waiverId: waiver.waiverId } : {}),\n\t\t\t\t...(isBlockingVerdict(verdict)\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tblockingOrigin:\n\t\t\t\t\t\t\t\tinputs.length === 0 ||\n\t\t\t\t\t\t\t\tclaimWitnesses.violating.length > 0 ||\n\t\t\t\t\t\t\t\t(claim.scopeSensitive === true && input.workspaceCompleteness !== \"complete\")\n\t\t\t\t\t\t\t\t\t? (\"local\" as const)\n\t\t\t\t\t\t\t\t\t: (\"children\" as const),\n\t\t\t\t\t\t}\n\t\t\t\t\t: {}),\n\t\t\t}),\n\t\t);\n\t}\n\tconst graphOrder = input.graph.claims.map((claim) => claim.claimId);\n\tconst ordered = graphOrder.map((claimId) => evaluations.get(claimId) as ClaimClosureEvaluation);\n\tconst requiredRoots = rootClaimIds(input.graph)\n\t\t.map((claimId) => evaluations.get(claimId) as ClaimClosureEvaluation)\n\t\t.filter((evaluation) => evaluation.severity === \"required\");\n\tconst blockingRoots = requiredRoots\n\t\t.filter((evaluation) => isBlockingVerdict(evaluation.verdict))\n\t\t.map((evaluation) => evaluation.claimId);\n\tconst blockingClaimIds = blockingPathClaimIds(claims, evaluations, blockingRoots, graphOrder);\n\tconst blockingCut = explainBlockingCut(claims, evaluations, blockingRoots);\n\treturn Object.freeze({\n\t\tverdict: globalVerdict(requiredRoots, input),\n\t\twitnessIndependence,\n\t\tclaimEvaluations: Object.freeze(ordered),\n\t\tblockingClaimIds: Object.freeze(blockingClaimIds),\n\t\tminimalBlockingCut: blockingCut.claimIds,\n\t\tblockingCut,\n\t\tunresolvedEffectIds: Object.freeze([...input.unresolvedEffectIds].sort(compareCodeUnits)),\n\t\tworkspaceCompleteness: input.workspaceCompleteness,\n\t});\n}\n"]}
@@ -25,7 +25,7 @@
25
25
  * is what `any` means — while `blockingClaimIds` still lists every required
26
26
  * claim on a blocking path so the cause is explainable.
27
27
  */
28
- import { isBlockingVerdict, minimalBlockingCut } from "./claim-blocking-cut.js";
28
+ import { explainBlockingCut, isBlockingVerdict } from "./claim-blocking-cut.js";
29
29
  import { ClaimGraphError, rootClaimIds, topologicalClaimOrder, validateClaimGraph } from "./claim-graph.js";
30
30
  import { CLAIM_VERDICT_PRECEDENCE, OBSERVATION_TRUST_RANK, } from "./claim-types.js";
31
31
  const ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
@@ -78,13 +78,18 @@ function ids(observations) {
78
78
  return observations.map((observation) => observation.observationId).sort(compareCodeUnits);
79
79
  }
80
80
  /** Steps 3–4 for a leaf claim. */
81
- function evaluateLeaf(claim, witnesses) {
81
+ function evaluateLeaf(claim, witnesses, independence) {
82
82
  if (witnesses.violating.length > 0)
83
83
  return { verdict: "violated", observationIds: ids(witnesses.violating) };
84
84
  const floor = OBSERVATION_TRUST_RANK[claim.trustFloor];
85
85
  const trusted = witnesses.supporting.filter((observation) => OBSERVATION_TRUST_RANK[observation.source] >= floor);
86
- const groups = new Set(trusted.map((observation) => observation.independenceGroup ?? observation.observationId));
87
- if (groups.size >= (claim.requiredWitnesses ?? 1))
86
+ const requiredWitnesses = claim.requiredWitnesses ?? 1;
87
+ const attributable = trusted.filter((observation) => independence === "legacy-observation-id" ||
88
+ requiredWitnesses === 1 ||
89
+ Boolean(observation.independenceGroup?.trim()));
90
+ const groups = new Set(attributable.map((observation) => observation.independenceGroup ??
91
+ (independence === "legacy-observation-id" ? observation.observationId : undefined)));
92
+ if (groups.size >= requiredWitnesses)
88
93
  return { verdict: "satisfied", observationIds: ids(trusted) };
89
94
  if (trusted.length > 0)
90
95
  return { verdict: "missing", observationIds: ids(trusted) };
@@ -159,6 +164,10 @@ function globalVerdict(requiredRoots, input) {
159
164
  }
160
165
  export function evaluateProofClosure(input) {
161
166
  const claims = validateClaimGraph(input.graph);
167
+ const witnessIndependence = input.witnessIndependence === undefined ? "legacy-observation-id" : input.witnessIndependence;
168
+ if (witnessIndependence !== "legacy-observation-id" && witnessIndependence !== "explicit-groups") {
169
+ throw new ClaimGraphError("invalid_input", "Unknown witness independence policy");
170
+ }
162
171
  requireTimestamp(input.now, "now");
163
172
  const witnesses = indexWitnesses(input, claims);
164
173
  const waivers = indexWaivers(input, claims);
@@ -167,7 +176,7 @@ export function evaluateProofClosure(input) {
167
176
  const claimWitnesses = witnesses.get(claim.claimId);
168
177
  const inputs = requiredInputs(claim, claims);
169
178
  const local = inputs.length === 0
170
- ? evaluateLeaf(claim, claimWitnesses)
179
+ ? evaluateLeaf(claim, claimWitnesses, witnessIndependence)
171
180
  : evaluateComposite(claim, inputs, claimWitnesses, evaluations);
172
181
  let verdict = local.verdict;
173
182
  if (verdict !== "violated" && claim.scopeSensitive === true && input.workspaceCompleteness !== "complete") {
@@ -184,6 +193,15 @@ export function evaluateProofClosure(input) {
184
193
  reasonCode: reasonFor(verdict),
185
194
  observationIds: Object.freeze([...local.observationIds]),
186
195
  ...(waived ? { waiverId: waiver.waiverId } : {}),
196
+ ...(isBlockingVerdict(verdict)
197
+ ? {
198
+ blockingOrigin: inputs.length === 0 ||
199
+ claimWitnesses.violating.length > 0 ||
200
+ (claim.scopeSensitive === true && input.workspaceCompleteness !== "complete")
201
+ ? "local"
202
+ : "children",
203
+ }
204
+ : {}),
187
205
  }));
188
206
  }
189
207
  const graphOrder = input.graph.claims.map((claim) => claim.claimId);
@@ -195,11 +213,14 @@ export function evaluateProofClosure(input) {
195
213
  .filter((evaluation) => isBlockingVerdict(evaluation.verdict))
196
214
  .map((evaluation) => evaluation.claimId);
197
215
  const blockingClaimIds = blockingPathClaimIds(claims, evaluations, blockingRoots, graphOrder);
216
+ const blockingCut = explainBlockingCut(claims, evaluations, blockingRoots);
198
217
  return Object.freeze({
199
218
  verdict: globalVerdict(requiredRoots, input),
219
+ witnessIndependence,
200
220
  claimEvaluations: Object.freeze(ordered),
201
221
  blockingClaimIds: Object.freeze(blockingClaimIds),
202
- minimalBlockingCut: Object.freeze([...minimalBlockingCut(claims, evaluations, blockingRoots)]),
222
+ minimalBlockingCut: blockingCut.claimIds,
223
+ blockingCut,
203
224
  unresolvedEffectIds: Object.freeze([...input.unresolvedEffectIds].sort(compareCodeUnits)),
204
225
  workspaceCompleteness: input.workspaceCompleteness,
205
226
  });