@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,320 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
CoverageValidationErrorV1,
|
|
4
|
+
analysisObligationsDigestV1,
|
|
5
|
+
analysisPlanDigestV1,
|
|
6
|
+
buildAnalysisPlanV1,
|
|
7
|
+
coverageArtifactDigestV1,
|
|
8
|
+
deriveCoverageSummaryV1,
|
|
9
|
+
parseAnalysisPlanV1,
|
|
10
|
+
type AnalysisPlanInputV1,
|
|
11
|
+
type CoverageRegionV1,
|
|
12
|
+
} from "./index.js";
|
|
13
|
+
|
|
14
|
+
const DIGEST_A = "a".repeat(64);
|
|
15
|
+
const DIGEST_B = "b".repeat(64);
|
|
16
|
+
|
|
17
|
+
function input(overrides: Partial<AnalysisPlanInputV1> = {}): AnalysisPlanInputV1 {
|
|
18
|
+
return {
|
|
19
|
+
binding: {
|
|
20
|
+
provider: "github",
|
|
21
|
+
providerInstanceId: "github.com",
|
|
22
|
+
repositoryExternalId: "R_1",
|
|
23
|
+
bindingId: "binding_1",
|
|
24
|
+
},
|
|
25
|
+
source: {
|
|
26
|
+
commitId: "commit-a",
|
|
27
|
+
treeId: "tree-a",
|
|
28
|
+
defaultBranch: "main",
|
|
29
|
+
acquiredBy: "github_app",
|
|
30
|
+
},
|
|
31
|
+
contract: { fcid: DIGEST_A, artifactDigest: DIGEST_B },
|
|
32
|
+
config: { path: "fragments.config.ts", digest: DIGEST_A },
|
|
33
|
+
profile: {
|
|
34
|
+
id: "react-web-v1",
|
|
35
|
+
version: "1.0.0",
|
|
36
|
+
capabilities: ["tsx", "css"],
|
|
37
|
+
},
|
|
38
|
+
regions: [
|
|
39
|
+
{
|
|
40
|
+
regionId: "styles",
|
|
41
|
+
pathPattern: "src/**/*.css",
|
|
42
|
+
kind: "style_source",
|
|
43
|
+
language: "css",
|
|
44
|
+
required: false,
|
|
45
|
+
requiredDialects: ["css"],
|
|
46
|
+
analyzer: { id: "css", interfaceVersion: "1", implementationVersion: "1.0.0" },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
regionId: "components",
|
|
50
|
+
pathPattern: "src/**/*.tsx",
|
|
51
|
+
kind: "component_source",
|
|
52
|
+
language: "typescript",
|
|
53
|
+
required: true,
|
|
54
|
+
requiredDialects: ["tsx", "jsx"],
|
|
55
|
+
analyzer: { id: "react", interfaceVersion: "1", implementationVersion: "1.0.0" },
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
limits: {
|
|
59
|
+
maxSourceFiles: 10_000,
|
|
60
|
+
maxSourceBytes: 20_000_000,
|
|
61
|
+
maxFileBytes: 1_000_000,
|
|
62
|
+
maxFacts: 100_000,
|
|
63
|
+
maxRegions: 1_000,
|
|
64
|
+
maxDiagnostics: 10_000,
|
|
65
|
+
maxDurationMs: 120_000,
|
|
66
|
+
},
|
|
67
|
+
evaluatorVersion: "evaluation-kernel:v2",
|
|
68
|
+
createdAt: "2026-09-01T00:00:00.000Z",
|
|
69
|
+
...overrides,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("AnalysisPlanV1", () => {
|
|
74
|
+
it("has checked-in stable exact and obligations digests", () => {
|
|
75
|
+
const plan = buildAnalysisPlanV1(input());
|
|
76
|
+
expect(plan.digest).toBe("c9e854bd2e9f177708b41140a5515a32833ed96e7cab283a2ebfbb24fbca6b02");
|
|
77
|
+
expect(plan.obligationsDigest).toBe(
|
|
78
|
+
"97d866010baa7ea2711c40e7a4c501528696da1404ea65cd3ec88568bcb55221"
|
|
79
|
+
);
|
|
80
|
+
expect(plan.analysisPlanId).toBe(`apl_${plan.digest}`);
|
|
81
|
+
expect(parseAnalysisPlanV1(plan)).toEqual(plan);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("normalizes only declared set-like arrays and region order", () => {
|
|
85
|
+
const first = buildAnalysisPlanV1(input());
|
|
86
|
+
const reordered = buildAnalysisPlanV1(
|
|
87
|
+
input({
|
|
88
|
+
profile: { ...input().profile, capabilities: ["css", "tsx", "css"] },
|
|
89
|
+
regions: [...input().regions].reverse().map((region) => ({
|
|
90
|
+
...region,
|
|
91
|
+
requiredDialects: [...region.requiredDialects].reverse(),
|
|
92
|
+
})),
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
expect(reordered.digest).toBe(first.digest);
|
|
96
|
+
expect(reordered.obligationsDigest).toBe(first.obligationsDigest);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("uses locale-independent Unicode ordering for plan and coverage digests", () => {
|
|
100
|
+
const regions = [
|
|
101
|
+
{
|
|
102
|
+
...input().regions[0],
|
|
103
|
+
regionId: "ä-region",
|
|
104
|
+
pathPattern: "src/umlaut/**/*.css",
|
|
105
|
+
requiredDialects: ["ä-dialect", "z-dialect"],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
...input().regions[1],
|
|
109
|
+
regionId: "z-region",
|
|
110
|
+
pathPattern: "src/z/**/*.tsx",
|
|
111
|
+
requiredDialects: ["z-dialect", "ä-dialect"],
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
const first = buildAnalysisPlanV1(
|
|
115
|
+
input({
|
|
116
|
+
profile: { ...input().profile, capabilities: ["ä-capability", "z-capability"] },
|
|
117
|
+
regions,
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
const reordered = buildAnalysisPlanV1(
|
|
121
|
+
input({
|
|
122
|
+
profile: { ...input().profile, capabilities: ["z-capability", "ä-capability"] },
|
|
123
|
+
regions: [...regions].reverse().map((region) => ({
|
|
124
|
+
...region,
|
|
125
|
+
requiredDialects: [...region.requiredDialects].reverse(),
|
|
126
|
+
})),
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
expect(first.regions.map((region) => region.regionId)).toEqual(["z-region", "ä-region"]);
|
|
130
|
+
expect(first.profile.capabilities).toEqual(["z-capability", "ä-capability"]);
|
|
131
|
+
expect(first.digest).toBe("3a5248714219d09d52c629bf97195b27642545a8dd736eb54f5914d9d10536cd");
|
|
132
|
+
expect(first.obligationsDigest).toBe(
|
|
133
|
+
"18b8467e9bf0526bc2dec1558adb36513b34a9f7b2250b1ce806552b4f65eb01"
|
|
134
|
+
);
|
|
135
|
+
expect(reordered.digest).toBe(first.digest);
|
|
136
|
+
|
|
137
|
+
const observations: CoverageRegionV1[] = [
|
|
138
|
+
{
|
|
139
|
+
regionId: "ä-region",
|
|
140
|
+
state: "analyzed",
|
|
141
|
+
fileCount: 1,
|
|
142
|
+
byteCount: 10,
|
|
143
|
+
diagnosticIds: ["ä-diagnostic", "z-diagnostic"],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
regionId: "z-region",
|
|
147
|
+
state: "analyzed",
|
|
148
|
+
fileCount: 1,
|
|
149
|
+
byteCount: 10,
|
|
150
|
+
diagnosticIds: ["z-diagnostic", "ä-diagnostic"],
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
const coverageDigest = coverageArtifactDigestV1(first, observations);
|
|
154
|
+
expect(coverageDigest).toBe("db5169e2e6b6388b30ba3ca44674eb770cb0427d2b1bbb586e98454502343c86");
|
|
155
|
+
expect(coverageArtifactDigestV1(reordered, [...observations].reverse())).toBe(coverageDigest);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("separates exact source identity from source-independent obligations", () => {
|
|
159
|
+
const first = buildAnalysisPlanV1(input());
|
|
160
|
+
const nextCommit = buildAnalysisPlanV1(
|
|
161
|
+
input({ source: { ...input().source, commitId: "commit-b", treeId: "tree-b" } })
|
|
162
|
+
);
|
|
163
|
+
expect(nextCommit.digest).not.toBe(first.digest);
|
|
164
|
+
expect(nextCommit.obligationsDigest).toBe(first.obligationsDigest);
|
|
165
|
+
|
|
166
|
+
const nextContract = buildAnalysisPlanV1(
|
|
167
|
+
input({ contract: { ...input().contract, fcid: "c".repeat(64) } })
|
|
168
|
+
);
|
|
169
|
+
expect(nextContract.obligationsDigest).not.toBe(first.obligationsDigest);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("excludes display branch and timestamp from authority digests", () => {
|
|
173
|
+
const first = buildAnalysisPlanV1(input());
|
|
174
|
+
const displayOnly = buildAnalysisPlanV1(
|
|
175
|
+
input({
|
|
176
|
+
source: { ...input().source, defaultBranch: "renamed-default" },
|
|
177
|
+
createdAt: "2026-09-02T00:00:00.000Z",
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
expect(displayOnly.digest).toBe(first.digest);
|
|
181
|
+
expect(displayOnly.obligationsDigest).toBe(first.obligationsDigest);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("rejects tampered derived identity", () => {
|
|
185
|
+
const plan = buildAnalysisPlanV1(input());
|
|
186
|
+
expect(() => parseAnalysisPlanV1({ ...plan, digest: DIGEST_B })).toThrow();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("enforces plan-specific region and byte-limit relationships", () => {
|
|
190
|
+
expect(() =>
|
|
191
|
+
buildAnalysisPlanV1(input({ limits: { ...input().limits, maxRegions: 1 } }))
|
|
192
|
+
).toThrow(/maxRegions/u);
|
|
193
|
+
expect(() =>
|
|
194
|
+
buildAnalysisPlanV1(
|
|
195
|
+
input({
|
|
196
|
+
limits: {
|
|
197
|
+
...input().limits,
|
|
198
|
+
maxSourceBytes: 100,
|
|
199
|
+
maxFileBytes: 101,
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
)
|
|
203
|
+
).toThrow(/maxFileBytes/u);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("builds and verifies a plan at the 1,000-region public bound", () => {
|
|
207
|
+
const regions = Array.from({ length: 1_000 }, (_, index) => ({
|
|
208
|
+
...input().regions[0],
|
|
209
|
+
regionId: `region-${index.toString().padStart(4, "0")}`,
|
|
210
|
+
pathPattern: `src/area-${index}/**/*.css`,
|
|
211
|
+
}));
|
|
212
|
+
const built = buildAnalysisPlanV1(input({ regions }));
|
|
213
|
+
expect(built.regions).toHaveLength(1_000);
|
|
214
|
+
expect(parseAnalysisPlanV1(built)).toEqual(built);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("CoverageRegionV1", () => {
|
|
219
|
+
const complete: CoverageRegionV1[] = [
|
|
220
|
+
{ regionId: "components", state: "analyzed", fileCount: 4, byteCount: 400, diagnosticIds: [] },
|
|
221
|
+
{
|
|
222
|
+
regionId: "styles",
|
|
223
|
+
state: "unsupported",
|
|
224
|
+
fileCount: 2,
|
|
225
|
+
byteCount: 100,
|
|
226
|
+
diagnosticIds: ["d-1"],
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
it("derives required and optional summaries without trusting a submitted summary", () => {
|
|
231
|
+
const plan = buildAnalysisPlanV1(input());
|
|
232
|
+
expect(deriveCoverageSummaryV1(plan, complete)).toEqual({
|
|
233
|
+
plannedRegions: 2,
|
|
234
|
+
observedRegions: 2,
|
|
235
|
+
requiredRegions: 1,
|
|
236
|
+
requiredByState: {
|
|
237
|
+
analyzed: 1,
|
|
238
|
+
unsupported: 0,
|
|
239
|
+
excluded_by_contract: 0,
|
|
240
|
+
skipped_limit: 0,
|
|
241
|
+
failed: 0,
|
|
242
|
+
},
|
|
243
|
+
optionalByState: {
|
|
244
|
+
analyzed: 0,
|
|
245
|
+
unsupported: 1,
|
|
246
|
+
excluded_by_contract: 0,
|
|
247
|
+
skipped_limit: 0,
|
|
248
|
+
failed: 0,
|
|
249
|
+
},
|
|
250
|
+
eligibleFiles: 6,
|
|
251
|
+
analyzedFiles: 4,
|
|
252
|
+
analyzedBytes: 400,
|
|
253
|
+
});
|
|
254
|
+
expect(coverageArtifactDigestV1(plan, complete)).toBe(
|
|
255
|
+
"d970c7a194fc25c6fc81dde1b90e00379220e924bb5ed2dbf37885c634ae7182"
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it.each([
|
|
260
|
+
[[complete[0]], ["styles"], []],
|
|
261
|
+
[[complete[1]], [], ["components"]],
|
|
262
|
+
] as const)(
|
|
263
|
+
"names missing optional and required observations",
|
|
264
|
+
(coverage, optional, required) => {
|
|
265
|
+
try {
|
|
266
|
+
deriveCoverageSummaryV1(buildAnalysisPlanV1(input()), [...coverage]);
|
|
267
|
+
throw new Error("expected coverage validation to fail");
|
|
268
|
+
} catch (error) {
|
|
269
|
+
expect(error).toBeInstanceOf(CoverageValidationErrorV1);
|
|
270
|
+
const coverageError = error as CoverageValidationErrorV1;
|
|
271
|
+
expect(coverageError.missingOptionalRegionIds).toEqual(optional);
|
|
272
|
+
expect(coverageError.missingRequiredRegionIds).toEqual(required);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
it("rejects duplicate and unknown observations", () => {
|
|
278
|
+
expect(() =>
|
|
279
|
+
deriveCoverageSummaryV1(buildAnalysisPlanV1(input()), [...complete, complete[0]])
|
|
280
|
+
).toThrow(CoverageValidationErrorV1);
|
|
281
|
+
expect(() =>
|
|
282
|
+
deriveCoverageSummaryV1(buildAnalysisPlanV1(input()), [
|
|
283
|
+
...complete,
|
|
284
|
+
{ ...complete[0], regionId: "unknown" },
|
|
285
|
+
])
|
|
286
|
+
).toThrow(CoverageValidationErrorV1);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it.each([
|
|
290
|
+
["files", { maxSourceFiles: 5 }, complete],
|
|
291
|
+
["bytes", { maxSourceBytes: 450, maxFileBytes: 450 }, complete],
|
|
292
|
+
[
|
|
293
|
+
"diagnostics",
|
|
294
|
+
{ maxDiagnostics: 1 },
|
|
295
|
+
complete.map((row) =>
|
|
296
|
+
row.regionId === "styles" ? { ...row, diagnosticIds: ["d-1", "d-2"] } : row
|
|
297
|
+
),
|
|
298
|
+
],
|
|
299
|
+
] as const)("rejects coverage beyond the plan's %s limit", (_kind, limits, observations) => {
|
|
300
|
+
const plan = buildAnalysisPlanV1(input({ limits: { ...input().limits, ...limits } }));
|
|
301
|
+
expect(() => deriveCoverageSummaryV1(plan, [...observations])).toThrow(
|
|
302
|
+
CoverageValidationErrorV1
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("represents zero-region coverage without implying support", () => {
|
|
307
|
+
const plan = buildAnalysisPlanV1(input({ regions: [] }));
|
|
308
|
+
expect(deriveCoverageSummaryV1(plan, [])).toMatchObject({
|
|
309
|
+
plannedRegions: 0,
|
|
310
|
+
observedRegions: 0,
|
|
311
|
+
requiredRegions: 0,
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("keeps public digest helpers equal to built records", () => {
|
|
316
|
+
const plan = buildAnalysisPlanV1(input());
|
|
317
|
+
expect(analysisPlanDigestV1(plan)).toBe(plan.digest);
|
|
318
|
+
expect(analysisObligationsDigestV1(plan)).toBe(plan.obligationsDigest);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { canonicalPreimage, compareCanonicalStrings, sha256Hex } from "../contract/hash.js";
|
|
2
|
+
import { digestHexSchema, type DigestHex } from "../domain-ids.js";
|
|
3
|
+
import { parseAnalysisPlanV1 } from "./digest.js";
|
|
4
|
+
import {
|
|
5
|
+
coverageRegionV1Schema,
|
|
6
|
+
coverageSummaryV1Schema,
|
|
7
|
+
type AnalysisPlanV1,
|
|
8
|
+
type CoverageRegionV1,
|
|
9
|
+
type CoverageStateV1,
|
|
10
|
+
type CoverageSummaryV1,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
|
|
13
|
+
const COVERAGE_STATES: readonly CoverageStateV1[] = [
|
|
14
|
+
"analyzed",
|
|
15
|
+
"unsupported",
|
|
16
|
+
"excluded_by_contract",
|
|
17
|
+
"skipped_limit",
|
|
18
|
+
"failed",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function emptyCounts(): Record<CoverageStateV1, number> {
|
|
22
|
+
return {
|
|
23
|
+
analyzed: 0,
|
|
24
|
+
unsupported: 0,
|
|
25
|
+
excluded_by_contract: 0,
|
|
26
|
+
skipped_limit: 0,
|
|
27
|
+
failed: 0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class CoverageValidationErrorV1 extends TypeError {
|
|
32
|
+
constructor(
|
|
33
|
+
message: string,
|
|
34
|
+
readonly duplicateRegionIds: readonly string[] = [],
|
|
35
|
+
readonly unknownRegionIds: readonly string[] = [],
|
|
36
|
+
readonly missingRequiredRegionIds: readonly string[] = [],
|
|
37
|
+
readonly missingOptionalRegionIds: readonly string[] = []
|
|
38
|
+
) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "CoverageValidationErrorV1";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeCoverage(coverage: readonly CoverageRegionV1[]): CoverageRegionV1[] {
|
|
45
|
+
return coverage
|
|
46
|
+
.map((observation) =>
|
|
47
|
+
coverageRegionV1Schema.parse({
|
|
48
|
+
...observation,
|
|
49
|
+
diagnosticIds: [...observation.diagnosticIds].sort(),
|
|
50
|
+
})
|
|
51
|
+
)
|
|
52
|
+
.sort((left, right) => compareCanonicalStrings(left.regionId, right.regionId));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function deriveCoverageSummaryFromNormalizedV1(
|
|
56
|
+
plan: AnalysisPlanV1,
|
|
57
|
+
coverage: CoverageRegionV1[]
|
|
58
|
+
): CoverageSummaryV1 {
|
|
59
|
+
const planned = new Map(plan.regions.map((region) => [region.regionId, region]));
|
|
60
|
+
const seen = new Set<string>();
|
|
61
|
+
const duplicateRegionIds = new Set<string>();
|
|
62
|
+
const unknownRegionIds = new Set<string>();
|
|
63
|
+
|
|
64
|
+
for (const observation of coverage) {
|
|
65
|
+
if (seen.has(observation.regionId)) duplicateRegionIds.add(observation.regionId);
|
|
66
|
+
seen.add(observation.regionId);
|
|
67
|
+
if (!planned.has(observation.regionId)) unknownRegionIds.add(observation.regionId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const missingRequiredRegionIds = plan.regions
|
|
71
|
+
.filter((region) => region.required && !seen.has(region.regionId))
|
|
72
|
+
.map((region) => region.regionId);
|
|
73
|
+
const missingOptionalRegionIds = plan.regions
|
|
74
|
+
.filter((region) => !region.required && !seen.has(region.regionId))
|
|
75
|
+
.map((region) => region.regionId);
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
duplicateRegionIds.size > 0 ||
|
|
79
|
+
unknownRegionIds.size > 0 ||
|
|
80
|
+
missingRequiredRegionIds.length > 0 ||
|
|
81
|
+
missingOptionalRegionIds.length > 0
|
|
82
|
+
) {
|
|
83
|
+
throw new CoverageValidationErrorV1(
|
|
84
|
+
"Coverage observations must join exactly once to every planned region",
|
|
85
|
+
[...duplicateRegionIds].sort(),
|
|
86
|
+
[...unknownRegionIds].sort(),
|
|
87
|
+
missingRequiredRegionIds.sort(),
|
|
88
|
+
missingOptionalRegionIds.sort()
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const requiredByState = emptyCounts();
|
|
93
|
+
const optionalByState = emptyCounts();
|
|
94
|
+
let eligibleFiles = 0;
|
|
95
|
+
let analyzedFiles = 0;
|
|
96
|
+
let analyzedBytes = 0;
|
|
97
|
+
let observedBytes = 0;
|
|
98
|
+
let diagnosticCount = 0;
|
|
99
|
+
|
|
100
|
+
for (const observation of coverage) {
|
|
101
|
+
const region = planned.get(observation.regionId)!;
|
|
102
|
+
(region.required ? requiredByState : optionalByState)[observation.state] += 1;
|
|
103
|
+
if (observation.state !== "excluded_by_contract") eligibleFiles += observation.fileCount;
|
|
104
|
+
if (observation.state === "analyzed") {
|
|
105
|
+
analyzedFiles += observation.fileCount;
|
|
106
|
+
analyzedBytes += observation.byteCount;
|
|
107
|
+
}
|
|
108
|
+
observedBytes += observation.byteCount;
|
|
109
|
+
diagnosticCount += observation.diagnosticIds.length;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
!Number.isSafeInteger(eligibleFiles) ||
|
|
114
|
+
!Number.isSafeInteger(analyzedFiles) ||
|
|
115
|
+
!Number.isSafeInteger(analyzedBytes) ||
|
|
116
|
+
!Number.isSafeInteger(observedBytes) ||
|
|
117
|
+
!Number.isSafeInteger(diagnosticCount)
|
|
118
|
+
) {
|
|
119
|
+
throw new RangeError("Coverage totals exceeded the safe integer range");
|
|
120
|
+
}
|
|
121
|
+
if (eligibleFiles > plan.limits.maxSourceFiles) {
|
|
122
|
+
throw new CoverageValidationErrorV1("Coverage file total exceeds the analysis-plan limit");
|
|
123
|
+
}
|
|
124
|
+
if (observedBytes > plan.limits.maxSourceBytes) {
|
|
125
|
+
throw new CoverageValidationErrorV1("Coverage byte total exceeds the analysis-plan limit");
|
|
126
|
+
}
|
|
127
|
+
if (diagnosticCount > plan.limits.maxDiagnostics) {
|
|
128
|
+
throw new CoverageValidationErrorV1(
|
|
129
|
+
"Coverage diagnostic total exceeds the analysis-plan limit"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const state of COVERAGE_STATES) {
|
|
134
|
+
if (
|
|
135
|
+
!Number.isSafeInteger(requiredByState[state]) ||
|
|
136
|
+
!Number.isSafeInteger(optionalByState[state])
|
|
137
|
+
) {
|
|
138
|
+
throw new RangeError("Coverage count exceeded the safe integer range");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return coverageSummaryV1Schema.parse({
|
|
143
|
+
plannedRegions: plan.regions.length,
|
|
144
|
+
observedRegions: coverage.length,
|
|
145
|
+
requiredRegions: plan.regions.filter((region) => region.required).length,
|
|
146
|
+
requiredByState,
|
|
147
|
+
optionalByState,
|
|
148
|
+
eligibleFiles,
|
|
149
|
+
analyzedFiles,
|
|
150
|
+
analyzedBytes,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function deriveCoverageSummaryV1(
|
|
155
|
+
rawPlan: AnalysisPlanV1,
|
|
156
|
+
rawCoverage: readonly CoverageRegionV1[]
|
|
157
|
+
): CoverageSummaryV1 {
|
|
158
|
+
return deriveCoverageSummaryFromNormalizedV1(
|
|
159
|
+
parseAnalysisPlanV1(rawPlan),
|
|
160
|
+
normalizeCoverage(rawCoverage)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function coverageArtifactDigestV1(
|
|
165
|
+
rawPlan: AnalysisPlanV1,
|
|
166
|
+
rawCoverage: readonly CoverageRegionV1[]
|
|
167
|
+
): DigestHex {
|
|
168
|
+
const plan = parseAnalysisPlanV1(rawPlan);
|
|
169
|
+
const coverage = normalizeCoverage(rawCoverage);
|
|
170
|
+
const summary = deriveCoverageSummaryFromNormalizedV1(plan, coverage);
|
|
171
|
+
return digestHexSchema.parse(
|
|
172
|
+
sha256Hex(
|
|
173
|
+
canonicalPreimage({
|
|
174
|
+
schema: "analysis-coverage.v1",
|
|
175
|
+
analysisPlanDigest: plan.digest,
|
|
176
|
+
coverage,
|
|
177
|
+
summary,
|
|
178
|
+
})
|
|
179
|
+
)
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { canonicalPreimage, compareCanonicalStrings, sha256Hex } from "../contract/hash.js";
|
|
2
|
+
import { analysisPlanIdFromDigest, digestHexSchema, type DigestHex } from "../domain-ids.js";
|
|
3
|
+
import {
|
|
4
|
+
normalizeRepositoryBindingKeyV1,
|
|
5
|
+
repositoryBindingDigestV1,
|
|
6
|
+
} from "../repository-binding.js";
|
|
7
|
+
import {
|
|
8
|
+
analysisPlanInputV1Schema,
|
|
9
|
+
analysisPlanV1Schema,
|
|
10
|
+
type AnalysisPlanInputV1,
|
|
11
|
+
type AnalysisPlanV1,
|
|
12
|
+
type AnalysisRegionV1,
|
|
13
|
+
} from "./types.js";
|
|
14
|
+
|
|
15
|
+
const sortedUnique = (values: readonly string[]): string[] => [...new Set(values)].sort();
|
|
16
|
+
|
|
17
|
+
function semanticInput(input: AnalysisPlanInputV1 | AnalysisPlanV1): AnalysisPlanInputV1 {
|
|
18
|
+
return {
|
|
19
|
+
binding: input.binding,
|
|
20
|
+
source: input.source,
|
|
21
|
+
contract: input.contract,
|
|
22
|
+
config: input.config,
|
|
23
|
+
profile: input.profile,
|
|
24
|
+
regions: input.regions,
|
|
25
|
+
limits: input.limits,
|
|
26
|
+
evaluatorVersion: input.evaluatorVersion,
|
|
27
|
+
createdAt: input.createdAt,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeRegion(region: AnalysisRegionV1): AnalysisRegionV1 {
|
|
32
|
+
return {
|
|
33
|
+
...region,
|
|
34
|
+
requiredDialects: sortedUnique(region.requiredDialects),
|
|
35
|
+
analyzer: { ...region.analyzer },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeAnalysisPlanInputV1(
|
|
40
|
+
input: AnalysisPlanInputV1 | AnalysisPlanV1
|
|
41
|
+
): AnalysisPlanInputV1 {
|
|
42
|
+
const semantic = semanticInput(input);
|
|
43
|
+
const normalized = {
|
|
44
|
+
...semantic,
|
|
45
|
+
binding: normalizeRepositoryBindingKeyV1(semantic.binding),
|
|
46
|
+
source: { ...semantic.source },
|
|
47
|
+
contract: { ...semantic.contract },
|
|
48
|
+
config: { ...semantic.config },
|
|
49
|
+
profile: {
|
|
50
|
+
...semantic.profile,
|
|
51
|
+
capabilities: sortedUnique(semantic.profile.capabilities),
|
|
52
|
+
},
|
|
53
|
+
regions: semantic.regions
|
|
54
|
+
.map(normalizeRegion)
|
|
55
|
+
.sort((left, right) => compareCanonicalStrings(left.regionId, right.regionId)),
|
|
56
|
+
limits: { ...semantic.limits },
|
|
57
|
+
};
|
|
58
|
+
return analysisPlanInputV1Schema.parse(normalized);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function obligationsProjection(normalized: AnalysisPlanInputV1): Record<string, unknown> {
|
|
62
|
+
return {
|
|
63
|
+
schema: "analysis-obligations.v1",
|
|
64
|
+
bindingDigest: repositoryBindingDigestV1(normalized.binding),
|
|
65
|
+
contract: normalized.contract,
|
|
66
|
+
config: normalized.config,
|
|
67
|
+
profile: normalized.profile,
|
|
68
|
+
regions: normalized.regions,
|
|
69
|
+
limits: normalized.limits,
|
|
70
|
+
evaluatorVersion: normalized.evaluatorVersion,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function exactPlanProjection(normalized: AnalysisPlanInputV1): Record<string, unknown> {
|
|
75
|
+
return {
|
|
76
|
+
schema: "analysis-plan.v1",
|
|
77
|
+
bindingDigest: repositoryBindingDigestV1(normalized.binding),
|
|
78
|
+
source: {
|
|
79
|
+
commitId: normalized.source.commitId,
|
|
80
|
+
treeId: normalized.source.treeId,
|
|
81
|
+
acquiredBy: normalized.source.acquiredBy,
|
|
82
|
+
},
|
|
83
|
+
contract: normalized.contract,
|
|
84
|
+
config: normalized.config,
|
|
85
|
+
profile: normalized.profile,
|
|
86
|
+
regions: normalized.regions,
|
|
87
|
+
limits: normalized.limits,
|
|
88
|
+
evaluatorVersion: normalized.evaluatorVersion,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function analysisObligationsDigestV1(
|
|
93
|
+
input: AnalysisPlanInputV1 | AnalysisPlanV1
|
|
94
|
+
): DigestHex {
|
|
95
|
+
const normalized = normalizeAnalysisPlanInputV1(input);
|
|
96
|
+
return digestHexSchema.parse(sha256Hex(canonicalPreimage(obligationsProjection(normalized))));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function analysisPlanDigestV1(input: AnalysisPlanInputV1 | AnalysisPlanV1): DigestHex {
|
|
100
|
+
const normalized = normalizeAnalysisPlanInputV1(input);
|
|
101
|
+
return digestHexSchema.parse(sha256Hex(canonicalPreimage(exactPlanProjection(normalized))));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function buildAnalysisPlanV1(input: AnalysisPlanInputV1): AnalysisPlanV1 {
|
|
105
|
+
const normalized = normalizeAnalysisPlanInputV1(input);
|
|
106
|
+
const digest = digestHexSchema.parse(
|
|
107
|
+
sha256Hex(canonicalPreimage(exactPlanProjection(normalized)))
|
|
108
|
+
);
|
|
109
|
+
return analysisPlanV1Schema.parse({
|
|
110
|
+
schemaVersion: 1,
|
|
111
|
+
analysisPlanId: analysisPlanIdFromDigest(digest),
|
|
112
|
+
...normalized,
|
|
113
|
+
obligationsDigest: digestHexSchema.parse(
|
|
114
|
+
sha256Hex(canonicalPreimage(obligationsProjection(normalized)))
|
|
115
|
+
),
|
|
116
|
+
digest,
|
|
117
|
+
}) as AnalysisPlanV1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function parseAnalysisPlanV1(value: unknown): AnalysisPlanV1 {
|
|
121
|
+
const parsed = analysisPlanV1Schema.parse(value);
|
|
122
|
+
const rebuilt = buildAnalysisPlanV1({
|
|
123
|
+
binding: parsed.binding,
|
|
124
|
+
source: parsed.source,
|
|
125
|
+
contract: parsed.contract,
|
|
126
|
+
config: parsed.config,
|
|
127
|
+
profile: parsed.profile,
|
|
128
|
+
regions: parsed.regions,
|
|
129
|
+
limits: parsed.limits,
|
|
130
|
+
evaluatorVersion: parsed.evaluatorVersion,
|
|
131
|
+
createdAt: parsed.createdAt,
|
|
132
|
+
});
|
|
133
|
+
if (
|
|
134
|
+
parsed.analysisPlanId !== rebuilt.analysisPlanId ||
|
|
135
|
+
parsed.digest !== rebuilt.digest ||
|
|
136
|
+
parsed.obligationsDigest !== rebuilt.obligationsDigest
|
|
137
|
+
) {
|
|
138
|
+
throw new TypeError("AnalysisPlanV1 derived identity does not match its semantic projection");
|
|
139
|
+
}
|
|
140
|
+
return rebuilt;
|
|
141
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ANALYSIS_LIMIT_CEILINGS_V1,
|
|
3
|
+
ANALYSIS_PLAN_MAX_CAPABILITIES_V1,
|
|
4
|
+
ANALYSIS_PLAN_MAX_DIALECTS_PER_REGION_V1,
|
|
5
|
+
ANALYSIS_PLAN_MAX_REGIONS_V1,
|
|
6
|
+
analysisLimitsV1Schema,
|
|
7
|
+
analysisPlanInputV1Schema,
|
|
8
|
+
analysisPlanV1Schema,
|
|
9
|
+
analysisRegionV1Schema,
|
|
10
|
+
coverageRegionV1Schema,
|
|
11
|
+
coverageStateV1Schema,
|
|
12
|
+
coverageSummaryV1Schema,
|
|
13
|
+
} from "./types.js";
|
|
14
|
+
export type {
|
|
15
|
+
AnalysisLimitsV1,
|
|
16
|
+
AnalysisPlanInputV1,
|
|
17
|
+
AnalysisPlanV1,
|
|
18
|
+
AnalysisRegionV1,
|
|
19
|
+
CoverageRegionV1,
|
|
20
|
+
CoverageStateV1,
|
|
21
|
+
CoverageSummaryV1,
|
|
22
|
+
} from "./types.js";
|
|
23
|
+
export {
|
|
24
|
+
analysisObligationsDigestV1,
|
|
25
|
+
analysisPlanDigestV1,
|
|
26
|
+
buildAnalysisPlanV1,
|
|
27
|
+
normalizeAnalysisPlanInputV1,
|
|
28
|
+
parseAnalysisPlanV1,
|
|
29
|
+
} from "./digest.js";
|
|
30
|
+
export {
|
|
31
|
+
CoverageValidationErrorV1,
|
|
32
|
+
coverageArtifactDigestV1,
|
|
33
|
+
deriveCoverageSummaryV1,
|
|
34
|
+
} from "./coverage.js";
|