upstream-radar 0.40.0 → 0.41.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 (48) hide show
  1. package/README.md +123 -225
  2. package/dist/src/cli.js +11 -2
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/dsh-compatibility-ir.d.ts +98 -0
  5. package/dist/src/dsh-compatibility-ir.d.ts.map +1 -0
  6. package/dist/src/dsh-compatibility-ir.js +389 -0
  7. package/dist/src/dsh-compatibility-ir.js.map +1 -0
  8. package/dist/src/dsh-compatibility-issues.d.ts +44 -0
  9. package/dist/src/dsh-compatibility-issues.d.ts.map +1 -0
  10. package/dist/src/dsh-compatibility-issues.js +185 -0
  11. package/dist/src/dsh-compatibility-issues.js.map +1 -0
  12. package/dist/src/dsh-compatibility-ledger.d.ts +150 -0
  13. package/dist/src/dsh-compatibility-ledger.d.ts.map +1 -0
  14. package/dist/src/dsh-compatibility-ledger.js +645 -0
  15. package/dist/src/dsh-compatibility-ledger.js.map +1 -0
  16. package/dist/src/dsh-install-observation.d.ts +88 -1
  17. package/dist/src/dsh-install-observation.d.ts.map +1 -1
  18. package/dist/src/dsh-install-observation.js +579 -6
  19. package/dist/src/dsh-install-observation.js.map +1 -1
  20. package/dist/src/dsh-install-plan.d.ts +16 -6
  21. package/dist/src/dsh-install-plan.d.ts.map +1 -1
  22. package/dist/src/dsh-install-plan.js +272 -46
  23. package/dist/src/dsh-install-plan.js.map +1 -1
  24. package/dist/src/dsh-plugin.d.ts.map +1 -1
  25. package/dist/src/dsh-plugin.js +6 -2
  26. package/dist/src/dsh-plugin.js.map +1 -1
  27. package/dist/src/dsh-runtime.d.ts +6 -0
  28. package/dist/src/dsh-runtime.d.ts.map +1 -1
  29. package/dist/src/dsh-runtime.js +38 -0
  30. package/dist/src/dsh-runtime.js.map +1 -1
  31. package/dist/src/index.d.ts +7 -4
  32. package/dist/src/index.d.ts.map +1 -1
  33. package/dist/src/index.js +4 -1
  34. package/dist/src/index.js.map +1 -1
  35. package/dist/src/installed-graph.d.ts.map +1 -1
  36. package/dist/src/installed-graph.js +105 -26
  37. package/dist/src/installed-graph.js.map +1 -1
  38. package/dist/src/radar-types.d.ts +15 -0
  39. package/dist/src/radar-types.d.ts.map +1 -1
  40. package/dist/src/semver.d.ts.map +1 -1
  41. package/dist/src/semver.js +7 -1
  42. package/dist/src/semver.js.map +1 -1
  43. package/dist/src/version.d.ts +1 -1
  44. package/dist/src/version.js +1 -1
  45. package/docs/README.zh-CN.md +25 -23
  46. package/package.json +1 -1
  47. package/schemas/dsh-compatibility-ir.schema.json +89 -0
  48. package/schemas/dsh-install-observation.schema.json +135 -2
@@ -0,0 +1,645 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { parseNpmSpec } from './npm.js';
3
+ /**
4
+ * Durable, current-state evidence for the exact plugin/DSH/runtime cells that
5
+ * Radar promises to keep checking. This is deliberately a ledger, not a test
6
+ * history database: one entry represents the latest trustworthy observation
7
+ * for one active cell.
8
+ */
9
+ export const DSH_COMPATIBILITY_LEDGER_SCHEMA = 'upstream-radar.dsh-compatibility-ledger/v1alpha1';
10
+ const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
11
+ const CASE_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
12
+ const FINGERPRINT = /^sha256:[a-f0-9]{64}$/;
13
+ const RESULTS = new Set(['compatible', 'runtime-incompatible', 'peer-contract-incompatible', 'install-failed', 'load-failed', 'unknown']);
14
+ const MAX_ENTRIES = 500;
15
+ const MAX_REPORTS = 100;
16
+ function record(value, label) {
17
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
18
+ throw new Error(`${label} must be an object`);
19
+ return value;
20
+ }
21
+ function boundedString(value, label, maximum) {
22
+ if (typeof value !== 'string' || value.trim() === '' || value.length > maximum) {
23
+ throw new Error(`${label} must be a non-empty string no longer than ${maximum} characters`);
24
+ }
25
+ return value;
26
+ }
27
+ function optionalBoundedString(value, label, maximum) {
28
+ return value === undefined ? undefined : boundedString(value, label, maximum);
29
+ }
30
+ function exactVersion(value, label) {
31
+ const parsed = boundedString(value, label, 256);
32
+ if (!EXACT_VERSION.test(parsed))
33
+ throw new Error(`${label} must be an exact semantic version`);
34
+ return parsed;
35
+ }
36
+ function caseId(value, label) {
37
+ const parsed = boundedString(value, label, 64);
38
+ if (!CASE_ID.test(parsed))
39
+ throw new Error(`${label} must be a short lowercase label`);
40
+ return parsed;
41
+ }
42
+ function exactSpec(value, label) {
43
+ const raw = boundedString(value, label, 512);
44
+ const parsed = parseNpmSpec(raw);
45
+ if (!EXACT_VERSION.test(parsed.version))
46
+ throw new Error(`${label} must be an exact npm package coordinate`);
47
+ return `${parsed.name}@${parsed.version}`;
48
+ }
49
+ function nodeMajor(value, label) {
50
+ if (!Number.isSafeInteger(value) || value < 16 || value > 40) {
51
+ throw new Error(`${label} must be a supported Node.js major version`);
52
+ }
53
+ return value;
54
+ }
55
+ function positiveInteger(value, label, maximum = Number.MAX_SAFE_INTEGER) {
56
+ if (!Number.isSafeInteger(value) || value < 0 || value > maximum) {
57
+ throw new Error(`${label} must be a bounded non-negative integer`);
58
+ }
59
+ return value;
60
+ }
61
+ function peerStaticUsage(value, label) {
62
+ const usage = boundedString(value, label, 64);
63
+ if (usage !== 'runtime-import-observed' && usage !== 'type-only-reference-observed'
64
+ && usage !== 'no-literal-reference-observed' && usage !== 'scan-incomplete') {
65
+ throw new Error(`${label} is unsupported`);
66
+ }
67
+ return usage;
68
+ }
69
+ function optionalDigest(value, label) {
70
+ if (value === undefined)
71
+ return undefined;
72
+ const digest = boundedString(value, label, 128);
73
+ if (!/^sha256:[a-f0-9]{64}$/.test(digest))
74
+ throw new Error(`${label} must be a sha256 digest`);
75
+ return digest;
76
+ }
77
+ function optionalBareSha256(value, label) {
78
+ if (value === undefined)
79
+ return undefined;
80
+ const digest = boundedString(value, label, 64);
81
+ if (!/^[a-f0-9]{64}$/.test(digest))
82
+ throw new Error(`${label} must be a SHA-256 hex digest`);
83
+ return digest;
84
+ }
85
+ function parseLifecycleScripts(value, label) {
86
+ if (!Array.isArray(value) || value.length > 4)
87
+ throw new Error(`${label} must be an array of at most four lifecycle script names`);
88
+ const allowed = new Set(['preinstall', 'install', 'postinstall', 'prepare']);
89
+ const scripts = value.map((item, index) => boundedString(item, `${label}[${index}]`, 32));
90
+ if (scripts.some(script => !allowed.has(script)))
91
+ throw new Error(`${label} contains an unsupported lifecycle script name`);
92
+ if (new Set(scripts).size !== scripts.length)
93
+ throw new Error(`${label} must not contain duplicate lifecycle script names`);
94
+ return scripts.sort();
95
+ }
96
+ function parseGraphGaps(item, label, count, field = 'unresolvedDependencies') {
97
+ const rawGaps = item[field];
98
+ if (rawGaps !== undefined && (!Array.isArray(rawGaps) || rawGaps.length > 32)) {
99
+ throw new Error(`${label}.${field} must be an array of at most 32 graph gaps`);
100
+ }
101
+ const unresolvedDependencies = rawGaps?.map((value, index) => {
102
+ const gap = record(value, `${label}.${field}[${index}]`);
103
+ const kind = boundedString(gap.kind, `${label}.${field}[${index}].kind`, 32);
104
+ if (!['runtime', 'development', 'optional', 'peer', 'host-runtime'].includes(kind)) {
105
+ throw new Error(`${label}.${field}[${index}].kind is unsupported`);
106
+ }
107
+ return {
108
+ from: boundedString(gap.from, `${label}.${field}[${index}].from`, 512),
109
+ name: boundedString(gap.name, `${label}.${field}[${index}].name`, 214),
110
+ spec: boundedString(gap.spec, `${label}.${field}[${index}].spec`, 512),
111
+ kind: kind,
112
+ };
113
+ });
114
+ if (unresolvedDependencies !== undefined && count !== undefined && unresolvedDependencies.length > count) {
115
+ throw new Error(`${label}.${field} cannot exceed its reported count`);
116
+ }
117
+ return unresolvedDependencies;
118
+ }
119
+ function parsePluginPeerContracts(value, label) {
120
+ if (value === undefined)
121
+ return undefined;
122
+ const item = record(value, label);
123
+ const declared = positiveInteger(item.declared, `${label}.declared`, 100_000);
124
+ const satisfied = positiveInteger(item.satisfied, `${label}.satisfied`, 100_000);
125
+ const mismatched = positiveInteger(item.mismatched, `${label}.mismatched`, 100_000);
126
+ const indeterminate = positiveInteger(item.indeterminate, `${label}.indeterminate`, 100_000);
127
+ const missing = positiveInteger(item.missing, `${label}.missing`, 100_000);
128
+ if (satisfied + mismatched + indeterminate + missing !== declared) {
129
+ throw new Error(`${label} counts must add up to declared`);
130
+ }
131
+ const rawRelations = item.relations;
132
+ if (!Array.isArray(rawRelations) || rawRelations.length > 64 || rawRelations.length !== declared) {
133
+ throw new Error(`${label}.relations must contain every declared peer, up to 64 entries`);
134
+ }
135
+ const relations = rawRelations.map((value, index) => {
136
+ const relation = record(value, `${label}.relations[${index}]`);
137
+ const status = boundedString(relation.status, `${label}.relations[${index}].status`, 32);
138
+ if (status !== 'satisfied' && status !== 'mismatched' && status !== 'indeterminate' && status !== 'missing') {
139
+ throw new Error(`${label}.relations[${index}].status is unsupported`);
140
+ }
141
+ const resolvedVersion = relation.resolvedVersion === undefined
142
+ ? undefined
143
+ : boundedString(relation.resolvedVersion, `${label}.relations[${index}].resolvedVersion`, 256);
144
+ if ((status === 'satisfied' || status === 'mismatched') && resolvedVersion === undefined) {
145
+ throw new Error(`${label}.relations[${index}] requires a resolvedVersion`);
146
+ }
147
+ if (status === 'missing' && resolvedVersion !== undefined) {
148
+ throw new Error(`${label}.relations[${index}].resolvedVersion must be absent for a missing peer`);
149
+ }
150
+ return {
151
+ name: boundedString(relation.name, `${label}.relations[${index}].name`, 214),
152
+ required: boundedString(relation.required, `${label}.relations[${index}].required`, 512),
153
+ status,
154
+ staticUsage: peerStaticUsage(relation.staticUsage, `${label}.relations[${index}].staticUsage`),
155
+ ...(resolvedVersion === undefined ? {} : { resolvedVersion }),
156
+ };
157
+ }).sort((left, right) => left.name.localeCompare(right.name));
158
+ if (new Set(relations.map(relation => relation.name)).size !== relations.length) {
159
+ throw new Error(`${label}.relations must not contain duplicate peer names`);
160
+ }
161
+ const relationCounts = {
162
+ satisfied: relations.filter(relation => relation.status === 'satisfied').length,
163
+ mismatched: relations.filter(relation => relation.status === 'mismatched').length,
164
+ indeterminate: relations.filter(relation => relation.status === 'indeterminate').length,
165
+ missing: relations.filter(relation => relation.status === 'missing').length,
166
+ };
167
+ if (relationCounts.satisfied !== satisfied || relationCounts.mismatched !== mismatched
168
+ || relationCounts.indeterminate !== indeterminate || relationCounts.missing !== missing) {
169
+ throw new Error(`${label}.relations do not match the reported peer contract counts`);
170
+ }
171
+ const rawIssues = item.issues;
172
+ if (rawIssues !== undefined && (!Array.isArray(rawIssues) || rawIssues.length > 32)) {
173
+ throw new Error(`${label}.issues must be an array of at most 32 peer contract issues`);
174
+ }
175
+ const issues = rawIssues?.map((value, index) => {
176
+ const issue = record(value, `${label}.issues[${index}]`);
177
+ const status = boundedString(issue.status, `${label}.issues[${index}].status`, 32);
178
+ if (status !== 'mismatched' && status !== 'indeterminate' && status !== 'missing') {
179
+ throw new Error(`${label}.issues[${index}].status is unsupported`);
180
+ }
181
+ const resolvedVersion = issue.resolvedVersion === undefined
182
+ ? undefined
183
+ : boundedString(issue.resolvedVersion, `${label}.issues[${index}].resolvedVersion`, 256);
184
+ if (status === 'missing' && resolvedVersion !== undefined) {
185
+ throw new Error(`${label}.issues[${index}].resolvedVersion must be absent for a missing peer`);
186
+ }
187
+ return {
188
+ name: boundedString(issue.name, `${label}.issues[${index}].name`, 214),
189
+ required: boundedString(issue.required, `${label}.issues[${index}].required`, 512),
190
+ status,
191
+ staticUsage: peerStaticUsage(issue.staticUsage, `${label}.issues[${index}].staticUsage`),
192
+ ...(resolvedVersion === undefined ? {} : { resolvedVersion }),
193
+ };
194
+ });
195
+ if (issues !== undefined && issues.length > mismatched + indeterminate + missing) {
196
+ throw new Error(`${label}.issues cannot exceed the number of non-satisfied peers`);
197
+ }
198
+ return {
199
+ declared,
200
+ satisfied,
201
+ mismatched,
202
+ indeterminate,
203
+ missing,
204
+ relations,
205
+ ...(issues === undefined ? {} : { issues }),
206
+ };
207
+ }
208
+ function parseProfileLockfile(value, label) {
209
+ if (value === undefined)
210
+ return undefined;
211
+ const item = record(value, label);
212
+ const sha256 = optionalBareSha256(item.sha256, `${label}.sha256`);
213
+ if (sha256 === undefined)
214
+ throw new Error(`${label}.sha256 is required`);
215
+ const graphDigest = optionalDigest(item.graphDigest, `${label}.graphDigest`);
216
+ const nodes = item.nodes === undefined ? undefined : positiveInteger(item.nodes, `${label}.nodes`, 100_000);
217
+ const edges = item.edges === undefined ? undefined : positiveInteger(item.edges, `${label}.edges`, 250_000);
218
+ const unresolved = item.unresolved === undefined ? undefined : positiveInteger(item.unresolved, `${label}.unresolved`, 250_000);
219
+ const unresolvedDependencies = parseGraphGaps(item, label, unresolved);
220
+ return {
221
+ sha256,
222
+ bytes: positiveInteger(item.bytes, `${label}.bytes`, 64 * 1024 * 1024),
223
+ ...(graphDigest === undefined ? {} : { graphDigest }),
224
+ ...(nodes === undefined ? {} : { nodes }),
225
+ ...(edges === undefined ? {} : { edges }),
226
+ ...(unresolved === undefined ? {} : { unresolved }),
227
+ ...(unresolvedDependencies === undefined ? {} : { unresolvedDependencies }),
228
+ };
229
+ }
230
+ function parseRuntimeGraph(value, label) {
231
+ if (value === undefined)
232
+ return undefined;
233
+ const item = record(value, label);
234
+ const digest = optionalDigest(item.digest, `${label}.digest`);
235
+ if (digest === undefined)
236
+ throw new Error(`${label}.digest is required`);
237
+ const unresolved = positiveInteger(item.unresolved, `${label}.unresolved`, 250_000);
238
+ const hostRuntime = item.hostRuntime === undefined ? undefined : record(item.hostRuntime, `${label}.hostRuntime`);
239
+ let parsedHostRuntime;
240
+ if (hostRuntime !== undefined) {
241
+ const source = boundedString(hostRuntime.source, `${label}.hostRuntime.source`, 64);
242
+ if (source !== 'dsh-profile-fallback' && source !== 'dsh-process') {
243
+ throw new Error(`${label}.hostRuntime.source is unsupported`);
244
+ }
245
+ const dshVersion = hostRuntime.dshVersion === undefined
246
+ ? undefined
247
+ : exactVersion(hostRuntime.dshVersion, `${label}.hostRuntime.dshVersion`);
248
+ parsedHostRuntime = {
249
+ source,
250
+ resolvedNodes: positiveInteger(hostRuntime.resolvedNodes, `${label}.hostRuntime.resolvedNodes`, 100_000),
251
+ ...(dshVersion === undefined ? {} : { dshVersion }),
252
+ };
253
+ }
254
+ const unresolvedDependencies = parseGraphGaps(item, label, unresolved);
255
+ const optionalUnavailable = item.optionalUnavailable === undefined
256
+ ? undefined
257
+ : positiveInteger(item.optionalUnavailable, `${label}.optionalUnavailable`, 250_000);
258
+ const optionalUnavailableDependencies = parseGraphGaps(item, label, optionalUnavailable, 'optionalUnavailableDependencies');
259
+ const pluginPeerContracts = parsePluginPeerContracts(item.pluginPeerContracts, `${label}.pluginPeerContracts`);
260
+ return {
261
+ digest,
262
+ nodes: positiveInteger(item.nodes, `${label}.nodes`, 100_000),
263
+ edges: positiveInteger(item.edges, `${label}.edges`, 250_000),
264
+ unresolved,
265
+ ...(unresolvedDependencies === undefined ? {} : { unresolvedDependencies }),
266
+ ...(optionalUnavailable === undefined ? {} : { optionalUnavailable }),
267
+ ...(optionalUnavailableDependencies === undefined ? {} : { optionalUnavailableDependencies }),
268
+ ...(pluginPeerContracts === undefined ? {} : { pluginPeerContracts }),
269
+ ...(parsedHostRuntime === undefined ? {} : { hostRuntime: parsedHostRuntime }),
270
+ };
271
+ }
272
+ function parseEntry(value, index) {
273
+ const item = record(value, `entries[${index}]`);
274
+ const runtime = record(item.runtime, `entries[${index}].runtime`);
275
+ const artifact = record(item.artifact, `entries[${index}].artifact`);
276
+ const observer = record(item.observer, `entries[${index}].observer`);
277
+ const result = boundedString(item.result, `entries[${index}].result`, 64);
278
+ if (!RESULTS.has(result))
279
+ throw new Error(`entries[${index}].result is not a supported install observation result`);
280
+ const staticFingerprint = boundedString(item.staticFingerprint, `entries[${index}].staticFingerprint`, 80);
281
+ const contractFingerprint = boundedString(item.contractFingerprint, `entries[${index}].contractFingerprint`, 80);
282
+ if (!FINGERPRINT.test(staticFingerprint) || !FINGERPRINT.test(contractFingerprint)) {
283
+ throw new Error(`entries[${index}] fingerprints must be SHA-256 digests`);
284
+ }
285
+ const observedAt = boundedString(item.observedAt, `entries[${index}].observedAt`, 64);
286
+ if (!Number.isFinite(Date.parse(observedAt)))
287
+ throw new Error(`entries[${index}].observedAt must be an ISO timestamp`);
288
+ const resolution = item.resolution === undefined ? undefined : record(item.resolution, `entries[${index}].resolution`);
289
+ const profileLockfile = resolution === undefined ? undefined : parseProfileLockfile(resolution.profileLockfile, `entries[${index}].resolution.profileLockfile`);
290
+ const runtimeGraph = resolution === undefined ? undefined : parseRuntimeGraph(resolution.runtimeGraph, `entries[${index}].resolution.runtimeGraph`);
291
+ const sha256 = optionalBareSha256(artifact.sha256, `entries[${index}].artifact.sha256`);
292
+ const integrity = optionalBoundedString(artifact.integrity, `entries[${index}].artifact.integrity`, 1_024);
293
+ const nodeEngine = optionalBoundedString(artifact.nodeEngine, `entries[${index}].artifact.nodeEngine`, 512);
294
+ const pnpmVersion = runtime.pnpmVersion === undefined ? undefined : exactVersion(runtime.pnpmVersion, `entries[${index}].runtime.pnpmVersion`);
295
+ return {
296
+ caseId: caseId(item.caseId, `entries[${index}].caseId`),
297
+ targetId: caseId(item.targetId, `entries[${index}].targetId`),
298
+ plugin: exactSpec(item.plugin, `entries[${index}].plugin`),
299
+ dshVersion: exactVersion(item.dshVersion, `entries[${index}].dshVersion`),
300
+ runtime: {
301
+ nodeMajor: nodeMajor(runtime.nodeMajor, `entries[${index}].runtime.nodeMajor`),
302
+ nodeVersion: exactVersion(runtime.nodeVersion, `entries[${index}].runtime.nodeVersion`),
303
+ platform: boundedString(runtime.platform, `entries[${index}].runtime.platform`, 64),
304
+ architecture: boundedString(runtime.architecture, `entries[${index}].runtime.architecture`, 64),
305
+ ...(pnpmVersion === undefined ? {} : { pnpmVersion }),
306
+ },
307
+ staticFingerprint,
308
+ contractFingerprint,
309
+ observedAt,
310
+ result,
311
+ reason: boundedString(item.reason, `entries[${index}].reason`, 4_096),
312
+ artifact: {
313
+ lifecycleScripts: parseLifecycleScripts(artifact.lifecycleScripts, `entries[${index}].artifact.lifecycleScripts`),
314
+ ...(sha256 === undefined ? {} : { sha256 }),
315
+ ...(integrity === undefined ? {} : { integrity }),
316
+ ...(nodeEngine === undefined ? {} : { nodeEngine }),
317
+ },
318
+ ...(profileLockfile === undefined && runtimeGraph === undefined
319
+ ? {}
320
+ : { resolution: {
321
+ ...(profileLockfile === undefined ? {} : { profileLockfile }),
322
+ ...(runtimeGraph === undefined ? {} : { runtimeGraph }),
323
+ } }),
324
+ observer: {
325
+ schema: boundedString(observer.schema, `entries[${index}].observer.schema`, 256),
326
+ version: boundedString(observer.version, `entries[${index}].observer.version`, 256),
327
+ },
328
+ };
329
+ }
330
+ export function emptyDshCompatibilityLedger() {
331
+ return { schema: DSH_COMPATIBILITY_LEDGER_SCHEMA, entries: [] };
332
+ }
333
+ export function parseDshCompatibilityLedger(input) {
334
+ const root = record(input, 'DSH compatibility ledger');
335
+ if (root.schema !== DSH_COMPATIBILITY_LEDGER_SCHEMA) {
336
+ throw new Error(`DSH compatibility ledger schema must be ${DSH_COMPATIBILITY_LEDGER_SCHEMA}`);
337
+ }
338
+ if (!Array.isArray(root.entries) || root.entries.length > MAX_ENTRIES) {
339
+ throw new Error(`DSH compatibility ledger entries must be an array of at most ${MAX_ENTRIES} entries`);
340
+ }
341
+ const entries = root.entries.map(parseEntry);
342
+ const ids = new Set();
343
+ for (const entry of entries) {
344
+ if (ids.has(entry.caseId))
345
+ throw new Error(`duplicate DSH compatibility ledger caseId: ${entry.caseId}`);
346
+ ids.add(entry.caseId);
347
+ }
348
+ entries.sort((left, right) => left.caseId.localeCompare(right.caseId));
349
+ return { schema: DSH_COMPATIBILITY_LEDGER_SCHEMA, entries };
350
+ }
351
+ function canonicalize(value) {
352
+ if (Array.isArray(value))
353
+ return value.map(canonicalize);
354
+ if (typeof value !== 'object' || value === null)
355
+ return value;
356
+ return Object.fromEntries(Object.entries(value)
357
+ .sort(([left], [right]) => left.localeCompare(right))
358
+ .map(([key, item]) => [key, canonicalize(item)]));
359
+ }
360
+ function fingerprint(value) {
361
+ return `sha256:${createHash('sha256').update(JSON.stringify(canonicalize(value))).digest('hex')}`;
362
+ }
363
+ /** Static facts that may invalidate a prior runtime observation. */
364
+ export function createDshCompatibilityStaticFingerprint(value) {
365
+ return fingerprint(value);
366
+ }
367
+ /** The controlled execution contract, deliberately separate from source facts. */
368
+ export function createDshCompatibilityContractFingerprint(value) {
369
+ return fingerprint({
370
+ probe: 'dsh-install/v1alpha1',
371
+ platform: 'linux',
372
+ architecture: 'x64',
373
+ packageManager: 'pnpm@11.7.0',
374
+ image: `node:${value.nodeMajor}-bookworm-slim`,
375
+ plugin: value.plugin,
376
+ dshVersion: value.dshVersion,
377
+ allowedBuilds: [...value.allowedBuilds].sort(),
378
+ });
379
+ }
380
+ export function dshCompatibilityCaseId(targetId, runtimeProfileId) {
381
+ const combined = `${targetId}-${runtimeProfileId}`;
382
+ if (!CASE_ID.test(combined))
383
+ throw new Error(`DSH compatibility case id is invalid: ${combined}`);
384
+ return combined;
385
+ }
386
+ function reportRecord(value, label) {
387
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
388
+ throw new Error(`${label} is not an object`);
389
+ return value;
390
+ }
391
+ function reportString(value, label, maximum) {
392
+ if (typeof value !== 'string' || value === '' || value.length > maximum)
393
+ throw new Error(`${label} is invalid`);
394
+ return value;
395
+ }
396
+ function parseExpectedCases(input) {
397
+ if (input.length > MAX_REPORTS)
398
+ throw new Error(`compatibility reconciliation accepts at most ${MAX_REPORTS} expected cases`);
399
+ const expected = new Map();
400
+ for (const item of input) {
401
+ const id = caseId(item.id, 'expected case id');
402
+ if (expected.has(id))
403
+ throw new Error(`duplicate expected compatibility case id: ${id}`);
404
+ const plugin = exactSpec(item.plugin, `expected case ${id} plugin`);
405
+ const dshVersion = exactVersion(item.dshVersion, `expected case ${id} DSH version`);
406
+ const allowedBuilds = item.allowedBuilds === '' ? [] : item.allowedBuilds.split(',');
407
+ if (allowedBuilds.some(name => !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(name))) {
408
+ throw new Error(`expected case ${id} contains an invalid approved dependency build`);
409
+ }
410
+ if (new Set(allowedBuilds).size !== allowedBuilds.length)
411
+ throw new Error(`expected case ${id} contains duplicate approved dependency builds`);
412
+ if (!FINGERPRINT.test(item.staticFingerprint) || !FINGERPRINT.test(item.contractFingerprint)) {
413
+ throw new Error(`expected case ${id} has an invalid fingerprint`);
414
+ }
415
+ expected.set(id, {
416
+ ...item,
417
+ id,
418
+ targetId: caseId(item.targetId, `expected case ${id} targetId`),
419
+ plugin,
420
+ dshVersion,
421
+ nodeMajor: nodeMajor(item.nodeMajor, `expected case ${id} nodeMajor`),
422
+ allowedBuilds: allowedBuilds.sort().join(','),
423
+ reasons: [...new Set(item.reasons)].sort(),
424
+ });
425
+ }
426
+ return expected;
427
+ }
428
+ function parseObservationReport(value, expected) {
429
+ const report = reportRecord(value, `report for ${expected.id}`);
430
+ if (report.schema !== 'upstream-radar.dsh-install-observation/v1alpha1')
431
+ throw new Error('report schema is not a DSH install observation');
432
+ if (report.probe !== 'dsh-install' || report.scope !== 'install-and-load-behavior')
433
+ throw new Error('report does not describe an install-and-load observation');
434
+ if (caseId(report.caseId, 'report caseId') !== expected.id)
435
+ throw new Error('report caseId does not match the scheduled case');
436
+ const dshVersion = exactVersion(report.dshVersion, 'report dshVersion');
437
+ if (dshVersion !== expected.dshVersion)
438
+ throw new Error(`report DSH version ${dshVersion} does not match scheduled ${expected.dshVersion}`);
439
+ const artifact = reportRecord(report.artifact, 'report artifact');
440
+ const plugin = exactSpec(artifact.spec, 'report artifact spec');
441
+ if (plugin !== expected.plugin)
442
+ throw new Error(`report artifact ${plugin} does not match scheduled ${expected.plugin}`);
443
+ const runtime = reportRecord(report.runtime, 'report runtime');
444
+ const runtimeNodeVersion = exactVersion(runtime.nodeVersion, 'report runtime.nodeVersion');
445
+ const actualMajor = Number(runtimeNodeVersion.split('.')[0]);
446
+ if (actualMajor !== expected.nodeMajor)
447
+ throw new Error(`report Node ${runtimeNodeVersion} does not match scheduled Node ${expected.nodeMajor}`);
448
+ const packageManager = reportRecord(runtime.packageManager, 'report runtime.packageManager');
449
+ if (packageManager.name !== 'pnpm')
450
+ throw new Error('report package manager is not pnpm');
451
+ const pnpmVersion = packageManager.version === undefined ? undefined : exactVersion(packageManager.version, 'report runtime.packageManager.version');
452
+ const boundary = reportRecord(report.boundary, 'report boundary');
453
+ const approved = parseLifecycleBuilds(boundary.approvedDependencyBuilds, 'report boundary.approvedDependencyBuilds');
454
+ if (approved.join(',') !== expected.allowedBuilds)
455
+ throw new Error('report approved dependency builds do not match the scheduled policy');
456
+ const result = reportString(report.result, 'report result', 64);
457
+ if (!RESULTS.has(result))
458
+ throw new Error('report result is unsupported');
459
+ const lifecycleScripts = parseLifecycleScripts(artifact.lifecycleScripts, 'report artifact.lifecycleScripts');
460
+ const sha256 = optionalBareSha256(artifact.sha256, 'report artifact.sha256');
461
+ const integrity = optionalBoundedString(artifact.integrity, 'report artifact.integrity', 1_024);
462
+ const nodeEngine = optionalBoundedString(artifact.nodeEngine, 'report artifact.nodeEngine', 512);
463
+ const resolutionRecord = report.resolution === undefined ? undefined : reportRecord(report.resolution, 'report resolution');
464
+ const profileLockfile = resolutionRecord === undefined ? undefined : parseProfileLockfile(resolutionRecord.profileLockfile, 'report resolution.profileLockfile');
465
+ const runtimeGraph = resolutionRecord === undefined ? undefined : parseRuntimeGraph(resolutionRecord.runtimeGraph, 'report resolution.runtimeGraph');
466
+ const tool = reportRecord(report.tool, 'report tool');
467
+ if (tool.name !== 'upstream-radar')
468
+ throw new Error('report was not produced by upstream-radar');
469
+ const observedAt = reportString(report.completedAt, 'report completedAt', 64);
470
+ if (!Number.isFinite(Date.parse(observedAt)))
471
+ throw new Error('report completedAt is not a timestamp');
472
+ return {
473
+ caseId: expected.id,
474
+ targetId: expected.targetId,
475
+ plugin,
476
+ dshVersion,
477
+ runtime: {
478
+ nodeMajor: expected.nodeMajor,
479
+ nodeVersion: runtimeNodeVersion,
480
+ platform: reportString(runtime.platform, 'report runtime.platform', 64),
481
+ architecture: reportString(runtime.architecture, 'report runtime.architecture', 64),
482
+ ...(pnpmVersion === undefined ? {} : { pnpmVersion }),
483
+ },
484
+ staticFingerprint: expected.staticFingerprint,
485
+ contractFingerprint: expected.contractFingerprint,
486
+ observedAt,
487
+ result,
488
+ reason: reportString(report.reason, 'report reason', 4_096),
489
+ artifact: {
490
+ lifecycleScripts,
491
+ ...(sha256 === undefined ? {} : { sha256 }),
492
+ ...(integrity === undefined ? {} : { integrity }),
493
+ ...(nodeEngine === undefined ? {} : { nodeEngine }),
494
+ },
495
+ ...(profileLockfile === undefined && runtimeGraph === undefined
496
+ ? {}
497
+ : { resolution: {
498
+ ...(profileLockfile === undefined ? {} : { profileLockfile }),
499
+ ...(runtimeGraph === undefined ? {} : { runtimeGraph }),
500
+ } }),
501
+ observer: {
502
+ schema: reportString(report.schema, 'report schema', 256),
503
+ version: reportString(tool.version, 'report tool.version', 256),
504
+ },
505
+ };
506
+ }
507
+ function parseLifecycleBuilds(value, label) {
508
+ if (!Array.isArray(value) || value.length > 16)
509
+ throw new Error(`${label} must be an array of at most 16 package names`);
510
+ const names = value.map((item, index) => reportString(item, `${label}[${index}]`, 214));
511
+ if (names.some(name => !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(name))) {
512
+ throw new Error(`${label} contains an invalid package name`);
513
+ }
514
+ if (new Set(names).size !== names.length)
515
+ throw new Error(`${label} contains duplicate package names`);
516
+ return names.sort();
517
+ }
518
+ function isIncompatible(result) {
519
+ return result !== 'compatible';
520
+ }
521
+ function sameResolution(previous, current) {
522
+ const beforeLockfile = previous?.profileLockfile;
523
+ const afterLockfile = current?.profileLockfile;
524
+ const sameLockfile = beforeLockfile === undefined || afterLockfile === undefined
525
+ ? beforeLockfile === afterLockfile
526
+ // pnpm can rewrite non-semantic metadata. Prefer the canonical graph when
527
+ // both runs established one; only fall back to raw lockfile bytes when it
528
+ // could not be parsed.
529
+ : beforeLockfile.graphDigest !== undefined && afterLockfile.graphDigest !== undefined
530
+ ? beforeLockfile.graphDigest === afterLockfile.graphDigest
531
+ : beforeLockfile.sha256 === afterLockfile.sha256;
532
+ const beforeRuntime = previous?.runtimeGraph;
533
+ const afterRuntime = current?.runtimeGraph;
534
+ const sameRuntime = beforeRuntime === undefined || afterRuntime === undefined
535
+ ? beforeRuntime === afterRuntime
536
+ : beforeRuntime.digest === afterRuntime.digest;
537
+ return sameLockfile && sameRuntime;
538
+ }
539
+ function transition(previous, current) {
540
+ const previousIncompatible = previous !== undefined && isIncompatible(previous.result);
541
+ const currentIncompatible = isIncompatible(current.result);
542
+ let status;
543
+ if (previous === undefined)
544
+ status = currentIncompatible ? 'new-incompatibility' : 'compatible';
545
+ else if (previousIncompatible && !currentIncompatible)
546
+ status = 'resolved-incompatibility';
547
+ else if (!previousIncompatible && currentIncompatible)
548
+ status = 'new-incompatibility';
549
+ else if (previousIncompatible && currentIncompatible && (previous.result !== current.result || previous.reason !== current.reason))
550
+ status = 'changed-incompatibility';
551
+ else if (currentIncompatible)
552
+ status = 'persisting-incompatibility';
553
+ // Compatibility-state transitions take precedence over artifact drift. A
554
+ // fixed release necessarily changes its bytes; classifying that as generic
555
+ // drift would prevent the managed incident from ever closing.
556
+ else if (previous.artifact.sha256 !== undefined && current.artifact.sha256 !== undefined && previous.artifact.sha256 !== current.artifact.sha256)
557
+ status = 'artifact-drift';
558
+ else if (!sameResolution(previous.resolution, current.resolution))
559
+ status = 'resolution-drift';
560
+ else
561
+ status = 'compatible';
562
+ return {
563
+ caseId: current.caseId,
564
+ status,
565
+ result: current.result,
566
+ reason: current.reason,
567
+ ...(previous === undefined ? {} : { previousResult: previous.result }),
568
+ };
569
+ }
570
+ /**
571
+ * Merge only reports that prove they belong to a scheduled exact cell. Bad or
572
+ * absent reports never become a green result; absent cells stay missing and
573
+ * therefore are planned again on the next reconciliation.
574
+ */
575
+ export function mergeDshCompatibilityLedger(input) {
576
+ if (input.reports.length > MAX_REPORTS)
577
+ throw new Error(`compatibility reconciliation accepts at most ${MAX_REPORTS} reports`);
578
+ const expected = parseExpectedCases(input.expected);
579
+ const previousByCase = new Map(input.ledger.entries.map(entry => [entry.caseId, entry]));
580
+ const nextByCase = new Map(previousByCase);
581
+ const transitions = [];
582
+ const accepted = new Set();
583
+ const rejectedReports = [];
584
+ for (const [index, rawReport] of input.reports.entries()) {
585
+ try {
586
+ const raw = reportRecord(rawReport, `reports[${index}]`);
587
+ const rawCaseId = caseId(raw.caseId, `reports[${index}].caseId`);
588
+ const scheduled = expected.get(rawCaseId);
589
+ if (scheduled === undefined)
590
+ throw new Error(`report case ${rawCaseId} was not scheduled in this reconciliation`);
591
+ if (accepted.has(rawCaseId))
592
+ throw new Error(`more than one report was supplied for scheduled case ${rawCaseId}`);
593
+ const entry = parseObservationReport(rawReport, scheduled);
594
+ nextByCase.set(entry.caseId, entry);
595
+ accepted.add(entry.caseId);
596
+ transitions.push(transition(previousByCase.get(entry.caseId), entry));
597
+ }
598
+ catch (error) {
599
+ const message = error instanceof Error ? error.message : String(error);
600
+ rejectedReports.push(`report ${index + 1}: ${message.replace(/[\u0000-\u001f\u007f]/g, '?').slice(0, 512)}`);
601
+ }
602
+ }
603
+ const missingCaseIds = [...expected.keys()].filter(id => !accepted.has(id)).sort();
604
+ const entries = [...nextByCase.values()].sort((left, right) => left.caseId.localeCompare(right.caseId));
605
+ return {
606
+ ledger: { schema: DSH_COMPATIBILITY_LEDGER_SCHEMA, entries },
607
+ transitions: transitions.sort((left, right) => left.caseId.localeCompare(right.caseId)),
608
+ acceptedCaseIds: [...accepted].sort(),
609
+ missingCaseIds,
610
+ rejectedReports: rejectedReports.sort(),
611
+ };
612
+ }
613
+ function inline(value) {
614
+ return value.replace(/[\u0000-\u001f\u007f`|]/g, ' ').slice(0, 512);
615
+ }
616
+ export function renderDshCompatibilityLedgerMerge(merge) {
617
+ const lines = [
618
+ '## Upstream Radar — DSH compatibility ledger',
619
+ '',
620
+ `- Accepted isolated reports: **${merge.acceptedCaseIds.length}**`,
621
+ `- Missing scheduled reports: **${merge.missingCaseIds.length}**`,
622
+ `- Rejected reports: **${merge.rejectedReports.length}**`,
623
+ `- Current active cells: **${merge.ledger.entries.length}**`,
624
+ ];
625
+ const actionable = merge.transitions.filter(item => item.status !== 'compatible' && item.status !== 'persisting-incompatibility');
626
+ if (actionable.length > 0) {
627
+ lines.push('', '### New or changed evidence', '');
628
+ for (const item of actionable) {
629
+ lines.push(`- \`${inline(item.caseId)}\`: **${inline(item.status)}** → \`${inline(item.result)}\` — ${inline(item.reason)}`);
630
+ }
631
+ }
632
+ if (merge.missingCaseIds.length > 0) {
633
+ lines.push('', '### Evidence still missing', '');
634
+ for (const id of merge.missingCaseIds)
635
+ lines.push(`- \`${inline(id)}\` did not return a usable report; it remains unsatisfied and will be planned again.`);
636
+ }
637
+ if (merge.rejectedReports.length > 0) {
638
+ lines.push('', '### Rejected report evidence', '');
639
+ for (const reason of merge.rejectedReports)
640
+ lines.push(`- ${inline(reason)}`);
641
+ }
642
+ lines.push('', 'The ledger records behavior evidence for an exact plugin × DSH × runtime contract. It does not claim that third-party code is safe.', '');
643
+ return lines.join('\n');
644
+ }
645
+ //# sourceMappingURL=dsh-compatibility-ledger.js.map