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.
- package/CHANGELOG.md +18 -0
- package/README.md +44 -0
- package/dist/claims/claim-blocking-cut.d.ts +8 -15
- package/dist/claims/claim-blocking-cut.d.ts.map +1 -1
- package/dist/claims/claim-blocking-cut.js +161 -41
- package/dist/claims/claim-blocking-cut.js.map +1 -1
- package/dist/claims/claim-closure.d.ts.map +1 -1
- package/dist/claims/claim-closure.js +27 -6
- package/dist/claims/claim-closure.js.map +1 -1
- package/dist/claims/claim-types.d.ts +20 -2
- package/dist/claims/claim-types.d.ts.map +1 -1
- package/dist/claims/claim-types.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/run-contract.d.ts +58 -0
- package/dist/run-contract.d.ts.map +1 -0
- package/dist/run-contract.js +112 -0
- package/dist/run-contract.js.map +1 -0
- package/dist/run-dag.d.ts +18 -0
- package/dist/run-dag.d.ts.map +1 -0
- package/dist/run-dag.js +76 -0
- package/dist/run-dag.js.map +1 -0
- package/dist/run-parsing.d.ts +13 -0
- package/dist/run-parsing.d.ts.map +1 -0
- package/dist/run-parsing.js +81 -0
- package/dist/run-parsing.js.map +1 -0
- package/dist/run-resume.d.ts +15 -0
- package/dist/run-resume.d.ts.map +1 -0
- package/dist/run-resume.js +28 -0
- package/dist/run-resume.js.map +1 -0
- package/dist/run-task-retry.d.ts +8 -0
- package/dist/run-task-retry.d.ts.map +1 -0
- package/dist/run-task-retry.js +25 -0
- package/dist/run-task-retry.js.map +1 -0
- package/dist/run-writer-restart.d.ts +14 -0
- package/dist/run-writer-restart.d.ts.map +1 -0
- package/dist/run-writer-restart.js +27 -0
- package/dist/run-writer-restart.js.map +1 -0
- 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
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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":"
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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
|
|
33
|
-
const order =
|
|
34
|
-
if (order
|
|
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
|
|
40
|
-
return [...new Set(sets.flat())].sort(
|
|
39
|
+
function union(sets) {
|
|
40
|
+
return [...new Set(sets.flat())].sort(compareIds);
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 {
|
|
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
|
|
87
|
-
|
|
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:
|
|
222
|
+
minimalBlockingCut: blockingCut.claimIds,
|
|
223
|
+
blockingCut,
|
|
203
224
|
unresolvedEffectIds: Object.freeze([...input.unresolvedEffectIds].sort(compareCodeUnits)),
|
|
204
225
|
workspaceCompleteness: input.workspaceCompleteness,
|
|
205
226
|
});
|