@ixo/domain.md 0.3.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 (70) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +6 -0
  3. package/README.md +30 -0
  4. package/assets/domain-md.schema.json +1103 -0
  5. package/assets/oracle-capsule-jcs-vectors.json +57 -0
  6. package/assets/oracle-capsule-manifest-contract.md +72 -0
  7. package/assets/oracle-capsule-source-lock.schema.json +45 -0
  8. package/assets/oracle-capsule.schema.json +265 -0
  9. package/assets/rules.json +275 -0
  10. package/assets/spec.md +1434 -0
  11. package/assets/template-manifest.schema.json +57 -0
  12. package/dist/capsule/index.d.ts +75 -0
  13. package/dist/capsule/index.js +20 -0
  14. package/dist/capsule/index.js.map +1 -0
  15. package/dist/chunk-BYBC2TSS.js +597 -0
  16. package/dist/chunk-BYBC2TSS.js.map +1 -0
  17. package/dist/chunk-G7PZJYAJ.js +31826 -0
  18. package/dist/chunk-G7PZJYAJ.js.map +1 -0
  19. package/dist/chunk-R6QZRNEX.js +164 -0
  20. package/dist/chunk-R6QZRNEX.js.map +1 -0
  21. package/dist/chunk-T3STBWXP.js +46 -0
  22. package/dist/chunk-T3STBWXP.js.map +1 -0
  23. package/dist/chunk-W7MRQP7I.js +862 -0
  24. package/dist/chunk-W7MRQP7I.js.map +1 -0
  25. package/dist/chunk-XDIWMZVF.js +1150 -0
  26. package/dist/chunk-XDIWMZVF.js.map +1 -0
  27. package/dist/cli-KcUIQEiE.d.ts +171 -0
  28. package/dist/cli.d.ts +2 -0
  29. package/dist/cli.js +400 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/index.d.ts +12 -0
  32. package/dist/index.js +83 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/resolver-B3mwnjJq.d.ts +96 -0
  35. package/dist/spec.d.ts +20 -0
  36. package/dist/spec.js +23 -0
  37. package/dist/spec.js.map +1 -0
  38. package/dist/templates/index.d.ts +7 -0
  39. package/dist/templates/index.js +24 -0
  40. package/dist/templates/index.js.map +1 -0
  41. package/dist/workers-Dr8B8a2O.d.ts +19 -0
  42. package/dist/workers.d.ts +3 -0
  43. package/dist/workers.js +31 -0
  44. package/dist/workers.js.map +1 -0
  45. package/package.json +97 -0
  46. package/src/capsule/canonical.ts +74 -0
  47. package/src/capsule/index.ts +10 -0
  48. package/src/capsule/json.ts +373 -0
  49. package/src/capsule/types.ts +63 -0
  50. package/src/capsule/validate.ts +551 -0
  51. package/src/cli.ts +405 -0
  52. package/src/compiled-validators.d.ts +7 -0
  53. package/src/compiled-validators.js +32512 -0
  54. package/src/constants.ts +250 -0
  55. package/src/diff.ts +111 -0
  56. package/src/export.ts +28 -0
  57. package/src/index.ts +36 -0
  58. package/src/lint.ts +42 -0
  59. package/src/parser.ts +174 -0
  60. package/src/sarif.ts +49 -0
  61. package/src/semantic.ts +1156 -0
  62. package/src/spec.ts +48 -0
  63. package/src/templates/errors.ts +3 -0
  64. package/src/templates/index.ts +11 -0
  65. package/src/templates/manifest.ts +94 -0
  66. package/src/templates/render.ts +435 -0
  67. package/src/templates/resolver.ts +200 -0
  68. package/src/templates/types.ts +98 -0
  69. package/src/types.ts +195 -0
  70. package/src/workers.ts +6 -0
@@ -0,0 +1,63 @@
1
+ import type { Finding, SourceLocation } from '../types.js';
2
+
3
+ export type JsonPrimitive = null | boolean | number | string;
4
+ export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
5
+
6
+ export interface StrictJsonOptions {
7
+ sourceName?: string;
8
+ maxBytes?: number;
9
+ maxDepth?: number;
10
+ maxNodes?: number;
11
+ maxScalarLength?: number;
12
+ }
13
+
14
+ export interface StrictJsonDocument {
15
+ value: JsonValue;
16
+ raw: string;
17
+ sourceName: string;
18
+ locations: ReadonlyMap<string, SourceLocation>;
19
+ }
20
+
21
+ export interface StrictJsonResult {
22
+ ok: boolean;
23
+ document?: StrictJsonDocument;
24
+ findings: Finding[];
25
+ }
26
+
27
+ export type CapsuleExternalCheckCategory =
28
+ 'identity' | 'authorization' | 'time' | 'privacy' | 'review' | 'distribution' | 'signer';
29
+
30
+ export interface CapsuleExternalCheck {
31
+ code: string;
32
+ category: CapsuleExternalCheckCategory;
33
+ reason: string;
34
+ }
35
+
36
+ export interface CapsuleValidationOptions extends StrictJsonOptions {
37
+ expectedIdentity?: {
38
+ cid: string;
39
+ sha256: string;
40
+ bytes?: number;
41
+ };
42
+ sourceLock?: string | Uint8Array;
43
+ lockedFiles?: Readonly<Record<string, string | Uint8Array>>;
44
+ }
45
+
46
+ export interface CapsuleValidationReport {
47
+ tool: '@ixo/domain.md';
48
+ contract: 'ixo.earth/oracle-capsule/v0alpha1';
49
+ sourceName: string;
50
+ status: 'static-pass' | 'fail';
51
+ ok: boolean;
52
+ findings: Finding[];
53
+ externalChecksRequired: CapsuleExternalCheck[];
54
+ manifest?: JsonValue;
55
+ }
56
+
57
+ export interface JcsGoldenVector {
58
+ name: string;
59
+ input: string;
60
+ canonical?: string;
61
+ sha256?: string;
62
+ errorCode?: string;
63
+ }
@@ -0,0 +1,551 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import type { ErrorObject } from 'ajv';
4
+ import { CID } from 'multiformats/cid';
5
+ import { sha256 } from 'multiformats/hashes/sha2';
6
+
7
+ import {
8
+ ORACLE_CAPSULE_CONTRACT,
9
+ ORACLE_CAPSULE_EXTERNAL_CHECKS,
10
+ ORACLE_CAPSULE_LIMITS,
11
+ } from '../constants.js';
12
+ import validators from '../compiled-validators.js';
13
+ import type { Finding, SourceLocation } from '../types.js';
14
+ import { oracleCapsuleReleaseDigest } from './canonical.js';
15
+ import { parseCapsuleJson } from './json.js';
16
+ import type {
17
+ CapsuleExternalCheck,
18
+ CapsuleValidationOptions,
19
+ CapsuleValidationReport,
20
+ JsonValue,
21
+ StrictJsonDocument,
22
+ } from './types.js';
23
+
24
+ function isRecord(value: unknown): value is Record<string, JsonValue> {
25
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
26
+ }
27
+
28
+ function pointerSegment(value: string): string {
29
+ return value.replace(/~/g, '~0').replace(/\//g, '~1');
30
+ }
31
+
32
+ function location(document: StrictJsonDocument, path: string): SourceLocation {
33
+ let candidate = path.length > 0 ? path : '/';
34
+ while (candidate !== '/') {
35
+ const found = document.locations.get(candidate);
36
+ if (found) return found;
37
+ candidate = candidate.slice(0, candidate.lastIndexOf('/')) || '/';
38
+ }
39
+ return document.locations.get('/') ?? { line: 1, column: 1 };
40
+ }
41
+
42
+ function add(
43
+ findings: Finding[],
44
+ document: StrictJsonDocument,
45
+ code: string,
46
+ message: string,
47
+ path = '/',
48
+ ): void {
49
+ findings.push({ severity: 'error', code, message, path, location: location(document, path) });
50
+ }
51
+
52
+ function schemaPath(error: ErrorObject): string {
53
+ const base = error.instancePath || '/';
54
+ if (error.keyword !== 'required') return base;
55
+ const missing = (error.params as { missingProperty?: string }).missingProperty;
56
+ if (!missing) return base;
57
+ return base === '/' || base === ''
58
+ ? `/${pointerSegment(missing)}`
59
+ : `${base}/${pointerSegment(missing)}`;
60
+ }
61
+
62
+ function addSchemaFindings(
63
+ findings: Finding[],
64
+ document: StrictJsonDocument,
65
+ errors: ErrorObject[] | null | undefined,
66
+ label: string,
67
+ ): void {
68
+ for (const error of errors ?? []) {
69
+ const path = schemaPath(error);
70
+ add(findings, document, 'capsule-schema', `${label}: ${error.message ?? error.keyword}.`, path);
71
+ }
72
+ }
73
+
74
+ function sha(bytes: Uint8Array): string {
75
+ return createHash('sha256').update(bytes).digest('hex');
76
+ }
77
+
78
+ function bytes(value: string | Uint8Array): Uint8Array {
79
+ return typeof value === 'string' ? Buffer.from(value, 'utf8') : value;
80
+ }
81
+
82
+ function records(value: JsonValue | undefined): Array<Record<string, JsonValue>> {
83
+ return Array.isArray(value) ? value.filter(isRecord) : [];
84
+ }
85
+
86
+ function artifactRecords(value: JsonValue, path = '/'): Array<[Record<string, JsonValue>, string]> {
87
+ const result: Array<[Record<string, JsonValue>, string]> = [];
88
+ if (Array.isArray(value)) {
89
+ value.forEach((child, index) => result.push(...artifactRecords(child, `${path}/${index}`)));
90
+ } else if (isRecord(value)) {
91
+ if (
92
+ typeof value.cid === 'string' &&
93
+ typeof value.sha256 === 'string' &&
94
+ typeof value.uri === 'string'
95
+ ) {
96
+ result.push([value, path]);
97
+ }
98
+ for (const [key, child] of Object.entries(value)) {
99
+ result.push(...artifactRecords(child, `${path === '/' ? '' : path}/${pointerSegment(key)}`));
100
+ }
101
+ }
102
+ return result;
103
+ }
104
+
105
+ function validateArtifactIdentity(
106
+ artifact: Record<string, JsonValue>,
107
+ path: string,
108
+ document: StrictJsonDocument,
109
+ findings: Finding[],
110
+ ): void {
111
+ const declaredCid = artifact.cid;
112
+ const declaredSha = artifact.sha256;
113
+ if (typeof declaredCid !== 'string' || typeof declaredSha !== 'string') return;
114
+ try {
115
+ const cid = CID.parse(declaredCid);
116
+ if (cid.version !== 1 || cid.code !== 0x55 || cid.multihash.code !== sha256.code) {
117
+ add(
118
+ findings,
119
+ document,
120
+ 'capsule-cid',
121
+ 'Only CIDv1 raw sha2-256 content addresses are supported.',
122
+ `${path}/cid`,
123
+ );
124
+ return;
125
+ }
126
+ if (!Buffer.from(cid.multihash.digest).equals(Buffer.from(declaredSha, 'hex'))) {
127
+ add(
128
+ findings,
129
+ document,
130
+ 'capsule-integrity',
131
+ 'CID multihash and declared SHA-256 identify different bytes.',
132
+ path,
133
+ );
134
+ }
135
+ } catch {
136
+ add(findings, document, 'capsule-cid', 'CID is not a valid CIDv1 value.', `${path}/cid`);
137
+ }
138
+ const uri = artifact.uri;
139
+ if (typeof uri === 'string') {
140
+ try {
141
+ const parsed = new URL(uri);
142
+ if (parsed.username || parsed.password || parsed.search) {
143
+ add(
144
+ findings,
145
+ document,
146
+ 'capsule-uri-policy',
147
+ 'Credential-bearing and query-bearing artifact URIs are forbidden.',
148
+ `${path}/uri`,
149
+ );
150
+ }
151
+ } catch {
152
+ add(findings, document, 'capsule-uri-policy', 'Artifact URI is invalid.', `${path}/uri`);
153
+ }
154
+ }
155
+ }
156
+
157
+ function validateExpectedIdentity(
158
+ input: string | Uint8Array,
159
+ expected: NonNullable<CapsuleValidationOptions['expectedIdentity']>,
160
+ document: StrictJsonDocument,
161
+ findings: Finding[],
162
+ ): void {
163
+ const content = bytes(input);
164
+ const actualSha = sha(content);
165
+ if (
166
+ actualSha !== expected.sha256.toLowerCase() ||
167
+ (expected.bytes !== undefined && expected.bytes !== content.byteLength)
168
+ ) {
169
+ add(
170
+ findings,
171
+ document,
172
+ 'capsule-integrity',
173
+ 'Manifest exact bytes do not match the expected SHA-256 or byte length.',
174
+ '/',
175
+ );
176
+ }
177
+ try {
178
+ const cid = CID.parse(expected.cid);
179
+ if (
180
+ cid.version !== 1 ||
181
+ cid.code !== 0x55 ||
182
+ cid.multihash.code !== sha256.code ||
183
+ !Buffer.from(cid.multihash.digest).equals(Buffer.from(actualSha, 'hex'))
184
+ ) {
185
+ add(
186
+ findings,
187
+ document,
188
+ 'capsule-integrity',
189
+ 'Manifest exact bytes do not match the expected CIDv1 raw sha2-256 address.',
190
+ '/',
191
+ );
192
+ }
193
+ } catch {
194
+ add(findings, document, 'capsule-cid', 'Expected manifest CID is invalid.', '/');
195
+ }
196
+ }
197
+
198
+ function uniqueIds(
199
+ entries: Array<Record<string, JsonValue>>,
200
+ path: string,
201
+ document: StrictJsonDocument,
202
+ findings: Finding[],
203
+ ): Set<string> {
204
+ const ids = new Set<string>();
205
+ entries.forEach((entry, index) => {
206
+ if (typeof entry.id !== 'string') return;
207
+ if (ids.has(entry.id))
208
+ add(
209
+ findings,
210
+ document,
211
+ 'capsule-duplicate-id',
212
+ `Duplicate id ${JSON.stringify(entry.id)}.`,
213
+ `${path}/${index}/id`,
214
+ );
215
+ ids.add(entry.id);
216
+ });
217
+ return ids;
218
+ }
219
+
220
+ function validateComponentGraph(
221
+ root: Record<string, JsonValue>,
222
+ document: StrictJsonDocument,
223
+ findings: Finding[],
224
+ ): void {
225
+ const components = records(root.components);
226
+ const ids = uniqueIds(components, '/components', document, findings);
227
+ const graph = new Map<string, string[]>();
228
+ components.forEach((component, index) => {
229
+ if (typeof component.id !== 'string') return;
230
+ const dependencies = Array.isArray(component.dependencies)
231
+ ? component.dependencies.filter((item): item is string => typeof item === 'string')
232
+ : [];
233
+ graph.set(component.id, dependencies);
234
+ for (const dependency of dependencies) {
235
+ if (!ids.has(dependency))
236
+ add(
237
+ findings,
238
+ document,
239
+ 'capsule-reference',
240
+ `Unknown component dependency ${JSON.stringify(dependency)}.`,
241
+ `/components/${index}/dependencies`,
242
+ );
243
+ if (dependency === component.id)
244
+ add(
245
+ findings,
246
+ document,
247
+ 'capsule-dependency-cycle',
248
+ 'A component cannot depend on itself.',
249
+ `/components/${index}/dependencies`,
250
+ );
251
+ }
252
+ });
253
+ const visiting = new Set<string>();
254
+ const visited = new Set<string>();
255
+ const visit = (id: string): boolean => {
256
+ if (visiting.has(id)) return true;
257
+ if (visited.has(id)) return false;
258
+ visiting.add(id);
259
+ for (const dependency of graph.get(id) ?? []) {
260
+ if (graph.has(dependency) && visit(dependency)) return true;
261
+ }
262
+ visiting.delete(id);
263
+ visited.add(id);
264
+ return false;
265
+ };
266
+ for (const id of graph.keys()) {
267
+ if (visit(id)) {
268
+ add(
269
+ findings,
270
+ document,
271
+ 'capsule-dependency-cycle',
272
+ 'Component dependency graph contains a cycle.',
273
+ '/components',
274
+ );
275
+ break;
276
+ }
277
+ }
278
+
279
+ const toolIds = uniqueIds(records(root.tools), '/tools', document, findings);
280
+ const capabilityIds = uniqueIds(
281
+ records(root.requestedCapabilities),
282
+ '/requestedCapabilities',
283
+ document,
284
+ findings,
285
+ );
286
+ void capabilityIds;
287
+ records(root.requestedCapabilities).forEach((request, index) => {
288
+ if (typeof request.tool === 'string' && !toolIds.has(request.tool))
289
+ add(
290
+ findings,
291
+ document,
292
+ 'capsule-reference',
293
+ `Requested capability names unknown tool ${JSON.stringify(request.tool)}.`,
294
+ `/requestedCapabilities/${index}/tool`,
295
+ );
296
+ });
297
+ }
298
+
299
+ function externalChecks(): CapsuleExternalCheck[] {
300
+ const definitions: Record<string, [CapsuleExternalCheck['category'], string]> = {
301
+ 'oracle-identity-current': [
302
+ 'identity',
303
+ 'Resolve the current Oracle DID/IID document and revocation state.',
304
+ ],
305
+ 'subject-domain-current': [
306
+ 'identity',
307
+ 'Resolve each subject domain and its current canonical resources.',
308
+ ],
309
+ 'capability-current-and-unrevoked': [
310
+ 'authorization',
311
+ 'Verify live grants, audience, scope, expiry and revocation before each call.',
312
+ ],
313
+ 'trusted-time-available': [
314
+ 'time',
315
+ 'Evaluate time-bound policy against an approved trusted clock.',
316
+ ],
317
+ 'private-resource-access-authorized': [
318
+ 'privacy',
319
+ 'Verify consent, disclosure and decryption rights before reading private evidence.',
320
+ ],
321
+ 'human-review-authorized': [
322
+ 'review',
323
+ 'Verify the named independent reviewer and exact approved output digest.',
324
+ ],
325
+ 'runtime-distribution-trusted': [
326
+ 'distribution',
327
+ 'Verify the installed kernel and adapters against an external trust anchor.',
328
+ ],
329
+ 'receipt-signer-current-and-unrevoked': [
330
+ 'signer',
331
+ 'Verify the external receipt signer key and current revocation status.',
332
+ ],
333
+ };
334
+ return ORACLE_CAPSULE_EXTERNAL_CHECKS.map((code) => ({
335
+ code,
336
+ category: definitions[code]?.[0] ?? 'authorization',
337
+ reason: definitions[code]?.[1] ?? 'Runtime verification is required.',
338
+ }));
339
+ }
340
+
341
+ function validateSourceLock(
342
+ manifest: Record<string, JsonValue>,
343
+ manifestDocument: StrictJsonDocument,
344
+ sourceLock: string | Uint8Array,
345
+ lockedFiles: Readonly<Record<string, string | Uint8Array>> | undefined,
346
+ findings: Finding[],
347
+ ): void {
348
+ const parsed = parseCapsuleJson(sourceLock, {
349
+ sourceName: `${manifestDocument.sourceName}#source-lock`,
350
+ });
351
+ if (!parsed.document) {
352
+ findings.push(...parsed.findings);
353
+ return;
354
+ }
355
+ const lockDocument = parsed.document;
356
+ const valid = validators.sourceLock(lockDocument.value);
357
+ if (!valid) {
358
+ addSchemaFindings(findings, lockDocument, validators.sourceLock.errors, 'Source lock schema');
359
+ return;
360
+ }
361
+ if (!isRecord(lockDocument.value)) return;
362
+ const lock = lockDocument.value;
363
+ const componentId = lock.component;
364
+ const component = records(manifest.components).find((entry) => entry.id === componentId);
365
+ if (!component || !isRecord(component.artifact)) {
366
+ add(
367
+ findings,
368
+ lockDocument,
369
+ 'capsule-lock',
370
+ 'Source lock component does not resolve in the manifest.',
371
+ '/component',
372
+ );
373
+ return;
374
+ }
375
+ const lockBytes = bytes(sourceLock);
376
+ const sourceLockReference = component.artifact.source_lock;
377
+ if (isRecord(sourceLockReference)) {
378
+ if (
379
+ sourceLockReference.sha256 !== sha(lockBytes) ||
380
+ sourceLockReference.bytes !== lockBytes.byteLength
381
+ ) {
382
+ add(
383
+ findings,
384
+ manifestDocument,
385
+ 'capsule-integrity',
386
+ 'Source lock exact bytes do not match the manifest reference.',
387
+ '/components',
388
+ );
389
+ }
390
+ }
391
+ const lockArtifact = lock.artifact;
392
+ if (
393
+ isRecord(lockArtifact) &&
394
+ (lockArtifact.cid !== component.artifact.cid ||
395
+ lockArtifact.sha256 !== component.artifact.sha256 ||
396
+ lockArtifact.bytes !== component.artifact.bytes)
397
+ ) {
398
+ add(
399
+ findings,
400
+ lockDocument,
401
+ 'capsule-lock',
402
+ 'Source lock artifact identity differs from its manifest component.',
403
+ '/artifact',
404
+ );
405
+ }
406
+ const files = records(lock.files);
407
+ const paths = new Set<string>();
408
+ const portablePaths = new Set<string>();
409
+ let totalBytes = 0;
410
+ files.forEach((file, index) => {
411
+ if (
412
+ typeof file.path !== 'string' ||
413
+ typeof file.bytes !== 'number' ||
414
+ typeof file.sha256 !== 'string'
415
+ )
416
+ return;
417
+ if (paths.has(file.path))
418
+ add(
419
+ findings,
420
+ lockDocument,
421
+ 'capsule-duplicate-path',
422
+ `Duplicate locked path ${JSON.stringify(file.path)}.`,
423
+ `/files/${index}/path`,
424
+ );
425
+ const portablePath = file.path.toLowerCase();
426
+ if (portablePaths.has(portablePath) && !paths.has(file.path))
427
+ add(
428
+ findings,
429
+ lockDocument,
430
+ 'capsule-path-collision',
431
+ `Locked path ${JSON.stringify(file.path)} collides on case-insensitive hosts.`,
432
+ `/files/${index}/path`,
433
+ );
434
+ paths.add(file.path);
435
+ portablePaths.add(portablePath);
436
+ totalBytes += file.bytes;
437
+ if (totalBytes > ORACLE_CAPSULE_LIMITS.maxLockedBytes)
438
+ add(
439
+ findings,
440
+ lockDocument,
441
+ 'capsule-limit',
442
+ `Locked files exceed ${ORACLE_CAPSULE_LIMITS.maxLockedBytes} bytes.`,
443
+ '/files',
444
+ );
445
+ if (lockedFiles) {
446
+ const content = lockedFiles[file.path];
447
+ if (content === undefined) {
448
+ add(
449
+ findings,
450
+ lockDocument,
451
+ 'capsule-lock',
452
+ `Locked file ${JSON.stringify(file.path)} is missing.`,
453
+ `/files/${index}/path`,
454
+ );
455
+ } else {
456
+ const contentBytes = bytes(content);
457
+ if (contentBytes.byteLength !== file.bytes || sha(contentBytes) !== file.sha256)
458
+ add(
459
+ findings,
460
+ lockDocument,
461
+ 'capsule-integrity',
462
+ `Locked file ${JSON.stringify(file.path)} failed byte-length or SHA-256 verification.`,
463
+ `/files/${index}`,
464
+ );
465
+ }
466
+ }
467
+ });
468
+ if (lockedFiles) {
469
+ for (const path of Object.keys(lockedFiles)) {
470
+ if (!paths.has(path))
471
+ add(
472
+ findings,
473
+ lockDocument,
474
+ 'capsule-lock',
475
+ `Unlocked file ${JSON.stringify(path)} is forbidden.`,
476
+ '/files',
477
+ );
478
+ }
479
+ }
480
+ }
481
+
482
+ export function validateOracleCapsule(
483
+ input: string | Uint8Array,
484
+ options: CapsuleValidationOptions = {},
485
+ ): CapsuleValidationReport {
486
+ const parsed = parseCapsuleJson(input, options);
487
+ const checks = externalChecks();
488
+ if (!parsed.document) {
489
+ return {
490
+ tool: '@ixo/domain.md',
491
+ contract: ORACLE_CAPSULE_CONTRACT,
492
+ sourceName: options.sourceName ?? '<memory>',
493
+ status: 'fail',
494
+ ok: false,
495
+ findings: parsed.findings,
496
+ externalChecksRequired: checks,
497
+ };
498
+ }
499
+ const findings = [...parsed.findings];
500
+ if (options.expectedIdentity)
501
+ validateExpectedIdentity(input, options.expectedIdentity, parsed.document, findings);
502
+ const validator = validators.manifest;
503
+ if (!validator(parsed.document.value))
504
+ addSchemaFindings(findings, parsed.document, validator.errors, 'Manifest schema');
505
+ if (isRecord(parsed.document.value)) {
506
+ for (const [artifact, path] of artifactRecords(parsed.document.value))
507
+ validateArtifactIdentity(artifact, path, parsed.document, findings);
508
+ validateComponentGraph(parsed.document.value, parsed.document, findings);
509
+ const metadata = parsed.document.value.metadata;
510
+ if (isRecord(metadata) && typeof metadata.release_digest === 'string') {
511
+ try {
512
+ const actual = oracleCapsuleReleaseDigest(parsed.document.value);
513
+ if (metadata.release_digest !== actual)
514
+ add(
515
+ findings,
516
+ parsed.document,
517
+ 'capsule-release-digest',
518
+ `Release digest mismatch; expected ${actual}.`,
519
+ '/metadata/release_digest',
520
+ );
521
+ } catch (error) {
522
+ add(
523
+ findings,
524
+ parsed.document,
525
+ 'capsule-canonicalization',
526
+ error instanceof Error ? error.message : String(error),
527
+ '/',
528
+ );
529
+ }
530
+ }
531
+ if (options.sourceLock)
532
+ validateSourceLock(
533
+ parsed.document.value,
534
+ parsed.document,
535
+ options.sourceLock,
536
+ options.lockedFiles,
537
+ findings,
538
+ );
539
+ }
540
+ const ok = !findings.some((finding) => finding.severity === 'error');
541
+ return {
542
+ tool: '@ixo/domain.md',
543
+ contract: ORACLE_CAPSULE_CONTRACT,
544
+ sourceName: parsed.document.sourceName,
545
+ status: ok ? 'static-pass' : 'fail',
546
+ ok,
547
+ findings,
548
+ externalChecksRequired: checks,
549
+ manifest: parsed.document.value,
550
+ };
551
+ }