@usefragments/core 1.10.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/{chunk-MZ4SW3TP.js → chunk-3IOWHECM.js} +8 -1
  2. package/dist/{chunk-MZ4SW3TP.js.map → chunk-3IOWHECM.js.map} +1 -1
  3. package/dist/chunk-7ULGH74M.js +66 -0
  4. package/dist/chunk-7ULGH74M.js.map +1 -0
  5. package/dist/{chunk-XN3LSDPY.js → chunk-BMPYIUZE.js} +612 -344
  6. package/dist/chunk-BMPYIUZE.js.map +1 -0
  7. package/dist/{chunk-YF65VYRY.js → chunk-ML5S6QNU.js} +191 -169
  8. package/dist/chunk-ML5S6QNU.js.map +1 -0
  9. package/dist/{chunk-3LLRNCPX.js → chunk-MZ2FS7U4.js} +1 -1
  10. package/dist/chunk-MZ2FS7U4.js.map +1 -0
  11. package/dist/chunk-PWIJMOI4.js +202 -0
  12. package/dist/chunk-PWIJMOI4.js.map +1 -0
  13. package/dist/chunk-RYFULE43.js +578 -0
  14. package/dist/chunk-RYFULE43.js.map +1 -0
  15. package/dist/codes/index.d.ts +2 -2
  16. package/dist/codes/index.js +3 -2
  17. package/dist/compiled-types/index.d.ts +327 -2
  18. package/dist/compiled-types/index.js +1 -1
  19. package/dist/generate/index.d.ts +3 -2
  20. package/dist/{governance-CLk_wkP9.d.ts → governance-hOPXGbbs.d.ts} +474 -515
  21. package/dist/governance-telemetry.d.ts +6 -0
  22. package/dist/governance-telemetry.js +1 -1
  23. package/dist/{index-_sxhUNqx.d.ts → index-C8bcXVav.d.ts} +448 -448
  24. package/dist/index.d.ts +3280 -1305
  25. package/dist/index.js +2414 -350
  26. package/dist/index.js.map +1 -1
  27. package/dist/manifest.d.ts +228 -0
  28. package/dist/manifest.js +24 -0
  29. package/dist/manifest.js.map +1 -0
  30. package/dist/preview/index.js +45 -1
  31. package/dist/preview/index.js.map +1 -1
  32. package/dist/preview-runtime.d.ts +1 -2
  33. package/dist/preview-runtime.js +150 -14
  34. package/dist/preview-runtime.js.map +1 -1
  35. package/dist/react-types.d.ts +1 -2
  36. package/dist/registry.d.ts +1412 -203
  37. package/dist/registry.js +30 -3
  38. package/dist/schemas/index.d.ts +1 -1
  39. package/dist/schemas/index.js +3 -2
  40. package/dist/storyAdapter.d.ts +1 -2
  41. package/dist/storyAdapter.js +11 -49
  42. package/dist/storyAdapter.js.map +1 -1
  43. package/dist/test-utils.d.ts +3 -2
  44. package/dist/topology/index.d.ts +1 -1
  45. package/dist/topology/index.js +1 -1
  46. package/package.json +8 -2
  47. package/src/__tests__/contract-parser.test.ts +318 -277
  48. package/src/__tests__/preview-runtime-hook.test.tsx +315 -0
  49. package/src/__tests__/preview-runtime.test.tsx +30 -8
  50. package/src/__tests__/schema.test.ts +191 -14
  51. package/src/analysis-plan/analysis-plan-v1.test.ts +320 -0
  52. package/src/analysis-plan/coverage.ts +181 -0
  53. package/src/analysis-plan/digest.ts +141 -0
  54. package/src/analysis-plan/index.ts +34 -0
  55. package/src/analysis-plan/types.ts +207 -0
  56. package/src/approved-contract-tokens.test.ts +39 -0
  57. package/src/approved-contract-tokens.ts +18 -0
  58. package/src/codes/__tests__/codes.test.ts +13 -0
  59. package/src/codes/codes.ts +40 -0
  60. package/src/compiled-types/index.ts +640 -39
  61. package/src/compiled-types/parse.test.ts +145 -4
  62. package/src/component-contract.ts +95 -53
  63. package/src/composition.ts +7 -13
  64. package/src/constants.ts +3 -6
  65. package/src/contract/hash.test.ts +20 -0
  66. package/src/contract/hash.ts +66 -9
  67. package/src/contract/index.ts +24 -1
  68. package/src/contract/manifest.test.ts +94 -0
  69. package/src/contract/manifest.ts +68 -0
  70. package/src/contract/preimage.test.ts +219 -1
  71. package/src/contract/preimage.ts +326 -6
  72. package/src/contract/stamp.test.ts +3 -0
  73. package/src/contract/stamp.ts +1 -1
  74. package/src/contract-parser.ts +54 -30
  75. package/src/defineFragment.test.ts +476 -91
  76. package/src/defineFragment.ts +204 -114
  77. package/src/domain-ids.test.ts +35 -0
  78. package/src/domain-ids.ts +61 -0
  79. package/src/evaluation/evaluate.test.ts +522 -0
  80. package/src/evaluation/evaluate.ts +690 -0
  81. package/src/evaluation/evaluation-v2-receipt-v1.test.ts +772 -0
  82. package/src/evaluation/index.ts +58 -0
  83. package/src/evaluation/receipt.ts +753 -0
  84. package/src/evaluation/types.ts +406 -0
  85. package/src/facts/builders.ts +2 -0
  86. package/src/facts/compile.ts +29 -6
  87. package/src/facts/fact-index.ts +13 -3
  88. package/src/facts/fact-integrity-v1.test.ts +172 -0
  89. package/src/facts/facts.test.ts +15 -0
  90. package/src/facts/ids.ts +46 -3
  91. package/src/facts/index.ts +14 -1
  92. package/src/facts/integrity.ts +134 -0
  93. package/src/facts/types.ts +36 -0
  94. package/src/governance-integrity.test.ts +1 -0
  95. package/src/governance-integrity.ts +5 -3
  96. package/src/governance-telemetry.ts +8 -0
  97. package/src/governance.ts +70 -8
  98. package/src/index.ts +230 -37
  99. package/src/preview/validation.test.ts +62 -0
  100. package/src/preview/validation.ts +48 -2
  101. package/src/preview-runtime.tsx +227 -20
  102. package/src/registry-install-plan.ts +200 -109
  103. package/src/registry-shards.test.ts +263 -0
  104. package/src/registry.ts +237 -0
  105. package/src/repository-binding.test.ts +50 -0
  106. package/src/repository-binding.ts +96 -0
  107. package/src/rules/families.test.ts +36 -0
  108. package/src/rules/finding.ts +7 -2
  109. package/src/rules/index.ts +17 -1
  110. package/src/rules/rule-config.test.ts +66 -0
  111. package/src/rules/rule-config.ts +73 -0
  112. package/src/rules/rules.test.ts +26 -0
  113. package/src/rules/tokens-css-vars-must-be-defined.test.ts +51 -2
  114. package/src/rules/tokens-css-vars-must-be-defined.ts +34 -1
  115. package/src/schema.ts +293 -113
  116. package/src/schemas/index.ts +1 -1
  117. package/src/storyAdapter.test.ts +68 -12
  118. package/src/storyAdapter.ts +44 -75
  119. package/src/topology/resolve-area.ts +1 -1
  120. package/src/types.ts +258 -40
  121. package/dist/chunk-3LLRNCPX.js.map +0 -1
  122. package/dist/chunk-RANPUC6C.js +0 -72
  123. package/dist/chunk-RANPUC6C.js.map +0 -1
  124. package/dist/chunk-XN3LSDPY.js.map +0 -1
  125. package/dist/chunk-YF65VYRY.js.map +0 -1
  126. package/src/fragment-types.ts +0 -214
  127. package/src/react-create-element.test.ts +0 -22
  128. package/src/react-create-element.ts +0 -12
  129. package/src/storyFilters.test.ts +0 -350
  130. package/src/storyFilters.ts +0 -253
@@ -0,0 +1,690 @@
1
+ /**
2
+ * The pure governance evaluation kernel (architecture spec §§1–2).
3
+ *
4
+ * Verdict model, settled by the parity corpus (`@repo/parity`):
5
+ *
6
+ * 1. **Integrity failures cap the verdict at `indeterminate`** — an unpinned,
7
+ * stale, or identity-mismatched contract, an inert scan, zero-file
8
+ * coverage, or unknown policy inputs mean neither pass nor block is
9
+ * provable (`blocking-but-unpinned` is the corpus row that forces the cap:
10
+ * a fully-trusted blocking finding on an unpinned verdict is still
11
+ * indeterminate).
12
+ * 2. With integrity intact, **definitive gating evidence blocks**: a blocked
13
+ * finding, a proven adoption regression, or an expired waiver.
14
+ * 3. Otherwise **any unknown holds the verdict at `indeterminate`** — pending
15
+ * trust, unavailable adoption history. Local green can never become Cloud
16
+ * red (spec §2): a candidate with unknown trust is never silently green.
17
+ * 4. Nothing gating, nothing unknown → `pass`.
18
+ *
19
+ * Per-finding pipeline (duplicate fingerprints classified independently, the
20
+ * hardest genuinely-supported state kept — never field-merged):
21
+ * waiver → eligibility (severity/policy/evidence) → required trust facts.
22
+ * Any required fact definitively negative → `advisory`; any unknown →
23
+ * `pending_cloud_trust`; all affirmative → `blocked`.
24
+ */
25
+
26
+ import { deriveCoverageSummaryV1 } from "../analysis-plan/index.js";
27
+ import {
28
+ contractHash,
29
+ canonicalPreimage,
30
+ compareCanonicalStrings,
31
+ sha256Hex,
32
+ } from "../contract/hash.js";
33
+ import { EVIDENCE_ORDER, canBlock, type EvidenceGrade } from "../evidence.js";
34
+ import { runRules } from "../rules/index.js";
35
+ import { configuredFindingSeverity, readRuleConfig } from "../rules/rule-config.js";
36
+ import { severityLevel } from "../severity.js";
37
+ import { tierFor } from "../rules/tiers.js";
38
+ import type {
39
+ EffectivePolicy,
40
+ EvaluatedFinding,
41
+ EvaluationFindingInput,
42
+ EvaluationInput,
43
+ EvaluationInputV2,
44
+ EvaluationReason,
45
+ EvaluationReasonV2,
46
+ EvaluationResult,
47
+ EvaluationResultV2,
48
+ EvaluationVerdict,
49
+ EvaluationWaiver,
50
+ } from "./types.js";
51
+ import { EVALUATOR_VERSION, EVALUATOR_VERSION_V2 } from "./types.js";
52
+ import { evaluationInputV2Schema, evaluationResultV2Schema } from "./receipt.js";
53
+
54
+ type TrustFactState = "affirmative" | "negative" | "unknown";
55
+
56
+ const INTEGRITY_REASONS: ReadonlySet<EvaluationReason> = new Set([
57
+ "zeroFileScan",
58
+ "governanceInert",
59
+ "contractUnpinned",
60
+ "contractStale",
61
+ "contractIdentityMismatch",
62
+ "inertConfigPresent",
63
+ "unknownPresetPresent",
64
+ ]);
65
+
66
+ const BLOCK_REASONS: ReadonlySet<EvaluationReason> = new Set([
67
+ "blockedFindings",
68
+ "adoptionRegression",
69
+ "expiredSuppression",
70
+ ]);
71
+
72
+ /** Stable reason ordering for deterministic results. */
73
+ const REASON_ORDER: readonly EvaluationReason[] = [
74
+ "zeroFileScan",
75
+ "governanceInert",
76
+ "contractUnpinned",
77
+ "contractStale",
78
+ "contractIdentityMismatch",
79
+ "inertConfigPresent",
80
+ "unknownPresetPresent",
81
+ "blockedFindings",
82
+ "adoptionRegression",
83
+ "expiredSuppression",
84
+ "pendingCloudTrust",
85
+ "adoptionBaselineUnavailable",
86
+ ];
87
+
88
+ export function evaluate(input: EvaluationInputV2): EvaluationResultV2;
89
+ export function evaluate(input: EvaluationInput): EvaluationResult;
90
+ export function evaluate(
91
+ input: EvaluationInput | EvaluationInputV2
92
+ ): EvaluationResult | EvaluationResultV2 {
93
+ return isEvaluationInputV2(input) ? evaluateV2(input) : evaluateV1(input);
94
+ }
95
+
96
+ function isEvaluationInputV2(
97
+ input: EvaluationInput | EvaluationInputV2
98
+ ): input is EvaluationInputV2 {
99
+ return "schemaVersion" in input && input.schemaVersion === 2;
100
+ }
101
+
102
+ function evaluateV1(input: EvaluationInput): EvaluationResult {
103
+ const reasons = new Set<EvaluationReason>();
104
+
105
+ // ---- Contract identity (the kernel recomputes the FCID itself) ----------
106
+ let fcidVerified: boolean | "unavailable" = "unavailable";
107
+ if (input.contract.state === "unpinned") {
108
+ reasons.add("contractUnpinned");
109
+ } else {
110
+ if (input.contract.stale) reasons.add("contractStale");
111
+ if (input.contract.preimage) {
112
+ fcidVerified = contractHash(input.contract.preimage) === input.contract.fcid;
113
+ if (!fcidVerified) reasons.add("contractIdentityMismatch");
114
+ }
115
+ }
116
+
117
+ // ---- Coverage / run integrity -------------------------------------------
118
+ if (input.scope.kind === "full" && input.scope.coverage.filesScanned === 0) {
119
+ reasons.add("zeroFileScan");
120
+ }
121
+ if (input.integrity.governance === "inert") {
122
+ // An adapter may soften the RENDERING of an inert local run — never the
123
+ // verdict: inert coverage cannot prove anything on any surface.
124
+ reasons.add("governanceInert");
125
+ }
126
+ if (input.policy.failOnInert === true && (input.integrity.inertConfigDiagnostics ?? 0) > 0) {
127
+ reasons.add("inertConfigPresent");
128
+ }
129
+ if ((input.integrity.unknownPresets?.length ?? 0) > 0) {
130
+ reasons.add("unknownPresetPresent");
131
+ }
132
+
133
+ // ---- Adoption history ----------------------------------------------------
134
+ if (input.adoption === "unavailable") {
135
+ reasons.add("adoptionBaselineUnavailable");
136
+ } else if (input.adoption?.regressed === true) {
137
+ reasons.add("adoptionRegression");
138
+ }
139
+
140
+ // ---- Waivers -------------------------------------------------------------
141
+ // A non-finite expiry (Infinity, NaN) means "never expires" — the same
142
+ // reading `canonicalWaivers` digests it as, so the digest and the verdict
143
+ // can never disagree about a waiver's state.
144
+ const activeWaivers = new Set<string>();
145
+ for (const waiver of input.waivers ?? []) {
146
+ if (
147
+ waiver.expiresAt !== undefined &&
148
+ Number.isFinite(waiver.expiresAt) &&
149
+ waiver.expiresAt <= input.evaluatedAt
150
+ ) {
151
+ // An expired waiver re-exposes the debt it hid — the finding it covered
152
+ // may not even be in this run's evidence, so this is a run-level gate,
153
+ // not merely the loss of a downgrade.
154
+ reasons.add("expiredSuppression");
155
+ } else {
156
+ activeWaivers.add(waiver.fingerprint);
157
+ }
158
+ }
159
+
160
+ // ---- Per-finding classification -----------------------------------------
161
+ // Duplicate fingerprints are classified INDEPENDENTLY and the hardest
162
+ // genuinely-supported outcome wins (blocked > pending > advisory). Nothing
163
+ // is merged field-by-field — a synthetic row could fabricate gating
164
+ // evidence no input row carries — and reordering never changes the result
165
+ // because rows are canonically sorted before classification.
166
+ const evidence = evidenceFindings(input);
167
+ const findings = [...groupByFingerprint(evidence).values()].map((rows) => {
168
+ const classified = rows.map((row) => classifyFinding(row, input, activeWaivers));
169
+ return classified.reduce((hardest, candidate) =>
170
+ STATE_RANK[candidate.state] < STATE_RANK[hardest.state] ? candidate : hardest
171
+ );
172
+ });
173
+
174
+ const counts = { blocked: 0, advisory: 0, pendingCloudTrust: 0 };
175
+ for (const finding of findings) {
176
+ if (finding.state === "blocked") counts.blocked += 1;
177
+ else if (finding.state === "pending_cloud_trust") counts.pendingCloudTrust += 1;
178
+ else counts.advisory += 1;
179
+ }
180
+ if (counts.blocked > 0) reasons.add("blockedFindings");
181
+ if (counts.pendingCloudTrust > 0) reasons.add("pendingCloudTrust");
182
+
183
+ // ---- Verdict -------------------------------------------------------------
184
+ const ordered = REASON_ORDER.filter((reason) => reasons.has(reason));
185
+ let verdict: EvaluationVerdict;
186
+ if (ordered.some((reason) => INTEGRITY_REASONS.has(reason))) {
187
+ verdict = "indeterminate";
188
+ } else if (ordered.some((reason) => BLOCK_REASONS.has(reason))) {
189
+ verdict = "block";
190
+ } else if (ordered.length > 0) {
191
+ verdict = "indeterminate";
192
+ } else {
193
+ verdict = "pass";
194
+ }
195
+
196
+ const derived = {
197
+ warnPresent: evidence.some((finding) => finding.severity === "warn"),
198
+ gatingFindingsPresent: counts.blocked > 0,
199
+ };
200
+
201
+ const inputDigest = sha256Hex(canonicalPreimage(normalizeInputForDigest(input)));
202
+ const partial = {
203
+ evaluatorVersion: EVALUATOR_VERSION,
204
+ verdict,
205
+ reasons: ordered,
206
+ findings,
207
+ counts,
208
+ derived,
209
+ integrity: { fcidVerified },
210
+ };
211
+ return {
212
+ ...partial,
213
+ inputDigest,
214
+ resultDigest: sha256Hex(canonicalPreimage({ ...partial, inputDigest })),
215
+ };
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Evaluation V2
220
+ // ---------------------------------------------------------------------------
221
+
222
+ const V2_REASON_ORDER: readonly EvaluationReasonV2[] = [
223
+ "analysisPlanIdentityMismatch",
224
+ "requiredCoverageMissing",
225
+ "requiredCoverageIncomplete",
226
+ "factConflict",
227
+ "unknownAnalysisProfile",
228
+ ...REASON_ORDER,
229
+ ];
230
+
231
+ const V2_INTEGRITY_REASONS = new Set<EvaluationReasonV2>([
232
+ ...INTEGRITY_REASONS,
233
+ "analysisPlanIdentityMismatch",
234
+ "requiredCoverageMissing",
235
+ "requiredCoverageIncomplete",
236
+ "factConflict",
237
+ "unknownAnalysisProfile",
238
+ ]);
239
+
240
+ /**
241
+ * V1 supports one exact first-party profile/fact/evaluator generation. Unknown
242
+ * generations are never interpreted optimistically: evaluator/fact schema
243
+ * mismatches reject the call, while an unknown analysis profile remains a
244
+ * typed indeterminate result so coverage can explain the unsupported source.
245
+ */
246
+ const SUPPORTED_FACT_SCHEMA_VERSIONS_V2 = new Set(["facts:v1"]);
247
+ const SUPPORTED_REACT_WEB_PROFILE_VERSIONS_V1 = new Set(["1"]);
248
+
249
+ function evaluateV2(rawInput: EvaluationInputV2): EvaluationResultV2 {
250
+ if (rawInput.evaluatorVersion !== EVALUATOR_VERSION_V2) {
251
+ throw new Error(`Unsupported evaluator version: ${rawInput.evaluatorVersion}`);
252
+ }
253
+ if (!SUPPORTED_FACT_SCHEMA_VERSIONS_V2.has(rawInput.factSchemaVersion)) {
254
+ throw new Error(`Unsupported fact schema version: ${rawInput.factSchemaVersion}`);
255
+ }
256
+
257
+ const input = evaluationInputV2Schema.parse(rawInput);
258
+ const coverageSummary = deriveCoverageSummaryV1(input.analysisPlan, input.coverage);
259
+ const profileKnown =
260
+ input.integrity.profile.state === "known" &&
261
+ input.analysisPlan.profile.id === "react-web-v1" &&
262
+ SUPPORTED_REACT_WEB_PROFILE_VERSIONS_V1.has(input.analysisPlan.profile.version);
263
+
264
+ const compatibilityInput: EvaluationInput = {
265
+ contract: {
266
+ state: "pinned",
267
+ fcid: input.contract.fcid,
268
+ preimage: input.contract.preimage,
269
+ ...(!input.contract.active ? { stale: true } : {}),
270
+ },
271
+ scope: { kind: "full", coverage: { filesScanned: coverageSummary.analyzedFiles } },
272
+ evidence: { kind: "facts", facts: input.facts },
273
+ policy: input.policy,
274
+ ...(input.baseline
275
+ ? {
276
+ baseline: {
277
+ identityVerified: true,
278
+ findingStates: input.baseline.findingStates,
279
+ },
280
+ }
281
+ : {}),
282
+ ...(input.adoption !== undefined ? { adoption: input.adoption } : {}),
283
+ ...(input.trust !== undefined ? { trust: input.trust } : {}),
284
+ waivers: input.waivers.flatMap((waiver) =>
285
+ waiver.matchedFindingFingerprints.map((fingerprint) => ({
286
+ fingerprint,
287
+ ...(waiver.expiresAt !== undefined ? { expiresAt: waiver.expiresAt } : {}),
288
+ }))
289
+ ),
290
+ integrity: {
291
+ governance: input.integrity.governance,
292
+ ...(input.integrity.inertConfigDiagnosticCount > 0
293
+ ? { inertConfigDiagnostics: input.integrity.inertConfigDiagnosticCount }
294
+ : {}),
295
+ },
296
+ evaluatedAt: input.evaluatedAt,
297
+ };
298
+ const compatibilityResult = evaluateV1(compatibilityInput);
299
+
300
+ const requiredCoverage =
301
+ coverageSummary.requiredRegions === 0
302
+ ? "no_required_regions"
303
+ : coverageSummary.requiredByState.analyzed === coverageSummary.requiredRegions
304
+ ? "complete"
305
+ : "incomplete";
306
+ const integrity: EvaluationResultV2["integrity"] = {
307
+ fcidVerified: compatibilityResult.integrity.fcidVerified === true,
308
+ analysisPlanVerified: true,
309
+ factIntegrity: input.integrity.facts.state,
310
+ requiredCoverage,
311
+ governance: input.integrity.governance,
312
+ profile: profileKnown ? ("known" as const) : ("unknown" as const),
313
+ };
314
+
315
+ const reasons = new Set<EvaluationReasonV2>(compatibilityResult.reasons);
316
+ if (requiredCoverage === "no_required_regions") reasons.add("requiredCoverageMissing");
317
+ if (requiredCoverage === "incomplete") reasons.add("requiredCoverageIncomplete");
318
+ if (integrity.factIntegrity === "conflict") reasons.add("factConflict");
319
+ if (integrity.profile === "unknown") reasons.add("unknownAnalysisProfile");
320
+ const orderedReasons = V2_REASON_ORDER.filter((reason) => reasons.has(reason));
321
+
322
+ let verdict = compatibilityResult.verdict;
323
+ if (orderedReasons.some((reason) => V2_INTEGRITY_REASONS.has(reason))) {
324
+ verdict = "indeterminate";
325
+ }
326
+
327
+ const inputDigest = evaluationInputDigestV2(input);
328
+ const partial = {
329
+ schemaVersion: 2 as const,
330
+ verdict,
331
+ reasons: orderedReasons,
332
+ findings: compatibilityResult.findings.map((finding) => ({
333
+ ...finding,
334
+ stateReasons: [...finding.stateReasons],
335
+ })),
336
+ counts: { ...compatibilityResult.counts },
337
+ derived: { ...compatibilityResult.derived },
338
+ coverageSummary,
339
+ integrity,
340
+ };
341
+ const resultDigest = evaluationResultDigestV2({ ...partial, inputDigest });
342
+ return evaluationResultV2Schema.parse({ ...partial, inputDigest, resultDigest });
343
+ }
344
+
345
+ /** Hash captured input identity without re-running the rule engine. */
346
+ export function evaluationInputDigestV2(input: EvaluationInputV2): string {
347
+ return sha256Hex(
348
+ canonicalPreimage(
349
+ normalizeInputV2ForDigest(input, deriveCoverageSummaryV1(input.analysisPlan, input.coverage))
350
+ )
351
+ );
352
+ }
353
+
354
+ /** Validate stored result identity using the same algorithm as evaluation. */
355
+ export function evaluationResultDigestV2(result: Omit<EvaluationResultV2, "resultDigest">): string {
356
+ return sha256Hex(canonicalPreimage({ schema: "evaluation-result:v2", ...result }));
357
+ }
358
+
359
+ function normalizeInputV2ForDigest(
360
+ input: EvaluationInputV2,
361
+ coverageSummary: ReturnType<typeof deriveCoverageSummaryV1>
362
+ ) {
363
+ const {
364
+ analysisPlanId: _analysisPlanId,
365
+ createdAt: _createdAt,
366
+ digest: _planDigest,
367
+ obligationsDigest: _obligationsDigest,
368
+ ...analysisPlanSemantic
369
+ } = input.analysisPlan;
370
+ return {
371
+ schema: "evaluation-input:v2",
372
+ binding: input.binding,
373
+ analysisPlan: {
374
+ ...analysisPlanSemantic,
375
+ analysisPlanDigest: input.analysisPlan.digest,
376
+ obligationsDigest: input.analysisPlan.obligationsDigest,
377
+ },
378
+ contract: input.contract,
379
+ factSchemaVersion: input.factSchemaVersion,
380
+ factsDigest: input.factsDigest,
381
+ coverage: [...input.coverage]
382
+ .map((region) => ({
383
+ ...region,
384
+ diagnosticIds: [...region.diagnosticIds].sort(compareCanonicalStrings),
385
+ }))
386
+ .sort((left, right) => compareCanonicalStrings(left.regionId, right.regionId)),
387
+ coverageSummary,
388
+ policy: input.policy,
389
+ baseline: input.baseline,
390
+ adoption: input.adoption,
391
+ trust: input.trust,
392
+ waivers: input.waivers
393
+ .map((waiver) => ({
394
+ ...waiver,
395
+ matchedFindingFingerprints: [...waiver.matchedFindingFingerprints].sort(
396
+ compareCanonicalStrings
397
+ ),
398
+ }))
399
+ .sort((left, right) => compareCanonicalStrings(left.waiverId, right.waiverId)),
400
+ integrity: input.integrity,
401
+ evaluatedAt: input.evaluatedAt,
402
+ evaluatorVersion: input.evaluatorVersion,
403
+ };
404
+ }
405
+
406
+ // ---------------------------------------------------------------------------
407
+ // Evidence
408
+ // ---------------------------------------------------------------------------
409
+
410
+ function evidenceFindings(input: EvaluationInput): EvaluationFindingInput[] {
411
+ if (input.evidence.kind === "findings") return [...input.evidence.findings];
412
+ // Facts mode: run the core rules engine inside the kernel (spec §1 — the
413
+ // kernel extends the `runRules(FactIndex)` seed). `runRules` is pure and
414
+ // its output ordering is already deterministic.
415
+ // The configured severity (`govern.rules[id].severity`, a `governance_rule_config`
416
+ // fact) is applied here exactly as the engine applies it to the findings it
417
+ // prints — otherwise a rule the config demoted to `info` gates the verdict.
418
+ const ruleConfig = readRuleConfig(input.evidence.facts);
419
+ return runRules(input.evidence.facts).map((finding) => ({
420
+ fingerprint: finding.fingerprint,
421
+ ruleId: finding.ruleId,
422
+ severity: severityLevel(configuredFindingSeverity(finding, ruleConfig)),
423
+ advisory: finding.attributes?.advisory === true,
424
+ evidenceGrade: finding.evidenceGrade,
425
+ }));
426
+ }
427
+
428
+ /** Hardest state first: an earned block outranks a pending hold outranks an
429
+ * advisory downgrade. */
430
+ const STATE_RANK = { blocked: 0, pending_cloud_trust: 1, advisory: 2 } as const;
431
+
432
+ const SEVERITY_RANK = { error: 0, warn: 1, info: 2 } as const;
433
+
434
+ /** Canonical row shape: the evidence grade is normalized (absent reads as
435
+ * source-backed) so semantically identical rows compare — and digest —
436
+ * identically. */
437
+ function canonicalRow(finding: EvaluationFindingInput): EvaluationFindingInput {
438
+ return {
439
+ fingerprint: finding.fingerprint,
440
+ ruleId: finding.ruleId,
441
+ severity: finding.severity,
442
+ advisory: finding.advisory === true,
443
+ evidenceGrade: coerceEvidenceGrade(finding.evidenceGrade),
444
+ waived: finding.waived === true,
445
+ ...(finding.sourceTrust !== undefined ? { sourceTrust: finding.sourceTrust } : {}),
446
+ ...(finding.mappingTrust !== undefined ? { mappingTrust: finding.mappingTrust } : {}),
447
+ ...(finding.baselineState !== undefined ? { baselineState: finding.baselineState } : {}),
448
+ };
449
+ }
450
+
451
+ function compareStrings(a: string, b: string): number {
452
+ return a < b ? -1 : a > b ? 1 : 0;
453
+ }
454
+
455
+ function compareRows(a: EvaluationFindingInput, b: EvaluationFindingInput): number {
456
+ return (
457
+ compareStrings(a.fingerprint, b.fingerprint) ||
458
+ SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] ||
459
+ compareStrings(a.ruleId, b.ruleId) ||
460
+ Number(a.advisory ?? false) - Number(b.advisory ?? false) ||
461
+ compareStrings(a.evidenceGrade ?? "", b.evidenceGrade ?? "") ||
462
+ Number(a.waived ?? false) - Number(b.waived ?? false) ||
463
+ compareStrings(a.sourceTrust ?? "", b.sourceTrust ?? "") ||
464
+ compareStrings(a.mappingTrust ?? "", b.mappingTrust ?? "") ||
465
+ compareStrings(a.baselineState ?? "", b.baselineState ?? "")
466
+ );
467
+ }
468
+
469
+ /** Canonicalized, fully-ordered, exact-duplicate-free rows — input ordering
470
+ * can never reach the classification or the digests. */
471
+ function canonicalRows(findings: readonly EvaluationFindingInput[]): EvaluationFindingInput[] {
472
+ const sorted = findings.map(canonicalRow).sort(compareRows);
473
+ return sorted.filter((row, index) => index === 0 || compareRows(row, sorted[index - 1]!) !== 0);
474
+ }
475
+
476
+ function groupByFingerprint(
477
+ findings: readonly EvaluationFindingInput[]
478
+ ): Map<string, EvaluationFindingInput[]> {
479
+ const groups = new Map<string, EvaluationFindingInput[]>();
480
+ for (const row of canonicalRows(findings)) {
481
+ const group = groups.get(row.fingerprint);
482
+ if (group) group.push(row);
483
+ else groups.set(row.fingerprint, [row]);
484
+ }
485
+ return groups;
486
+ }
487
+
488
+ // ---------------------------------------------------------------------------
489
+ // Per-finding classification
490
+ // ---------------------------------------------------------------------------
491
+
492
+ function classifyFinding(
493
+ finding: EvaluationFindingInput,
494
+ input: EvaluationInput,
495
+ activeWaivers: ReadonlySet<string>
496
+ ): EvaluatedFinding {
497
+ const base = {
498
+ fingerprint: finding.fingerprint,
499
+ ruleId: finding.ruleId,
500
+ severity: finding.severity,
501
+ };
502
+
503
+ if (activeWaivers.has(finding.fingerprint) || finding.waived === true) {
504
+ return { ...base, state: "advisory", stateReasons: ["waived"] };
505
+ }
506
+
507
+ const eligibility = eligibilityFor(finding, input.policy);
508
+ if (eligibility !== null) {
509
+ return { ...base, state: "advisory", stateReasons: [eligibility] };
510
+ }
511
+
512
+ // Candidate. A verified baseline that records this exact finding as existing
513
+ // debt downgrades it regardless of the rule's trust requirements — existing
514
+ // debt may be green (spec §2), and Cloud can never promote existing back to
515
+ // introduced.
516
+ const introduced = introducedFactFor(finding, input);
517
+ if (introduced.state === "negative") {
518
+ return { ...base, state: "advisory", stateReasons: ["baseline:existing"] };
519
+ }
520
+
521
+ // A policy override relaxes which UNKNOWN facts hold a finding — it never
522
+ // silences a DEFINITIVE negative the rule's intrinsic (default) requirement
523
+ // knows about: spec §3 never enforces a proven non-reproduction, and a
524
+ // contract-tier rule with an authoritatively unconfirmed mapping has no
525
+ // named fix to offer, override or not.
526
+ const requirement =
527
+ input.policy.rules?.[finding.ruleId]?.trust ?? defaultTrustRequirement(finding.ruleId);
528
+ const mappingBinds =
529
+ requirement === "canonical" || defaultTrustRequirement(finding.ruleId) === "canonical";
530
+ if (requirement === "none") {
531
+ const definitives: Array<[string, TrustFactState]> = [sourceFact(finding, input)];
532
+ if (mappingBinds) definitives.push(mappingFact(finding, input));
533
+ const definitiveNegative = definitives.find(([, state]) => state === "negative");
534
+ if (definitiveNegative) {
535
+ return { ...base, state: "advisory", stateReasons: [`trust:${definitiveNegative[0]}`] };
536
+ }
537
+ return { ...base, state: "blocked", stateReasons: [`eligible:${finding.severity}`] };
538
+ }
539
+
540
+ const facts: Array<[string, TrustFactState]> = [
541
+ [`introduced:${introduced.detail}`, introduced.state],
542
+ sourceFact(finding, input),
543
+ ];
544
+ if (requirement === "canonical") {
545
+ facts.push(mappingFact(finding, input));
546
+ } else if (mappingBinds) {
547
+ const mapping = mappingFact(finding, input);
548
+ if (mapping[1] === "negative") facts.push(mapping);
549
+ }
550
+ const negative = facts.find(([, state]) => state === "negative");
551
+ if (negative) {
552
+ return { ...base, state: "advisory", stateReasons: [`trust:${negative[0]}`] };
553
+ }
554
+ const unknowns = facts.filter(([, state]) => state === "unknown");
555
+ if (unknowns.length > 0) {
556
+ return {
557
+ ...base,
558
+ state: "pending_cloud_trust",
559
+ stateReasons: unknowns.map(([label]) => `trust:${label}`),
560
+ };
561
+ }
562
+ return { ...base, state: "blocked", stateReasons: [`eligible:${finding.severity}`, "trust:all"] };
563
+ }
564
+
565
+ /** Non-null = the advisory state reason; null = the finding is a candidate. */
566
+ function eligibilityFor(finding: EvaluationFindingInput, policy: EffectivePolicy): string | null {
567
+ if (finding.advisory === true) return "ineligible:advisory-tier";
568
+ if (!canBlock(coerceEvidenceGrade(finding.evidenceGrade))) return "ineligible:evidence-grade";
569
+ if (finding.severity === "error") return null;
570
+ if (finding.severity === "warn") {
571
+ return policy.failOnWarnings ? null : "ineligible:warn-not-gated";
572
+ }
573
+ return "ineligible:info";
574
+ }
575
+
576
+ /** Missing grades read as source-backed (compat with older reports); unknown
577
+ * grade strings read as non-blocking — an unclassifiable claim never gates. */
578
+ function coerceEvidenceGrade(grade: string | undefined): EvidenceGrade {
579
+ if (grade === undefined) return "source_backed";
580
+ return (EVIDENCE_ORDER as readonly string[]).includes(grade) ? (grade as EvidenceGrade) : "none";
581
+ }
582
+
583
+ function defaultTrustRequirement(ruleId: string): "canonical" | "evidence" | "none" {
584
+ // RULE_TIER is the SSOT: contract-tier rules judge the declared canonical
585
+ // vocabulary and need the full canonical trust facts. EVERY other rule —
586
+ // hygiene and unknown ids alike — still requires source + introduced (spec
587
+ // §2: a candidate with unknown trust is never silently green, and spec §3
588
+ // never enforces unverified evidence), but a canonical mapping a rule
589
+ // cannot have (registry drift, style rules) is not demanded of it. `none`
590
+ // exists only as an explicit per-rule policy override.
591
+ return tierFor(ruleId) === "contract" ? "canonical" : "evidence";
592
+ }
593
+
594
+ function introducedFactFor(
595
+ finding: EvaluationFindingInput,
596
+ input: EvaluationInput
597
+ ): { state: TrustFactState; detail: string } {
598
+ const baseline = input.baseline;
599
+ if (!baseline) return { state: "unknown", detail: "no-baseline" };
600
+ if (!baseline.identityVerified) return { state: "unknown", detail: "identity-unverified" };
601
+ // Verified baseline: an absent entry means the finding is not in the
602
+ // baseline — introduced. A record-level judgment overrides the map.
603
+ const recorded =
604
+ finding.baselineState ?? baseline.findingStates?.[finding.fingerprint] ?? "introduced";
605
+ if (recorded === "existing") return { state: "negative", detail: "existing" };
606
+ if (recorded === "introduced") return { state: "affirmative", detail: "introduced" };
607
+ return { state: "unknown", detail: "unknown" };
608
+ }
609
+
610
+ function sourceFact(
611
+ finding: EvaluationFindingInput,
612
+ input: EvaluationInput
613
+ ): [string, TrustFactState] {
614
+ const source =
615
+ finding.sourceTrust ??
616
+ input.trust?.sources?.[finding.fingerprint] ??
617
+ input.trust?.source ??
618
+ "unknown";
619
+ if (source === "verified") return ["source:verified", "affirmative"];
620
+ if (source === "unverified") return ["source:non-reproduction", "negative"];
621
+ return ["source:unknown", "unknown"];
622
+ }
623
+
624
+ function mappingFact(
625
+ finding: EvaluationFindingInput,
626
+ input: EvaluationInput
627
+ ): [string, TrustFactState] {
628
+ const mapping = finding.mappingTrust ?? input.trust?.mappings[finding.fingerprint] ?? "unknown";
629
+ if (mapping === "confirmed") return ["mapping:confirmed", "affirmative"];
630
+ if (mapping === "unconfirmed") return ["mapping:unconfirmed", "negative"];
631
+ return ["mapping:unknown", "unknown"];
632
+ }
633
+
634
+ // ---------------------------------------------------------------------------
635
+ // Digests
636
+ // ---------------------------------------------------------------------------
637
+
638
+ /**
639
+ * Digest over the judgment-relevant input. Facts-mode evidence digests the
640
+ * DERIVED findings (a `FactIndex` is a class instance, not a canonical value);
641
+ * two inputs that judge identically digest identically.
642
+ */
643
+ function normalizeInputForDigest(input: EvaluationInput) {
644
+ return {
645
+ schema: "evaluation-input:v1",
646
+ contract: input.contract,
647
+ scope: input.scope,
648
+ evidence: canonicalRows(evidenceFindings(input)),
649
+ policy: input.policy,
650
+ baseline: input.baseline,
651
+ adoption: input.adoption,
652
+ trust: input.trust,
653
+ waivers: canonicalWaivers(input.waivers ?? []),
654
+ integrity: {
655
+ ...input.integrity,
656
+ ...(input.integrity.unknownPresets
657
+ ? { unknownPresets: [...new Set(input.integrity.unknownPresets)].sort() }
658
+ : {}),
659
+ },
660
+ evaluatedAt: input.evaluatedAt,
661
+ };
662
+ }
663
+
664
+ /** Totally ordered (fingerprint, then expiry) and exact-duplicate-free.
665
+ * Absent, `Infinity`, and `NaN` expiries all mean "never expires" against
666
+ * `expiresAt <= evaluatedAt`, so they canonicalize identically (no expiry). */
667
+ function canonicalWaivers(waivers: readonly EvaluationWaiver[]) {
668
+ const expiryOf = (waiver: EvaluationWaiver) =>
669
+ waiver.expiresAt === undefined || Number.isNaN(waiver.expiresAt)
670
+ ? Number.POSITIVE_INFINITY
671
+ : waiver.expiresAt;
672
+ const canonical = waivers.map((waiver) => {
673
+ const expiresAt = expiryOf(waiver);
674
+ return expiresAt === Number.POSITIVE_INFINITY
675
+ ? { fingerprint: waiver.fingerprint }
676
+ : { fingerprint: waiver.fingerprint, expiresAt };
677
+ });
678
+ const sorted = canonical.sort((a, b) => {
679
+ if (a.fingerprint !== b.fingerprint) return a.fingerprint < b.fingerprint ? -1 : 1;
680
+ const ea = expiryOf(a);
681
+ const eb = expiryOf(b);
682
+ return ea === eb ? 0 : ea < eb ? -1 : 1;
683
+ });
684
+ return sorted.filter(
685
+ (waiver, index) =>
686
+ index === 0 ||
687
+ waiver.fingerprint !== sorted[index - 1]!.fingerprint ||
688
+ expiryOf(waiver) !== expiryOf(sorted[index - 1]!)
689
+ );
690
+ }