@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,406 @@
1
+ /**
2
+ * Evaluation Module types — the pure governance kernel (architecture spec §§1–2).
3
+ *
4
+ * `evaluate(EvaluationInput): EvaluationResult` is deterministic and free of
5
+ * I/O, clock, and network: time arrives as `evaluatedAt`, contract identity as
6
+ * an FCID + preimage the kernel re-hashes itself, and trust as explicit
7
+ * three-state facts. Everything a verdict depends on is IN the input, so two
8
+ * runtimes given the same input must produce byte-identical results (the
9
+ * digests below are the proof handle).
10
+ *
11
+ * Brief 2 ships the kernel in shadow mode only: CLI and Cloud keep their
12
+ * legacy verdict machines and additionally run the kernel, recording
13
+ * agreement/divergence. The parity corpus (`@repo/parity`) is the acceptance
14
+ * contract — every scenario's authored kernel verdict must land.
15
+ */
16
+
17
+ import type { ContractPreimage } from "../contract/preimage.js";
18
+ import type {
19
+ AnalysisPlanV1,
20
+ CoverageRegionV1,
21
+ CoverageSummaryV1,
22
+ } from "../analysis-plan/index.js";
23
+ import type { DigestHex, EvaluationReceiptId } from "../domain-ids.js";
24
+ import type { CanonicalFactIntegrityV1 } from "../facts/types.js";
25
+ import type { FactIndex } from "../facts/fact-index.js";
26
+ import type { RepositoryBindingKeyV1 } from "../repository-binding.js";
27
+
28
+ /** Bumped when kernel semantics change in a way that can flip a verdict. */
29
+ export const EVALUATOR_VERSION = "evaluation-kernel:v1" as const;
30
+ /** Authoritative facts-only evaluator introduced by Fragments V1 Brief 04. */
31
+ export const EVALUATOR_VERSION_V2 = "evaluation-kernel:v2" as const;
32
+
33
+ /**
34
+ * Four-state per-finding model (spec §2). `candidate` is the derivation step —
35
+ * "severity/rule says it could block" — and always resolves to one of the
36
+ * other three states in a result: all required trust facts affirmative →
37
+ * `blocked`; any required fact definitively negative (or the finding is
38
+ * ineligible/waived/existing) → `advisory`; any required fact unknown →
39
+ * `pending_cloud_trust`. The type keeps all four so adapters can speak the
40
+ * full vocabulary when rendering derivations.
41
+ */
42
+ export type EvaluationFindingState = "candidate" | "blocked" | "advisory" | "pending_cloud_trust";
43
+
44
+ export type EvaluationVerdict = "pass" | "block" | "indeterminate";
45
+
46
+ /**
47
+ * Run-level reasons the kernel owns (spec §2). `warnPresent` and
48
+ * `findingsExitCode` are deliberately absent — they are derived outputs
49
+ * (`EvaluationResult["derived"]`), not reasons. `proofFailed` belongs to the
50
+ * repair/prove module and `requiredCloudReportMissing` to the delivery
51
+ * adapter; the kernel never sees either.
52
+ */
53
+ export type EvaluationReason =
54
+ // Integrity/coverage — these CAP the verdict at `indeterminate`: without a
55
+ // proven contract identity and a real scan, neither pass nor block is
56
+ // provable (see `blocking-but-unpinned` in the parity corpus).
57
+ | "zeroFileScan"
58
+ | "governanceInert"
59
+ | "contractUnpinned"
60
+ | "contractStale"
61
+ | "contractIdentityMismatch"
62
+ | "inertConfigPresent"
63
+ | "unknownPresetPresent"
64
+ // Definitive gating evidence — produce `block` when integrity is intact.
65
+ | "blockedFindings"
66
+ | "adoptionRegression"
67
+ | "expiredSuppression"
68
+ // Unknowns — produce `indeterminate` when nothing blocks.
69
+ | "pendingCloudTrust"
70
+ | "adoptionBaselineUnavailable";
71
+
72
+ /** Three-state source trust (spec §1): `unverified` is a DEFINITIVE
73
+ * non-reproduction (advisory), `unknown` means the check could not run
74
+ * (pending). Today's boolean surfaces conflate the two — their adapters must
75
+ * map `false` to `unknown`, never to `unverified`. */
76
+ export type EvaluationSourceTrust = "verified" | "unverified" | "unknown";
77
+
78
+ /** Three-state mapping trust: `unconfirmed` is an AUTHORITATIVE negative
79
+ * (Cloud says the mapping is proposed-only or absent), `unknown` means the
80
+ * mapping state could not be read. */
81
+ export type EvaluationMappingTrust = "confirmed" | "unconfirmed" | "unknown";
82
+
83
+ /** A finding offered to the kernel as evidence. Machine-neutral: adapters
84
+ * translate their native finding/occurrence shapes into this. */
85
+ export interface EvaluationFindingInput {
86
+ fingerprint: string;
87
+ ruleId: string;
88
+ severity: "error" | "warn" | "info";
89
+ /** Rule-flagged low-confidence tier (`attributes.advisory`): never blocks. */
90
+ advisory?: boolean;
91
+ /** Evidence grade per `@usefragments/core` evidence semantics; absent means
92
+ * source-backed (compatibility with older reports). */
93
+ evidenceGrade?: string;
94
+ /**
95
+ * Per-RECORD facts. Duplicate fingerprints are distinct records of one
96
+ * finding identity, each classified independently on its OWN facts — the
97
+ * hardest genuinely-supported outcome wins. A record-level fact overrides
98
+ * the fingerprint-keyed maps (`trust.sources`/`trust.mappings`/
99
+ * `baseline.findingStates`), which remain the shape for adapters whose
100
+ * records are already one-per-fingerprint. Facts from one record never
101
+ * apply to another: an adapter must not fold contradictory records into a
102
+ * single fact map (that synthesizes a conjunction no record supports).
103
+ */
104
+ sourceTrust?: EvaluationSourceTrust;
105
+ mappingTrust?: EvaluationMappingTrust;
106
+ /** Record-level baseline judgment; only meaningful under a baseline whose
107
+ * identity the adapter vouches for. */
108
+ baselineState?: "introduced" | "existing" | "unknown";
109
+ /** This record is individually waived (e.g. a suppressed Cloud occurrence).
110
+ * Unlike a fingerprint waiver it never lends its waiver — or any other
111
+ * fact — to an unwaived duplicate. No expiry semantics (use
112
+ * `EvaluationWaiver` for expirable suppressions). */
113
+ waived?: boolean;
114
+ }
115
+
116
+ export interface EvaluationWaiver {
117
+ fingerprint: string;
118
+ /** Epoch ms; absent = never expires. Compared against `evaluatedAt` — the
119
+ * kernel has no clock. An expired waiver re-exposes the debt it hid
120
+ * (`expiredSuppression`, gating). */
121
+ expiresAt?: number;
122
+ }
123
+
124
+ export interface EvaluationTrust {
125
+ /** Run-default source trust, used when no per-finding entry exists. */
126
+ source: EvaluationSourceTrust;
127
+ /** Per-fingerprint source trust — Cloud verifies each occurrence's location
128
+ * independently, so a mixed run can carry one verified blocker beside an
129
+ * unknown peer without the peer poisoning the blocker's facts. */
130
+ sources?: Readonly<Record<string, EvaluationSourceTrust>>;
131
+ /** Keyed by finding fingerprint. Absent key = `unknown`. */
132
+ mappings: Readonly<Record<string, EvaluationMappingTrust>>;
133
+ }
134
+
135
+ export interface EvaluationBaseline {
136
+ /**
137
+ * Whether the baseline's identity (binding + repository + FCID + base
138
+ * generation) was verified (spec §2). An unverified baseline provides NO
139
+ * facts: its `findingStates` are ignored and introduced-ness stays unknown.
140
+ */
141
+ identityVerified: boolean;
142
+ /**
143
+ * The baseline judgment per evidence fingerprint. On a verified baseline an
144
+ * absent entry means the finding is NOT in the baseline — i.e. `introduced`.
145
+ */
146
+ findingStates?: Readonly<Record<string, "introduced" | "existing" | "unknown">>;
147
+ }
148
+
149
+ export interface EvaluationScope {
150
+ kind: "full" | "changed" | "single-file" | "task";
151
+ coverage: { filesScanned: number };
152
+ }
153
+
154
+ export interface EvaluationIntegrityInput {
155
+ governance: "healthy" | "inert";
156
+ /** Count of inert-config diagnostics (FUI9004 class); gates only when the
157
+ * policy arms `failOnInert`. */
158
+ inertConfigDiagnostics?: number;
159
+ unknownPresets?: readonly string[];
160
+ }
161
+
162
+ export interface EffectiveRulePolicy {
163
+ /**
164
+ * Which trust facts blocking requires for this rule. `canonical` = mapping +
165
+ * source + introduced must all be affirmative; `evidence` = source +
166
+ * introduced (rules with no canonical-mapping concept: hygiene, registry
167
+ * drift, unknown ids); `none` = policy eligibility alone blocks
168
+ * (baseline/waiver facts still downgrade when definitively known, and so do
169
+ * DEFINITIVE trust negatives — a proven source non-reproduction always, an
170
+ * authoritatively unconfirmed mapping for contract-tier rules — an
171
+ * explicit per-rule override only, never a default: spec §2 never lets a
172
+ * candidate with unknown trust go silently green, and spec §3 never
173
+ * enforces unverified evidence). Defaults from `RULE_TIER`: contract-tier
174
+ * rules → `canonical`, everything else → `evidence`.
175
+ */
176
+ trust?: "canonical" | "evidence" | "none";
177
+ }
178
+
179
+ export interface EffectivePolicy {
180
+ /** Resolved by the adapter (CI defaults, config) — the kernel never knows
181
+ * whether a run "is CI"; it only sees the resolved gate. */
182
+ failOnWarnings: boolean;
183
+ /** Arms the `inertConfigPresent` integrity gate. */
184
+ failOnInert?: boolean;
185
+ rules?: Readonly<Record<string, EffectiveRulePolicy>>;
186
+ }
187
+
188
+ /**
189
+ * Contract identity input. When pinned WITH a preimage the kernel recomputes
190
+ * the FCID itself — mismatch is an integrity failure (spec §1). A pinned
191
+ * contract without a preimage is a shadow-mode concession: identity cannot be
192
+ * verified (`fcidVerified: "unavailable"`), which never caps a shadow verdict
193
+ * but MUST fail receipt minting once receipts exist (brief 3+).
194
+ */
195
+ export type EvaluationContractInput =
196
+ | {
197
+ state: "pinned";
198
+ fcid: string;
199
+ preimage?: ContractPreimage;
200
+ /** The pinned FCID is no longer the active contract. */
201
+ stale?: boolean;
202
+ }
203
+ | { state: "unpinned" };
204
+
205
+ /**
206
+ * Evidence union. `findings` is the shadow-mode path (both legacy machines
207
+ * already derived findings); `facts` runs the core rules engine (`runRules`)
208
+ * inside the kernel — the spec-§1 long-term shape.
209
+ */
210
+ export type EvaluationEvidence =
211
+ | { kind: "findings"; findings: readonly EvaluationFindingInput[] }
212
+ | { kind: "facts"; facts: FactIndex };
213
+
214
+ export interface EvaluationInput {
215
+ contract: EvaluationContractInput;
216
+ scope: EvaluationScope;
217
+ evidence: EvaluationEvidence;
218
+ policy: EffectivePolicy;
219
+ baseline?: EvaluationBaseline;
220
+ /**
221
+ * Adoption-floor comparison. Deliberately SEPARATE from `baseline`: the
222
+ * adoption floor is its own committed document with its own provenance, so
223
+ * finding-baseline identity verification neither grants nor revokes these
224
+ * facts (the "unverified baseline provides no facts" invariant is scoped to
225
+ * `baseline.findingStates`).
226
+ */
227
+ adoption?: "unavailable" | { regressed: boolean };
228
+ trust?: EvaluationTrust;
229
+ waivers?: readonly EvaluationWaiver[];
230
+ integrity: EvaluationIntegrityInput;
231
+ /** Supplied by the adapter; part of the input digest. Keeps waiver-expiry
232
+ * evaluation deterministic. */
233
+ evaluatedAt: number;
234
+ }
235
+
236
+ export interface EvaluatedFinding {
237
+ fingerprint: string;
238
+ ruleId: string;
239
+ severity: "error" | "warn" | "info";
240
+ state: Exclude<EvaluationFindingState, "candidate">;
241
+ /** Stable machine-readable derivation trail, e.g. `["eligible:error",
242
+ * "trust:mapping:unknown"]`. Rendering belongs to adapters. */
243
+ stateReasons: readonly string[];
244
+ }
245
+
246
+ export interface EvaluationResult {
247
+ evaluatorVersion: typeof EVALUATOR_VERSION;
248
+ verdict: EvaluationVerdict;
249
+ reasons: readonly EvaluationReason[];
250
+ findings: readonly EvaluatedFinding[];
251
+ counts: { blocked: number; advisory: number; pendingCloudTrust: number };
252
+ /** Legacy-compat derived outputs (spec §2): never reasons. */
253
+ derived: {
254
+ /** A warn-severity finding is present in the evidence. */
255
+ warnPresent: boolean;
256
+ /** At least one finding gates (blocked) — the `findingsExitCode` analog. */
257
+ gatingFindingsPresent: boolean;
258
+ };
259
+ integrity: {
260
+ /** `true` = preimage re-hash matched the pinned FCID; `false` = mismatch
261
+ * (also a `contractIdentityMismatch` reason); `"unavailable"` = no
262
+ * preimage to verify (shadow concession) or unpinned. */
263
+ fcidVerified: boolean | "unavailable";
264
+ };
265
+ /** sha256 over the canonical serialization of the (normalized) input. */
266
+ inputDigest: string;
267
+ /** sha256 over the canonical serialization of the result minus digests. */
268
+ resultDigest: string;
269
+ }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // Evaluation V2 and receipt V1 (additive; V1 kernel remains unchanged)
273
+ // ---------------------------------------------------------------------------
274
+
275
+ export interface EffectiveEvaluationWaiverV2 {
276
+ waiverId: string;
277
+ scope:
278
+ | { kind: "finding"; fingerprint: string }
279
+ | { kind: "rule"; ruleId: string }
280
+ | { kind: "path"; pathPattern: string }
281
+ | { kind: "rule_path"; ruleId: string; pathPattern: string };
282
+ matchedFindingFingerprints: string[];
283
+ reason: string;
284
+ expiresAt?: number;
285
+ }
286
+
287
+ export interface EvaluationIntegrityInputV2 {
288
+ governance: "healthy" | "inert";
289
+ inertConfigDiagnosticCount: number;
290
+ profile:
291
+ | { state: "known"; profileId: string; profileVersion: string }
292
+ | { state: "unknown"; profileId: string; profileVersion?: string };
293
+ facts: CanonicalFactIntegrityV1;
294
+ }
295
+
296
+ export interface EvaluationInputV2 {
297
+ schemaVersion: 2;
298
+ binding: RepositoryBindingKeyV1;
299
+ analysisPlan: AnalysisPlanV1;
300
+ contract: { fcid: DigestHex; preimage: ContractPreimage; active: boolean };
301
+ factSchemaVersion: string;
302
+ facts: FactIndex;
303
+ factsDigest: DigestHex;
304
+ coverage: CoverageRegionV1[];
305
+ policy: EffectivePolicy;
306
+ baseline?: {
307
+ identity: {
308
+ bindingDigest: DigestHex;
309
+ repositoryExternalId: string;
310
+ fcid: DigestHex;
311
+ sourceCommitId: string;
312
+ generationId: string;
313
+ };
314
+ findingStates: Record<string, "introduced" | "existing" | "unknown">;
315
+ };
316
+ adoption?: "unavailable" | { regressed: boolean };
317
+ trust?: EvaluationTrust;
318
+ waivers: EffectiveEvaluationWaiverV2[];
319
+ integrity: EvaluationIntegrityInputV2;
320
+ evaluatedAt: number;
321
+ evaluatorVersion: string;
322
+ }
323
+
324
+ export type EvaluationReasonV2 =
325
+ | EvaluationReason
326
+ | "analysisPlanIdentityMismatch"
327
+ | "requiredCoverageMissing"
328
+ | "requiredCoverageIncomplete"
329
+ | "factConflict"
330
+ | "unknownAnalysisProfile";
331
+
332
+ export interface FindingV2 {
333
+ fingerprint: string;
334
+ ruleId: string;
335
+ severity: "error" | "warn" | "info";
336
+ state: "blocked" | "advisory" | "pending_cloud_trust";
337
+ stateReasons: string[];
338
+ }
339
+
340
+ export interface EvaluationResultV2 {
341
+ schemaVersion: 2;
342
+ verdict: EvaluationVerdict;
343
+ reasons: EvaluationReasonV2[];
344
+ findings: FindingV2[];
345
+ counts: { blocked: number; advisory: number; pendingCloudTrust: number };
346
+ derived: { warnPresent: boolean; gatingFindingsPresent: boolean };
347
+ coverageSummary: CoverageSummaryV1;
348
+ integrity: {
349
+ fcidVerified: boolean;
350
+ analysisPlanVerified: boolean;
351
+ factIntegrity: "healthy" | "conflict";
352
+ requiredCoverage: "complete" | "incomplete" | "no_required_regions";
353
+ governance: "healthy" | "inert";
354
+ profile: "known" | "unknown";
355
+ };
356
+ inputDigest: DigestHex;
357
+ resultDigest: DigestHex;
358
+ }
359
+
360
+ export type EvaluationEvidenceTrustV1 = "source_verified" | "ci_attested" | "local" | "unknown";
361
+ export type EvaluationAuthorityV1 = "advisory" | "enforcement_eligible";
362
+
363
+ export interface ProviderProofSemanticV1 {
364
+ schemaVersion: 1;
365
+ adapterId: string;
366
+ adapterVersion: string;
367
+ providerInstanceId: string;
368
+ bindingDigest: DigestHex;
369
+ repositoryExternalId: string;
370
+ sourceCommitId: string;
371
+ eventExternalId: string;
372
+ changeExternalId?: string;
373
+ publisherExternalId: string;
374
+ }
375
+
376
+ export interface ProviderProofV1 extends ProviderProofSemanticV1 {
377
+ proofDigest: DigestHex;
378
+ }
379
+
380
+ export interface EvaluationReceiptSemanticV1 {
381
+ schemaVersion: 1;
382
+ trust: EvaluationEvidenceTrustV1;
383
+ authority: EvaluationAuthorityV1;
384
+ binding: RepositoryBindingKeyV1;
385
+ sourceCommitId: string;
386
+ fcid: DigestHex;
387
+ analysisPlanDigest: DigestHex;
388
+ factsDigest: DigestHex;
389
+ inputDigest: DigestHex;
390
+ resultDigest: DigestHex;
391
+ evaluatorVersion: string;
392
+ providerProof?: ProviderProofV1;
393
+ }
394
+
395
+ export interface EvaluationReceiptV1 extends EvaluationReceiptSemanticV1 {
396
+ receiptId: EvaluationReceiptId;
397
+ mintedAt: string;
398
+ digest: DigestHex;
399
+ }
400
+
401
+ export type EvaluationReceiptMintInputV1 = Omit<
402
+ EvaluationReceiptSemanticV1,
403
+ "authority" | "providerProof" | "trust"
404
+ > & {
405
+ trust: Exclude<EvaluationEvidenceTrustV1, "source_verified">;
406
+ };
@@ -447,11 +447,13 @@ export function makeStyleFontSizeScaleFact(input: {
447
447
  export function makeStyleCssVarsMustBeDefinedFact(input: {
448
448
  severity: GovernanceSeverity;
449
449
  exclude?: readonly PolicyExclude[];
450
+ configPath?: string;
450
451
  }): StyleCssVarsMustBeDefinedFact {
451
452
  return {
452
453
  id: factId("style_css_vars_must_be_defined", {}),
453
454
  kind: "style_css_vars_must_be_defined",
454
455
  severity: input.severity,
456
+ ...(input.configPath ? { configPath: input.configPath } : {}),
455
457
  ...policyExcludeField(input.exclude),
456
458
  };
457
459
  }
@@ -18,7 +18,7 @@ import type {
18
18
  GovernedFragmentDefinition,
19
19
  ResolvedGovernedFragmentDefinition,
20
20
  } from "../governance.js";
21
- import { resolveComponentGovernance } from "../governance.js";
21
+ import { isPresetSourcedRule, resolveComponentGovernance } from "../governance.js";
22
22
  import { RULE_FAMILY_MEMBERS } from "../rules/families.js";
23
23
  import { normalizePolicyExcludes, type PolicyExclude } from "../policy-exclude.js";
24
24
  import { ownedImportMatchesRoot, ownedImportsEqual } from "../package-identity-match.js";
@@ -62,6 +62,17 @@ function recordExclude(record: { exclude?: unknown }): { exclude?: PolicyExclude
62
62
  return exclude ? { exclude } : {};
63
63
  }
64
64
 
65
+ /**
66
+ * Scan-internal path stamped onto `style.cssVars.mustBeDefined` after config
67
+ * load. Not a public schema field — read here so FUI2018 can name the file.
68
+ */
69
+ function cssVarsConfigPathField(style: { kind: "style.cssVars.mustBeDefined" }): {
70
+ configPath?: string;
71
+ } {
72
+ const value = (style as { configPath?: unknown }).configPath;
73
+ return typeof value === "string" && value.length > 0 ? { configPath: value } : {};
74
+ }
75
+
65
76
  /**
66
77
  * Reconcile excludes across records that compile to the SAME policy fact.
67
78
  *
@@ -168,6 +179,7 @@ export function compileGlobalGovernanceFacts(govern: GovernanceConfig | undefine
168
179
  makeStyleCssVarsMustBeDefinedFact({
169
180
  severity: style.severity,
170
181
  ...recordExclude(style),
182
+ ...cssVarsConfigPathField(style),
171
183
  })
172
184
  );
173
185
  break;
@@ -343,14 +355,25 @@ function compileRuleConfigFacts(govern: GovernanceConfig): PolicyFact[] {
343
355
 
344
356
  const configs = new Map<string, ReturnType<typeof ruleConfigFromValue>>();
345
357
  for (const [ruleId, value] of Object.entries(rules ?? {})) {
346
- const family = RULE_FAMILY_MEMBERS[ruleId];
347
- if (!family) continue;
348
- const config = ruleConfigFromValue(value, govern.severity ?? "warn");
349
- for (const memberRuleId of family) configs.set(memberRuleId, config);
358
+ configs.set(ruleId, ruleConfigFromValue(value, govern.severity ?? "warn"));
350
359
  }
351
360
 
361
+ // Family expansion. A member entry the user wrote beats the aggregate (it is the
362
+ // more specific statement); a member entry a preset minted does not beat an
363
+ // aggregate the user wrote — `tokens/hardcoded-values: true` over the customer
364
+ // default must turn its Tier-B members on, and `false` over the `fragments`
365
+ // preset must turn them off. Presets are internally consistent, so when both
366
+ // sides are preset-sourced the member stands.
352
367
  for (const [ruleId, value] of Object.entries(rules ?? {})) {
353
- configs.set(ruleId, ruleConfigFromValue(value, govern.severity ?? "warn"));
368
+ const family = RULE_FAMILY_MEMBERS[ruleId];
369
+ if (!family) continue;
370
+ const aggregateIsPreset = isPresetSourcedRule(value);
371
+ const config = ruleConfigFromValue(value, govern.severity ?? "warn");
372
+ for (const memberRuleId of family) {
373
+ const member = rules?.[memberRuleId];
374
+ if (member !== undefined && (aggregateIsPreset || !isPresetSourcedRule(member))) continue;
375
+ configs.set(memberRuleId, config);
376
+ }
354
377
  }
355
378
 
356
379
  if (govern.canonicalSources?.length) {
@@ -57,9 +57,17 @@ export interface FactConflict {
57
57
  skipped: Fact;
58
58
  }
59
59
 
60
+ /** Two observations with the same fact identity and material value. */
61
+ export interface FactDuplicate {
62
+ kept: Fact;
63
+ duplicate: Fact;
64
+ }
65
+
60
66
  export interface FactIndexOptions {
61
67
  /** Optional internal diagnostic route. Product output is quiet by default. */
62
68
  onConflict?: (message: string, conflict: FactConflict) => void;
69
+ /** Optional provenance route for a materially identical duplicate. */
70
+ onDuplicate?: (duplicate: FactDuplicate) => void;
63
71
  }
64
72
 
65
73
  interface FactWithComponent {
@@ -140,14 +148,16 @@ export class FactIndex {
140
148
  }
141
149
  const existing = this.facts.get(fact.id);
142
150
  if (existing) {
143
- if (
151
+ const differs =
144
152
  canonicalJson(logicalFactForComparison(existing)) !==
145
- canonicalJson(logicalFactForComparison(fact))
146
- ) {
153
+ canonicalJson(logicalFactForComparison(fact));
154
+ if (differs) {
147
155
  this.options.onConflict?.(
148
156
  `FactIndex: conflicting facts for id ${fact.id} — keeping ${describeFactForConflict(existing)}, skipping ${describeFactForConflict(fact)}`,
149
157
  { kept: existing, skipped: fact }
150
158
  );
159
+ } else {
160
+ this.options.onDuplicate?.({ kept: existing, duplicate: fact });
151
161
  }
152
162
  return;
153
163
  }
@@ -0,0 +1,172 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { canonicalFactIntegrityV1Schema, normalizeCanonicalFactIntegrityV1 } from "./index.js";
3
+
4
+ describe("CanonicalFactIntegrityV1", () => {
5
+ it("represents healthy coalescing without inventing a conflict", () => {
6
+ expect(
7
+ normalizeCanonicalFactIntegrityV1({ state: "healthy", coalescedDuplicateCount: 2 })
8
+ ).toEqual({ state: "healthy", coalescedDuplicateCount: 2 });
9
+ });
10
+
11
+ it("canonically sorts conflicts, values, and provenance", () => {
12
+ const normalized = normalizeCanonicalFactIntegrityV1({
13
+ state: "conflict",
14
+ coalescedDuplicateCount: 1,
15
+ conflicts: [
16
+ {
17
+ canonicalKey: "b",
18
+ factKind: "token_definition",
19
+ state: "conflict",
20
+ values: [
21
+ {
22
+ valueDigest: "b".repeat(64),
23
+ factIds: ["fact-b", "fact-a"],
24
+ analyzerIds: ["z", "a"],
25
+ evidenceRefs: ["src/b:1", "src/a:1"],
26
+ },
27
+ {
28
+ valueDigest: "a".repeat(64),
29
+ factIds: ["fact-c"],
30
+ analyzerIds: ["a"],
31
+ evidenceRefs: ["src/c:1"],
32
+ },
33
+ ],
34
+ },
35
+ ],
36
+ });
37
+ expect(normalized.conflicts[0].values[0].valueDigest).toBe("a".repeat(64));
38
+ expect(normalized.conflicts[0].values[1].factIds).toEqual(["fact-a", "fact-b"]);
39
+ });
40
+
41
+ it("uses locale-independent Unicode order for conflict identity", () => {
42
+ const conflict = (canonicalKey: string) => ({
43
+ canonicalKey,
44
+ factKind: "token_definition",
45
+ state: "conflict" as const,
46
+ values: [
47
+ {
48
+ valueDigest: "b".repeat(64),
49
+ factIds: ["ä-fact", "z-fact"],
50
+ analyzerIds: ["ä-analyzer", "z-analyzer"],
51
+ evidenceRefs: ["ä.css:1", "z.css:1"],
52
+ },
53
+ {
54
+ valueDigest: "a".repeat(64),
55
+ factIds: ["fact-a"],
56
+ analyzerIds: ["css"],
57
+ evidenceRefs: ["a.css:1"],
58
+ },
59
+ ],
60
+ });
61
+ const normalized = normalizeCanonicalFactIntegrityV1({
62
+ state: "conflict",
63
+ coalescedDuplicateCount: 0,
64
+ conflicts: [conflict("ä-key"), conflict("z-key")],
65
+ });
66
+ expect(normalized.conflicts.map((entry) => entry.canonicalKey)).toEqual(["z-key", "ä-key"]);
67
+ expect(normalized.conflicts[1].values[1].factIds).toEqual(["z-fact", "ä-fact"]);
68
+ });
69
+
70
+ it("rejects a conflict with only one material value", () => {
71
+ expect(() =>
72
+ canonicalFactIntegrityV1Schema.parse({
73
+ state: "conflict",
74
+ coalescedDuplicateCount: 0,
75
+ conflicts: [
76
+ {
77
+ canonicalKey: "token:a",
78
+ factKind: "token_definition",
79
+ state: "conflict",
80
+ values: [
81
+ {
82
+ valueDigest: "a".repeat(64),
83
+ factIds: ["fact-a"],
84
+ analyzerIds: ["css"],
85
+ evidenceRefs: ["src/a.css:1"],
86
+ },
87
+ ],
88
+ },
89
+ ],
90
+ })
91
+ ).toThrow();
92
+ });
93
+
94
+ it("rejects unknown fields and duplicate provenance", () => {
95
+ expect(() =>
96
+ canonicalFactIntegrityV1Schema.parse({
97
+ state: "healthy",
98
+ coalescedDuplicateCount: 0,
99
+ winner: "last",
100
+ })
101
+ ).toThrow();
102
+ expect(() =>
103
+ canonicalFactIntegrityV1Schema.parse({
104
+ state: "conflict",
105
+ coalescedDuplicateCount: 0,
106
+ conflicts: [
107
+ {
108
+ canonicalKey: "token:a",
109
+ factKind: "token_definition",
110
+ state: "conflict",
111
+ values: [
112
+ {
113
+ valueDigest: "a".repeat(64),
114
+ factIds: ["fact-a", "fact-a"],
115
+ analyzerIds: ["css"],
116
+ evidenceRefs: ["src/a.css:1"],
117
+ },
118
+ {
119
+ valueDigest: "b".repeat(64),
120
+ factIds: ["fact-b"],
121
+ analyzerIds: ["css"],
122
+ evidenceRefs: ["src/b.css:1"],
123
+ },
124
+ ],
125
+ },
126
+ ],
127
+ })
128
+ ).toThrow();
129
+ });
130
+
131
+ it("retains bounded conflict samples while recording honest total counts", () => {
132
+ const normalized = normalizeCanonicalFactIntegrityV1({
133
+ state: "conflict",
134
+ coalescedDuplicateCount: 0,
135
+ totalConflictCount: 2_000,
136
+ conflicts: [
137
+ {
138
+ canonicalKey: "token:a",
139
+ factKind: "token_definition",
140
+ state: "conflict",
141
+ totalValueCount: 100,
142
+ values: [
143
+ {
144
+ valueDigest: "a".repeat(64),
145
+ factIds: ["fact-a"],
146
+ totalFactIdCount: 10_000,
147
+ analyzerIds: ["css"],
148
+ totalAnalyzerIdCount: 2,
149
+ evidenceRefs: ["src/a.css:1"],
150
+ totalEvidenceRefCount: 10_000,
151
+ },
152
+ {
153
+ valueDigest: "b".repeat(64),
154
+ factIds: ["fact-b"],
155
+ analyzerIds: ["css"],
156
+ evidenceRefs: ["src/b.css:1"],
157
+ },
158
+ ],
159
+ },
160
+ ],
161
+ });
162
+
163
+ if (normalized.state !== "conflict") throw new Error("expected conflict integrity");
164
+ expect(normalized.totalConflictCount).toBe(2_000);
165
+ expect(normalized.conflicts[0]!.totalValueCount).toBe(100);
166
+ expect(normalized.conflicts[0]!.values[0]).toMatchObject({
167
+ totalFactIdCount: 10_000,
168
+ totalAnalyzerIdCount: 2,
169
+ totalEvidenceRefCount: 10_000,
170
+ });
171
+ });
172
+ });