@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.
- package/dist/{chunk-MZ4SW3TP.js → chunk-3IOWHECM.js} +8 -1
- package/dist/{chunk-MZ4SW3TP.js.map → chunk-3IOWHECM.js.map} +1 -1
- package/dist/chunk-7ULGH74M.js +66 -0
- package/dist/chunk-7ULGH74M.js.map +1 -0
- package/dist/{chunk-XN3LSDPY.js → chunk-BMPYIUZE.js} +612 -344
- package/dist/chunk-BMPYIUZE.js.map +1 -0
- package/dist/{chunk-YF65VYRY.js → chunk-ML5S6QNU.js} +191 -169
- package/dist/chunk-ML5S6QNU.js.map +1 -0
- package/dist/{chunk-3LLRNCPX.js → chunk-MZ2FS7U4.js} +1 -1
- package/dist/chunk-MZ2FS7U4.js.map +1 -0
- package/dist/chunk-PWIJMOI4.js +202 -0
- package/dist/chunk-PWIJMOI4.js.map +1 -0
- package/dist/chunk-RYFULE43.js +578 -0
- package/dist/chunk-RYFULE43.js.map +1 -0
- package/dist/codes/index.d.ts +2 -2
- package/dist/codes/index.js +3 -2
- package/dist/compiled-types/index.d.ts +327 -2
- package/dist/compiled-types/index.js +1 -1
- package/dist/generate/index.d.ts +3 -2
- package/dist/{governance-CLk_wkP9.d.ts → governance-hOPXGbbs.d.ts} +474 -515
- package/dist/governance-telemetry.d.ts +6 -0
- package/dist/governance-telemetry.js +1 -1
- package/dist/{index-_sxhUNqx.d.ts → index-C8bcXVav.d.ts} +448 -448
- package/dist/index.d.ts +3280 -1305
- package/dist/index.js +2414 -350
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +228 -0
- package/dist/manifest.js +24 -0
- package/dist/manifest.js.map +1 -0
- package/dist/preview/index.js +45 -1
- package/dist/preview/index.js.map +1 -1
- package/dist/preview-runtime.d.ts +1 -2
- package/dist/preview-runtime.js +150 -14
- package/dist/preview-runtime.js.map +1 -1
- package/dist/react-types.d.ts +1 -2
- package/dist/registry.d.ts +1412 -203
- package/dist/registry.js +30 -3
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +3 -2
- package/dist/storyAdapter.d.ts +1 -2
- package/dist/storyAdapter.js +11 -49
- package/dist/storyAdapter.js.map +1 -1
- package/dist/test-utils.d.ts +3 -2
- package/dist/topology/index.d.ts +1 -1
- package/dist/topology/index.js +1 -1
- package/package.json +8 -2
- package/src/__tests__/contract-parser.test.ts +318 -277
- package/src/__tests__/preview-runtime-hook.test.tsx +315 -0
- package/src/__tests__/preview-runtime.test.tsx +30 -8
- package/src/__tests__/schema.test.ts +191 -14
- package/src/analysis-plan/analysis-plan-v1.test.ts +320 -0
- package/src/analysis-plan/coverage.ts +181 -0
- package/src/analysis-plan/digest.ts +141 -0
- package/src/analysis-plan/index.ts +34 -0
- package/src/analysis-plan/types.ts +207 -0
- package/src/approved-contract-tokens.test.ts +39 -0
- package/src/approved-contract-tokens.ts +18 -0
- package/src/codes/__tests__/codes.test.ts +13 -0
- package/src/codes/codes.ts +40 -0
- package/src/compiled-types/index.ts +640 -39
- package/src/compiled-types/parse.test.ts +145 -4
- package/src/component-contract.ts +95 -53
- package/src/composition.ts +7 -13
- package/src/constants.ts +3 -6
- package/src/contract/hash.test.ts +20 -0
- package/src/contract/hash.ts +66 -9
- package/src/contract/index.ts +24 -1
- package/src/contract/manifest.test.ts +94 -0
- package/src/contract/manifest.ts +68 -0
- package/src/contract/preimage.test.ts +219 -1
- package/src/contract/preimage.ts +326 -6
- package/src/contract/stamp.test.ts +3 -0
- package/src/contract/stamp.ts +1 -1
- package/src/contract-parser.ts +54 -30
- package/src/defineFragment.test.ts +476 -91
- package/src/defineFragment.ts +204 -114
- package/src/domain-ids.test.ts +35 -0
- package/src/domain-ids.ts +61 -0
- package/src/evaluation/evaluate.test.ts +522 -0
- package/src/evaluation/evaluate.ts +690 -0
- package/src/evaluation/evaluation-v2-receipt-v1.test.ts +772 -0
- package/src/evaluation/index.ts +58 -0
- package/src/evaluation/receipt.ts +753 -0
- package/src/evaluation/types.ts +406 -0
- package/src/facts/builders.ts +2 -0
- package/src/facts/compile.ts +29 -6
- package/src/facts/fact-index.ts +13 -3
- package/src/facts/fact-integrity-v1.test.ts +172 -0
- package/src/facts/facts.test.ts +15 -0
- package/src/facts/ids.ts +46 -3
- package/src/facts/index.ts +14 -1
- package/src/facts/integrity.ts +134 -0
- package/src/facts/types.ts +36 -0
- package/src/governance-integrity.test.ts +1 -0
- package/src/governance-integrity.ts +5 -3
- package/src/governance-telemetry.ts +8 -0
- package/src/governance.ts +70 -8
- package/src/index.ts +230 -37
- package/src/preview/validation.test.ts +62 -0
- package/src/preview/validation.ts +48 -2
- package/src/preview-runtime.tsx +227 -20
- package/src/registry-install-plan.ts +200 -109
- package/src/registry-shards.test.ts +263 -0
- package/src/registry.ts +237 -0
- package/src/repository-binding.test.ts +50 -0
- package/src/repository-binding.ts +96 -0
- package/src/rules/families.test.ts +36 -0
- package/src/rules/finding.ts +7 -2
- package/src/rules/index.ts +17 -1
- package/src/rules/rule-config.test.ts +66 -0
- package/src/rules/rule-config.ts +73 -0
- package/src/rules/rules.test.ts +26 -0
- package/src/rules/tokens-css-vars-must-be-defined.test.ts +51 -2
- package/src/rules/tokens-css-vars-must-be-defined.ts +34 -1
- package/src/schema.ts +293 -113
- package/src/schemas/index.ts +1 -1
- package/src/storyAdapter.test.ts +68 -12
- package/src/storyAdapter.ts +44 -75
- package/src/topology/resolve-area.ts +1 -1
- package/src/types.ts +258 -40
- package/dist/chunk-3LLRNCPX.js.map +0 -1
- package/dist/chunk-RANPUC6C.js +0 -72
- package/dist/chunk-RANPUC6C.js.map +0 -1
- package/dist/chunk-XN3LSDPY.js.map +0 -1
- package/dist/chunk-YF65VYRY.js.map +0 -1
- package/src/fragment-types.ts +0 -214
- package/src/react-create-element.test.ts +0 -22
- package/src/react-create-element.ts +0 -12
- package/src/storyFilters.test.ts +0 -350
- 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
|
+
};
|
package/src/facts/builders.ts
CHANGED
|
@@ -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
|
}
|
package/src/facts/compile.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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) {
|
package/src/facts/fact-index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
});
|