@holoscript/holosystem 0.2.2 → 0.2.3

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.
@@ -0,0 +1,789 @@
1
+ import { createHash, createPublicKey, verify } from 'node:crypto';
2
+
3
+ export const HOLOSYSTEM_SUBSTRATE_SCHEMA = 'holoscript.holosystem.substrate.v1';
4
+ export const HOLOSYSTEM_REBUILD_ATTESTATION_SCHEMA = 'holoscript.holosystem.rebuild-attestation.v1';
5
+
6
+ const DIGEST_PATTERN = /^(?:sha256:[a-f0-9]{64}|sha512:[a-f0-9]{128})$/u;
7
+ const PORTABLE_SOURCE_PATTERN = /^(?:git\+https|https|holorepo|npm|pypi|oci):\/\//u;
8
+ const FLOATING_VERSION_PATTERN =
9
+ /(?:^|[\s:])(?:latest|main|master|next|workspace|file|link|portal|git|https?)(?:$|[\s:])|[*^~<>=|]/iu;
10
+
11
+ function list(value) {
12
+ return Array.isArray(value) ? value : [];
13
+ }
14
+
15
+ function isRecord(value) {
16
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
17
+ }
18
+
19
+ function validId(value) {
20
+ return typeof value === 'string' && /^[a-z0-9][a-z0-9._-]{0,127}$/iu.test(value);
21
+ }
22
+
23
+ function pinnedVersion(value) {
24
+ return (
25
+ typeof value === 'string' &&
26
+ value.trim().length > 0 &&
27
+ value.length <= 160 &&
28
+ !FLOATING_VERSION_PATTERN.test(value)
29
+ );
30
+ }
31
+
32
+ function portableSource(value) {
33
+ if (
34
+ typeof value !== 'string' ||
35
+ value.length > 512 ||
36
+ !PORTABLE_SOURCE_PATTERN.test(value) ||
37
+ value.includes('..')
38
+ ) {
39
+ return false;
40
+ }
41
+ try {
42
+ const parsed = new URL(value);
43
+ return !parsed.username && !parsed.password && !parsed.search && !parsed.hash;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ function pinnedRevision(value) {
50
+ return (
51
+ typeof value === 'string' &&
52
+ value.trim().length > 0 &&
53
+ value.length <= 200 &&
54
+ !/^(?:latest|main|master|next|head)$/iu.test(value.trim())
55
+ );
56
+ }
57
+
58
+ function validDigest(value) {
59
+ return typeof value === 'string' && DIGEST_PATTERN.test(value);
60
+ }
61
+
62
+ function encodeRebuildAttestation({ verifier, component }) {
63
+ return JSON.stringify({
64
+ schema: HOLOSYSTEM_REBUILD_ATTESTATION_SCHEMA,
65
+ verifier,
66
+ component: {
67
+ id: component.id,
68
+ kind: component.kind,
69
+ version: component.version,
70
+ source: {
71
+ uri: component.source.uri,
72
+ revision: component.source.revision,
73
+ },
74
+ artifact: { digest: component.artifact.digest },
75
+ execution: { installScripts: component.execution.installScripts },
76
+ },
77
+ });
78
+ }
79
+
80
+ export function createRebuildAttestationPayload({ verifier, component } = {}) {
81
+ if (
82
+ !validId(verifier) ||
83
+ !validId(component?.id) ||
84
+ !validId(component?.kind) ||
85
+ !pinnedVersion(component?.version) ||
86
+ !portableSource(component?.source?.uri) ||
87
+ !pinnedRevision(component?.source?.revision) ||
88
+ !validDigest(component?.artifact?.digest) ||
89
+ !['none', 'present'].includes(component?.execution?.installScripts)
90
+ ) {
91
+ throw new TypeError('Cannot create a rebuild attestation for an invalid component tuple.');
92
+ }
93
+ return encodeRebuildAttestation({ verifier, component });
94
+ }
95
+
96
+ function hashReceipt(value) {
97
+ return `sha256:${createHash('sha256').update(JSON.stringify(value)).digest('hex')}`;
98
+ }
99
+
100
+ function publicKeyFingerprint(key) {
101
+ return `sha256:${createHash('sha256')
102
+ .update(key.export({ type: 'spki', format: 'der' }))
103
+ .digest('hex')}`;
104
+ }
105
+
106
+ function decodeEd25519Signature(value) {
107
+ if (typeof value !== 'string' || value.length !== 88 || !/^[A-Za-z0-9+/]+={2}$/u.test(value)) {
108
+ return null;
109
+ }
110
+ const decoded = Buffer.from(value, 'base64');
111
+ return decoded.length === 64 && decoded.toString('base64') === value ? decoded : null;
112
+ }
113
+
114
+ function issue(issues, code, path, message) {
115
+ issues.push({ code, path, message });
116
+ }
117
+
118
+ function compareText(left, right) {
119
+ return String(left || '').localeCompare(String(right || ''));
120
+ }
121
+
122
+ function normalizeCoverage(value, issues) {
123
+ const issueCountBefore = issues.length;
124
+ if (!isRecord(value)) {
125
+ issue(
126
+ issues,
127
+ 'substrate-coverage-missing',
128
+ 'coverage',
129
+ 'Substrate coverage and known missing layers must be declared.'
130
+ );
131
+ }
132
+ const normalizeLayers = (layers, path) => {
133
+ const normalized = [];
134
+ const seen = new Set();
135
+ for (const [index, layer] of list(layers).entries()) {
136
+ if (!validId(layer)) {
137
+ issue(issues, 'substrate-layer-invalid', `${path}[${index}]`, 'Layer id is invalid.');
138
+ continue;
139
+ }
140
+ if (seen.has(layer)) {
141
+ issue(
142
+ issues,
143
+ 'substrate-layer-duplicate',
144
+ `${path}[${index}]`,
145
+ 'Layer ids must be unique within each coverage set.'
146
+ );
147
+ continue;
148
+ }
149
+ seen.add(layer);
150
+ normalized.push(layer);
151
+ }
152
+ return normalized.sort(compareText);
153
+ };
154
+ if (!Array.isArray(value?.includedLayers) || value.includedLayers.length === 0) {
155
+ issue(
156
+ issues,
157
+ 'substrate-included-layers-missing',
158
+ 'coverage.includedLayers',
159
+ 'At least one covered substrate layer is required.'
160
+ );
161
+ }
162
+ if (!Array.isArray(value?.missingLayers)) {
163
+ issue(
164
+ issues,
165
+ 'substrate-missing-layers-undeclared',
166
+ 'coverage.missingLayers',
167
+ 'Known missing substrate layers must be an explicit array.'
168
+ );
169
+ }
170
+ const includedLayers = normalizeLayers(value?.includedLayers, 'coverage.includedLayers');
171
+ const missingLayers = normalizeLayers(value?.missingLayers, 'coverage.missingLayers');
172
+ const includedSet = new Set(includedLayers);
173
+ for (const [index, layer] of missingLayers.entries()) {
174
+ if (includedSet.has(layer)) {
175
+ issue(
176
+ issues,
177
+ 'substrate-layer-conflict',
178
+ `coverage.missingLayers[${index}]`,
179
+ 'A layer cannot be both included and missing.'
180
+ );
181
+ }
182
+ }
183
+ if (missingLayers.length > 0) {
184
+ issue(
185
+ issues,
186
+ 'substrate-coverage-incomplete',
187
+ 'coverage.missingLayers',
188
+ 'Known substrate layers are not represented in this closure.'
189
+ );
190
+ }
191
+ return {
192
+ includedLayers,
193
+ missingLayers,
194
+ complete: missingLayers.length === 0 && issues.length === issueCountBefore,
195
+ };
196
+ }
197
+
198
+ function normalizeVerificationPolicy(value, issues) {
199
+ const minimum = value?.minimumIndependentRebuilds;
200
+ if (!Number.isInteger(minimum) || minimum < 1 || minimum > 16) {
201
+ issue(
202
+ issues,
203
+ 'verification-minimum-invalid',
204
+ 'verificationPolicy.minimumIndependentRebuilds',
205
+ 'minimumIndependentRebuilds must be an integer from 1 to 16.'
206
+ );
207
+ }
208
+ if (!Array.isArray(value?.trustRoots) || value.trustRoots.length === 0) {
209
+ issue(
210
+ issues,
211
+ 'trust-roots-missing',
212
+ 'verificationPolicy.trustRoots',
213
+ 'At least one caller-owned Ed25519 trust root is required.'
214
+ );
215
+ }
216
+
217
+ const roots = [];
218
+ for (const [index, root] of list(value?.trustRoots).entries()) {
219
+ const path = `verificationPolicy.trustRoots[${index}]`;
220
+ const verifier = validId(root?.verifier) ? root.verifier : null;
221
+ const trustDomain = validId(root?.trustDomain) ? root.trustDomain : null;
222
+ let key = null;
223
+ let fingerprint = null;
224
+ if (!verifier) {
225
+ issue(issues, 'trust-root-verifier-invalid', `${path}.verifier`, 'Verifier id is invalid.');
226
+ }
227
+ if (!trustDomain) {
228
+ issue(
229
+ issues,
230
+ 'trust-root-domain-invalid',
231
+ `${path}.trustDomain`,
232
+ 'Trust domain id is invalid.'
233
+ );
234
+ }
235
+ try {
236
+ if (
237
+ typeof root?.publicKey !== 'string' ||
238
+ root.publicKey.length > 8192 ||
239
+ !root.publicKey.startsWith('-----BEGIN PUBLIC KEY-----') ||
240
+ !root.publicKey.trimEnd().endsWith('-----END PUBLIC KEY-----')
241
+ ) {
242
+ throw new TypeError('Public key must be bounded SPKI PEM text.');
243
+ }
244
+ key = createPublicKey(root.publicKey);
245
+ if (key.asymmetricKeyType !== 'ed25519') {
246
+ throw new TypeError('Public key must be Ed25519.');
247
+ }
248
+ fingerprint = publicKeyFingerprint(key);
249
+ } catch {
250
+ issue(
251
+ issues,
252
+ 'trust-root-key-invalid',
253
+ `${path}.publicKey`,
254
+ 'Trust root must contain an Ed25519 public key.'
255
+ );
256
+ key = null;
257
+ }
258
+ roots.push({
259
+ verifier,
260
+ trustDomain,
261
+ publicKeyFingerprint: fingerprint,
262
+ key,
263
+ sourceIndex: index,
264
+ });
265
+ }
266
+ roots.sort(
267
+ (left, right) =>
268
+ compareText(left.verifier, right.verifier) || compareText(left.trustDomain, right.trustDomain)
269
+ );
270
+
271
+ const trustRootMap = new Map();
272
+ const fingerprintOwners = new Map();
273
+ for (const root of roots) {
274
+ if (!root.verifier || !root.trustDomain || !root.key || !root.publicKeyFingerprint) continue;
275
+ if (trustRootMap.has(root.verifier)) {
276
+ issue(
277
+ issues,
278
+ 'trust-root-verifier-duplicate',
279
+ `verificationPolicy.trustRoots[${root.sourceIndex}].verifier`,
280
+ 'Each verifier must have exactly one trust root.'
281
+ );
282
+ continue;
283
+ }
284
+ if (fingerprintOwners.has(root.publicKeyFingerprint)) {
285
+ issue(
286
+ issues,
287
+ 'trust-root-key-duplicate',
288
+ `verificationPolicy.trustRoots[${root.sourceIndex}].publicKey`,
289
+ 'Each trust root key may identify only one verifier.'
290
+ );
291
+ continue;
292
+ }
293
+ trustRootMap.set(root.verifier, root);
294
+ fingerprintOwners.set(root.publicKeyFingerprint, root.verifier);
295
+ }
296
+
297
+ return {
298
+ minimumIndependentRebuilds:
299
+ Number.isInteger(minimum) && minimum >= 1 && minimum <= 16 ? minimum : null,
300
+ trustRoots: roots.map(({ verifier, trustDomain, publicKeyFingerprint: fingerprint }) => ({
301
+ verifier,
302
+ trustDomain,
303
+ publicKeyFingerprint: fingerprint,
304
+ })),
305
+ trustRootMap,
306
+ };
307
+ }
308
+
309
+ function normalizeRequirement(value, componentPath, index, issues) {
310
+ const path = `${componentPath}.requires[${index}]`;
311
+ const id = validId(value?.id) ? value.id : null;
312
+ const type = validId(value?.type) ? value.type : null;
313
+ if (!id) {
314
+ issue(issues, 'dependency-id-invalid', `${path}.id`, 'Dependency id must be portable.');
315
+ }
316
+ if (!type) {
317
+ issue(issues, 'dependency-type-invalid', `${path}.type`, 'Dependency type must be portable.');
318
+ }
319
+ return { id, type };
320
+ }
321
+
322
+ function normalizeRebuild(value, componentPath, index, component, trustRootMap, issues) {
323
+ const path = `${componentPath}.verification.rebuilds[${index}]`;
324
+ const verifier = validId(value?.verifier) ? value.verifier : null;
325
+ const digest = validDigest(value?.digest) ? value.digest : null;
326
+ const signature = decodeEd25519Signature(value?.signature);
327
+ if (!verifier) {
328
+ issue(issues, 'rebuild-verifier-invalid', `${path}.verifier`, 'Verifier id must be portable.');
329
+ }
330
+ if (!digest) {
331
+ issue(
332
+ issues,
333
+ 'rebuild-digest-invalid',
334
+ `${path}.digest`,
335
+ 'Rebuild digest must be sha256 or sha512.'
336
+ );
337
+ }
338
+ if (!signature) {
339
+ issue(
340
+ issues,
341
+ 'rebuild-signature-invalid',
342
+ `${path}.signature`,
343
+ 'Rebuild signature must be a canonical Ed25519 signature in base64.'
344
+ );
345
+ }
346
+
347
+ const trustRoot = verifier ? trustRootMap.get(verifier) : null;
348
+ if (verifier && !trustRoot) {
349
+ issue(
350
+ issues,
351
+ 'rebuild-verifier-untrusted',
352
+ `${path}.verifier`,
353
+ 'Verifier is not present in the caller-owned trust policy.'
354
+ );
355
+ }
356
+ const distinctTrustDomain = Boolean(
357
+ trustRoot?.trustDomain &&
358
+ component.custody.trustDomain &&
359
+ trustRoot.trustDomain !== component.custody.trustDomain
360
+ );
361
+ if (trustRoot && component.custody.trustDomain && !distinctTrustDomain) {
362
+ issue(
363
+ issues,
364
+ 'rebuild-trust-domain-not-independent',
365
+ `${path}.verifier`,
366
+ 'Verifier and component custodian belong to the same declared trust domain.'
367
+ );
368
+ }
369
+ const digestMatches = Boolean(digest && component.artifact.digest === digest);
370
+ if (digest && component.artifact.digest && !digestMatches) {
371
+ issue(
372
+ issues,
373
+ 'independent-rebuild-mismatch',
374
+ `${path}.digest`,
375
+ 'Rebuild digest does not match the declared artifact.'
376
+ );
377
+ }
378
+
379
+ const componentTupleValid = Boolean(
380
+ component.id &&
381
+ component.kind &&
382
+ component.version &&
383
+ component.source.uri &&
384
+ component.source.revision &&
385
+ component.artifact.digest &&
386
+ component.execution.installScripts
387
+ );
388
+ let signatureVerified = false;
389
+ let attestationHash = null;
390
+ if (verifier && trustRoot?.key && signature && componentTupleValid) {
391
+ const payload = encodeRebuildAttestation({ verifier, component });
392
+ signatureVerified = verify(null, Buffer.from(payload), trustRoot.key, signature);
393
+ attestationHash = hashReceipt({ payload, signature: value.signature });
394
+ if (!signatureVerified) {
395
+ issue(
396
+ issues,
397
+ 'rebuild-attestation-invalid',
398
+ `${path}.signature`,
399
+ 'Rebuild signature does not authenticate the declared component tuple.'
400
+ );
401
+ }
402
+ }
403
+
404
+ const policyIndependent = Boolean(
405
+ signatureVerified && distinctTrustDomain && digestMatches && trustRoot?.publicKeyFingerprint
406
+ );
407
+ return {
408
+ verifier,
409
+ digest,
410
+ trustDomain: trustRoot?.trustDomain || null,
411
+ publicKeyFingerprint: trustRoot?.publicKeyFingerprint || null,
412
+ attestationHash,
413
+ signatureVerified,
414
+ policyIndependent,
415
+ };
416
+ }
417
+
418
+ function normalizeComponent(value, index, verificationPolicy, issues) {
419
+ const path = `components[${index}]`;
420
+ const id = validId(value?.id) ? value.id : null;
421
+ const kind = validId(value?.kind) ? value.kind : null;
422
+ const version = pinnedVersion(value?.version) ? value.version : null;
423
+ const custodyMode = value?.custody?.mode;
424
+ const custodyOwner = validId(value?.custody?.owner) ? value.custody.owner : null;
425
+ const custodyTrustDomain = validId(value?.custody?.trustDomain)
426
+ ? value.custody.trustDomain
427
+ : null;
428
+ const sourceUri = portableSource(value?.source?.uri) ? value.source.uri : null;
429
+ const sourceRevision = pinnedRevision(value?.source?.revision) ? value.source.revision : null;
430
+ const artifactDigest = validDigest(value?.artifact?.digest) ? value.artifact.digest : null;
431
+ const installScripts = ['none', 'present'].includes(value?.execution?.installScripts)
432
+ ? value.execution.installScripts
433
+ : null;
434
+
435
+ if (!id) issue(issues, 'component-id-invalid', `${path}.id`, 'Component id must be portable.');
436
+ if (!kind)
437
+ issue(issues, 'component-kind-invalid', `${path}.kind`, 'Component kind must be portable.');
438
+ if (!version) {
439
+ issue(
440
+ issues,
441
+ 'component-version-floating',
442
+ `${path}.version`,
443
+ 'Component version must be exact rather than floating or local.'
444
+ );
445
+ }
446
+ if (custodyMode !== 'owned' && custodyMode !== 'external') {
447
+ issue(
448
+ issues,
449
+ 'custody-mode-invalid',
450
+ `${path}.custody.mode`,
451
+ 'Custody mode must be owned or external.'
452
+ );
453
+ }
454
+ if (!custodyOwner) {
455
+ issue(issues, 'custody-owner-invalid', `${path}.custody.owner`, 'Custody owner is required.');
456
+ }
457
+ if (!custodyTrustDomain) {
458
+ issue(
459
+ issues,
460
+ 'custody-trust-domain-invalid',
461
+ `${path}.custody.trustDomain`,
462
+ 'Custody trust domain is required.'
463
+ );
464
+ }
465
+ if (!sourceUri) {
466
+ issue(
467
+ issues,
468
+ 'source-uri-not-portable',
469
+ `${path}.source.uri`,
470
+ 'Source must use a portable public or HoloRepo URI.'
471
+ );
472
+ }
473
+ if (!sourceRevision) {
474
+ issue(
475
+ issues,
476
+ 'source-revision-not-pinned',
477
+ `${path}.source.revision`,
478
+ 'Source revision must be pinned.'
479
+ );
480
+ }
481
+ if (!artifactDigest) {
482
+ issue(
483
+ issues,
484
+ 'artifact-digest-invalid',
485
+ `${path}.artifact.digest`,
486
+ 'Artifact digest must be sha256 or sha512.'
487
+ );
488
+ }
489
+ if (!installScripts) {
490
+ issue(
491
+ issues,
492
+ 'install-script-status-missing',
493
+ `${path}.execution.installScripts`,
494
+ 'Component must declare whether installation scripts are present.'
495
+ );
496
+ } else if (installScripts === 'present') {
497
+ issue(
498
+ issues,
499
+ 'install-script-present',
500
+ `${path}.execution.installScripts`,
501
+ 'Installation scripts require a future enforced disable or sandbox receipt.'
502
+ );
503
+ }
504
+ if (!Array.isArray(value?.requires)) {
505
+ issue(
506
+ issues,
507
+ 'dependencies-missing',
508
+ `${path}.requires`,
509
+ 'Component dependencies must be explicit.'
510
+ );
511
+ }
512
+
513
+ const requires = list(value?.requires)
514
+ .map((item, requirementIndex) => normalizeRequirement(item, path, requirementIndex, issues))
515
+ .sort((left, right) => compareText(left.id, right.id) || compareText(left.type, right.type));
516
+ const componentTuple = {
517
+ id,
518
+ kind,
519
+ version,
520
+ custody: {
521
+ mode: custodyMode === 'owned' || custodyMode === 'external' ? custodyMode : null,
522
+ owner: custodyOwner,
523
+ trustDomain: custodyTrustDomain,
524
+ },
525
+ source: { uri: sourceUri, revision: sourceRevision },
526
+ artifact: { digest: artifactDigest },
527
+ execution: { installScripts },
528
+ };
529
+ const rebuilds = list(value?.verification?.rebuilds)
530
+ .map((item, rebuildIndex) =>
531
+ normalizeRebuild(
532
+ item,
533
+ path,
534
+ rebuildIndex,
535
+ componentTuple,
536
+ verificationPolicy.trustRootMap,
537
+ issues
538
+ )
539
+ )
540
+ .sort(
541
+ (left, right) =>
542
+ compareText(left.verifier, right.verifier) || compareText(left.digest, right.digest)
543
+ );
544
+ const policyIndependentRoots = new Set(
545
+ rebuilds
546
+ .filter((rebuild) => rebuild.policyIndependent)
547
+ .map((rebuild) => rebuild.publicKeyFingerprint)
548
+ );
549
+ const requiredRebuilds = verificationPolicy.minimumIndependentRebuilds;
550
+ if (requiredRebuilds && policyIndependentRoots.size < requiredRebuilds) {
551
+ issue(
552
+ issues,
553
+ 'independent-rebuild-missing',
554
+ `${path}.verification.rebuilds`,
555
+ `Component needs ${requiredRebuilds} policy-independent signed rebuild attestation(s).`
556
+ );
557
+ }
558
+
559
+ return {
560
+ id,
561
+ kind,
562
+ version,
563
+ custody: {
564
+ mode: custodyMode === 'owned' || custodyMode === 'external' ? custodyMode : null,
565
+ owner: custodyOwner,
566
+ trustDomain: custodyTrustDomain,
567
+ },
568
+ source: { uri: sourceUri, revision: sourceRevision },
569
+ artifact: { digest: artifactDigest },
570
+ execution: { installScripts },
571
+ requires,
572
+ verification: {
573
+ rebuilds,
574
+ policyIndependentRebuilds: policyIndependentRoots.size,
575
+ },
576
+ };
577
+ }
578
+
579
+ function findCycles(componentMap) {
580
+ const visiting = new Set();
581
+ const visited = new Set();
582
+ const cycles = [];
583
+
584
+ function visit(id, trail) {
585
+ if (visiting.has(id)) {
586
+ const start = trail.indexOf(id);
587
+ cycles.push([...trail.slice(Math.max(0, start)), id]);
588
+ return;
589
+ }
590
+ if (visited.has(id)) return;
591
+ visiting.add(id);
592
+ const component = componentMap.get(id);
593
+ for (const dependency of component?.requires || []) {
594
+ if (dependency.id && componentMap.has(dependency.id)) {
595
+ visit(dependency.id, [...trail, id]);
596
+ }
597
+ }
598
+ visiting.delete(id);
599
+ visited.add(id);
600
+ }
601
+
602
+ for (const id of [...componentMap.keys()].sort(compareText)) visit(id, []);
603
+ return cycles;
604
+ }
605
+
606
+ function reachableFrom(root, componentMap) {
607
+ const reachable = new Set();
608
+ function visit(id) {
609
+ if (reachable.has(id) || !componentMap.has(id)) return;
610
+ reachable.add(id);
611
+ for (const dependency of componentMap.get(id).requires) visit(dependency.id);
612
+ }
613
+ visit(root);
614
+ return reachable;
615
+ }
616
+
617
+ function dependencyFirstOrder(root, componentMap) {
618
+ const ordered = [];
619
+ const visited = new Set();
620
+ function visit(id) {
621
+ if (visited.has(id) || !componentMap.has(id)) return;
622
+ visited.add(id);
623
+ for (const dependency of componentMap.get(id).requires) visit(dependency.id);
624
+ ordered.push(id);
625
+ }
626
+ visit(root);
627
+ return ordered;
628
+ }
629
+
630
+ export function buildSubstrateClosure({
631
+ root,
632
+ coverage: coverageInput,
633
+ verificationPolicy: policyInput,
634
+ components = [],
635
+ now = new Date(),
636
+ } = {}) {
637
+ const issues = [];
638
+ const coverage = normalizeCoverage(coverageInput, issues);
639
+ const verificationPolicy = normalizeVerificationPolicy(policyInput, issues);
640
+ const normalizedRoot = validId(root) ? root : null;
641
+ if (!normalizedRoot) {
642
+ issue(issues, 'root-id-invalid', 'root', 'Root component id must be portable.');
643
+ }
644
+ if (!Array.isArray(components) || components.length === 0) {
645
+ issue(issues, 'components-missing', 'components', 'At least one component is required.');
646
+ }
647
+
648
+ const normalized = list(components)
649
+ .map((component, index) => normalizeComponent(component, index, verificationPolicy, issues))
650
+ .sort((left, right) => compareText(left.id, right.id) || compareText(left.kind, right.kind));
651
+ const componentMap = new Map();
652
+ for (const [index, component] of normalized.entries()) {
653
+ if (!component.id) continue;
654
+ if (componentMap.has(component.id)) {
655
+ issue(
656
+ issues,
657
+ 'component-id-duplicate',
658
+ `components[${index}].id`,
659
+ 'Component ids must be unique.'
660
+ );
661
+ continue;
662
+ }
663
+ componentMap.set(component.id, component);
664
+ }
665
+ if (normalizedRoot && !componentMap.has(normalizedRoot)) {
666
+ issue(issues, 'root-component-missing', 'root', 'Root component is not declared.');
667
+ }
668
+
669
+ const edges = [];
670
+ let dependencyMissing = false;
671
+ for (const component of componentMap.values()) {
672
+ for (const dependency of component.requires) {
673
+ if (!dependency.id || !dependency.type) continue;
674
+ edges.push({ from: component.id, to: dependency.id, type: dependency.type });
675
+ if (!componentMap.has(dependency.id)) {
676
+ dependencyMissing = true;
677
+ issue(
678
+ issues,
679
+ 'dependency-component-missing',
680
+ `components.${component.id}.requires`,
681
+ 'A dependency edge points to an undeclared component.'
682
+ );
683
+ }
684
+ }
685
+ }
686
+ edges.sort(
687
+ (left, right) =>
688
+ compareText(left.from, right.from) ||
689
+ compareText(left.to, right.to) ||
690
+ compareText(left.type, right.type)
691
+ );
692
+
693
+ const cycles = findCycles(componentMap);
694
+ for (const cycle of cycles) {
695
+ issue(
696
+ issues,
697
+ 'dependency-cycle',
698
+ `components.${cycle[0]}.requires`,
699
+ 'Dependency graph contains a cycle.'
700
+ );
701
+ }
702
+
703
+ const reachable = normalizedRoot ? reachableFrom(normalizedRoot, componentMap) : new Set();
704
+ if (normalizedRoot && componentMap.has(normalizedRoot)) {
705
+ for (const id of [...componentMap.keys()].sort(compareText)) {
706
+ if (!reachable.has(id)) {
707
+ issue(
708
+ issues,
709
+ 'component-unreachable',
710
+ `components.${id}`,
711
+ 'Component is outside the declared root closure.'
712
+ );
713
+ }
714
+ }
715
+ }
716
+
717
+ issues.sort(
718
+ (left, right) => compareText(left.path, right.path) || compareText(left.code, right.code)
719
+ );
720
+ const uniqueComponents = [...componentMap.values()].sort((left, right) =>
721
+ compareText(left.id, right.id)
722
+ );
723
+ const externalBoundaries = uniqueComponents
724
+ .filter((component) => component.custody.mode === 'external')
725
+ .map((component) => ({
726
+ id: component.id,
727
+ kind: component.kind,
728
+ owner: component.custody.owner,
729
+ trustDomain: component.custody.trustDomain,
730
+ }));
731
+ const independentlyVerified = uniqueComponents.filter(
732
+ (component) =>
733
+ verificationPolicy.minimumIndependentRebuilds &&
734
+ component.verification.policyIndependentRebuilds >=
735
+ verificationPolicy.minimumIndependentRebuilds
736
+ ).length;
737
+ const ready = issues.length === 0;
738
+ const buildOrder =
739
+ !dependencyMissing &&
740
+ cycles.length === 0 &&
741
+ normalizedRoot &&
742
+ reachable.size === componentMap.size
743
+ ? dependencyFirstOrder(normalizedRoot, componentMap)
744
+ : null;
745
+ const receipt = {
746
+ schema: HOLOSYSTEM_SUBSTRATE_SCHEMA,
747
+ generatedAt: now.toISOString(),
748
+ status: ready ? 'ready' : 'blocked',
749
+ ready,
750
+ root: normalizedRoot,
751
+ rule: 'Every infrastructure dependency is explicit, pinned, custody-attributed, content-addressed, free of unmodeled install scripts, and rebuilt under a distinct trust domain authorized by the caller policy.',
752
+ coverage,
753
+ verificationPolicy: {
754
+ minimumIndependentRebuilds: verificationPolicy.minimumIndependentRebuilds,
755
+ trustRoots: verificationPolicy.trustRoots,
756
+ },
757
+ summary: {
758
+ components: uniqueComponents.length,
759
+ dependencies: edges.length,
760
+ independentlyVerified,
761
+ owned: uniqueComponents.filter((component) => component.custody.mode === 'owned').length,
762
+ external: externalBoundaries.length,
763
+ installScriptPackages: uniqueComponents.filter(
764
+ (component) => component.execution.installScripts === 'present'
765
+ ).length,
766
+ issues: issues.length,
767
+ },
768
+ sovereignty: {
769
+ fullyOwned: ready && externalBoundaries.length === 0,
770
+ externalBoundaries,
771
+ },
772
+ buildOrder,
773
+ components: uniqueComponents,
774
+ dependencies: edges,
775
+ issues,
776
+ boundaries: {
777
+ provenanceIsNotReview: true,
778
+ signaturesAreNotSafetyProof: true,
779
+ trustDomainsAreCallerAssertions: true,
780
+ policyIndependenceIsNotOrganizationalProof: true,
781
+ signedPolicyIndependentRebuildRequired: true,
782
+ installScriptsRequireEnforcedDisableOrSandboxReceipt: true,
783
+ declaredCoverageIsNotDiscoveryProof: true,
784
+ externalDependenciesRemainVisible: true,
785
+ },
786
+ };
787
+ receipt.receiptHash = hashReceipt({ ...receipt, generatedAt: null });
788
+ return receipt;
789
+ }