@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,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";