@open-agent-toolkit/cli 0.2.4 → 0.2.6

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 (106) hide show
  1. package/assets/docs/cli-utilities/configuration.md +40 -0
  2. package/assets/docs/cli-utilities/tool-packs.md +31 -0
  3. package/assets/docs/workflows/projects/artifacts.md +27 -0
  4. package/assets/docs/workflows/projects/implementation-execution.md +30 -5
  5. package/assets/docs/workflows/skills/explainer-kit.md +125 -0
  6. package/assets/docs/workflows/skills/index.md +4 -0
  7. package/assets/migration/pjm-restructure.md +12 -0
  8. package/assets/public-package-versions.json +4 -4
  9. package/assets/skills/explainer-kit/SKILL.md +95 -0
  10. package/assets/skills/explainer-kit/examples/project-explainer/content.md +31 -0
  11. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +22 -0
  12. package/assets/skills/explainer-kit/examples/project-recap/content.md +34 -0
  13. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +22 -0
  14. package/assets/skills/explainer-kit/examples/theme-bundle.json +87 -0
  15. package/assets/skills/explainer-kit/palettes/ember.json +37 -0
  16. package/assets/skills/explainer-kit/palettes/forest.json +37 -0
  17. package/assets/skills/explainer-kit/palettes/neutral.json +37 -0
  18. package/assets/skills/explainer-kit/palettes/ocean.json +37 -0
  19. package/assets/skills/explainer-kit/palettes/violet.json +37 -0
  20. package/assets/skills/explainer-kit/profiles/clean.json +42 -0
  21. package/assets/skills/explainer-kit/profiles/editorial.json +42 -0
  22. package/assets/skills/explainer-kit/profiles/technical.json +39 -0
  23. package/assets/skills/explainer-kit/recipes/engineer-tour.json +33 -0
  24. package/assets/skills/explainer-kit/recipes/program-recap.json +34 -0
  25. package/assets/skills/explainer-kit/recipes/project-explainer.json +33 -0
  26. package/assets/skills/explainer-kit/recipes/project-recap.json +34 -0
  27. package/assets/skills/explainer-kit/references/contracts.md +101 -0
  28. package/assets/skills/explainer-kit/references/destination-contract.md +94 -0
  29. package/assets/skills/explainer-kit/references/extension-contract.md +71 -0
  30. package/assets/skills/explainer-kit/references/fact-base-contract.md +119 -0
  31. package/assets/skills/explainer-kit/schemas/build-record.schema.json +92 -0
  32. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +53 -0
  33. package/assets/skills/explainer-kit/schemas/fact-base.schema.json +129 -0
  34. package/assets/skills/explainer-kit/schemas/manifest.schema.json +193 -0
  35. package/assets/skills/explainer-kit/schemas/publish-receipt.schema.json +97 -0
  36. package/assets/skills/explainer-kit/schemas/publish-request.schema.json +32 -0
  37. package/assets/skills/explainer-kit/schemas/run-request.schema.json +132 -0
  38. package/assets/skills/explainer-kit/schemas/theme.schema.json +217 -0
  39. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +180 -0
  40. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +680 -0
  41. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +538 -0
  42. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +494 -0
  43. package/assets/skills/explainer-kit/scripts/lib/fs-safe.mjs +218 -0
  44. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +512 -0
  45. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +289 -0
  46. package/assets/skills/explainer-kit/scripts/lib/records.mjs +231 -0
  47. package/assets/skills/explainer-kit/scripts/lib/render.mjs +485 -0
  48. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +587 -0
  49. package/assets/skills/explainer-kit/scripts/lib/safe-paths.mjs +96 -0
  50. package/assets/skills/explainer-kit/scripts/lib/theme.mjs +332 -0
  51. package/assets/skills/explainer-kit/scripts/publish.mjs +76 -0
  52. package/assets/skills/explainer-kit/scripts/record-durability.mjs +64 -0
  53. package/assets/skills/explainer-kit/scripts/render-qa.mjs +273 -0
  54. package/assets/skills/explainer-kit/scripts/run.mjs +778 -0
  55. package/assets/skills/explainer-kit/scripts/validate.mjs +59 -0
  56. package/assets/skills/explainer-kit/templates/deck-shell.html +233 -0
  57. package/assets/skills/explainer-kit/templates/diagram-shell.html +219 -0
  58. package/assets/skills/explainer-kit/templates/engineer-tour.html +273 -0
  59. package/assets/skills/explainer-kit/templates/house-style.html +218 -0
  60. package/assets/skills/oat-explainer-kit/SKILL.md +92 -0
  61. package/assets/skills/oat-explainer-kit/references/config-contract.md +67 -0
  62. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +143 -0
  63. package/assets/skills/oat-explainer-kit/references/migration.md +143 -0
  64. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +103 -0
  65. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +127 -0
  66. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +392 -0
  67. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +130 -0
  68. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +293 -0
  69. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +209 -0
  70. package/assets/skills/oat-explainer-kit/scripts/resolve-paths.mjs +166 -0
  71. package/assets/skills/oat-explainer-kit/scripts/run.mjs +284 -0
  72. package/assets/skills/oat-project-autonomous/SKILL.md +22 -1
  73. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +5 -1
  74. package/assets/skills/oat-project-complete/SKILL.md +166 -28
  75. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +5 -1
  76. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  77. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +84 -9
  78. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +5 -1
  79. package/assets/skills/oat-project-next/SKILL.md +31 -11
  80. package/assets/skills/oat-project-plan/SKILL.md +30 -1
  81. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +5 -1
  82. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +5 -1
  83. package/assets/skills/oat-project-summary/SKILL.md +20 -1
  84. package/assets/templates/state.md +4 -1
  85. package/dist/commands/config/index.d.ts.map +1 -1
  86. package/dist/commands/config/index.js +232 -1
  87. package/dist/commands/init/tools/shared/skill-manifest.d.ts +2 -2
  88. package/dist/commands/init/tools/shared/skill-manifest.d.ts.map +1 -1
  89. package/dist/commands/init/tools/shared/skill-manifest.js +2 -0
  90. package/dist/commands/project/archive/archive-utils.d.ts +12 -0
  91. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  92. package/dist/commands/project/archive/archive-utils.js +320 -8
  93. package/dist/commands/project/archive/index.d.ts.map +1 -1
  94. package/dist/commands/project/archive/index.js +1 -0
  95. package/dist/commands/project/archive/push-runner.d.ts +1 -0
  96. package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
  97. package/dist/commands/project/archive/push-runner.js +9 -2
  98. package/dist/config/oat-config.d.ts +26 -0
  99. package/dist/config/oat-config.d.ts.map +1 -1
  100. package/dist/config/oat-config.js +88 -0
  101. package/dist/config/resolve.d.ts.map +1 -1
  102. package/dist/config/resolve.js +18 -0
  103. package/dist/validation/project-state.d.ts +3 -0
  104. package/dist/validation/project-state.d.ts.map +1 -1
  105. package/dist/validation/project-state.js +91 -0
  106. package/package.json +2 -2
@@ -0,0 +1,680 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+
4
+ import { validatePortablePath } from './safe-paths.mjs';
5
+
6
+ const SCHEMA_FILES = {
7
+ 'run-request': 'run-request.schema.json',
8
+ 'fact-base': 'fact-base.schema.json',
9
+ theme: 'theme.schema.json',
10
+ manifest: 'manifest.schema.json',
11
+ 'build-record': 'build-record.schema.json',
12
+ 'durability-evidence': 'durability-evidence.schema.json',
13
+ 'publish-request': 'publish-request.schema.json',
14
+ 'publish-receipt': 'publish-receipt.schema.json',
15
+ };
16
+
17
+ const SCHEMAS = Object.fromEntries(
18
+ Object.entries(SCHEMA_FILES).map(([kind, file]) => [
19
+ kind,
20
+ JSON.parse(
21
+ readFileSync(new URL(`../../schemas/${file}`, import.meta.url), 'utf8'),
22
+ ),
23
+ ]),
24
+ );
25
+ const SCHEMAS_BY_ID = new Map(
26
+ Object.values(SCHEMAS).map((schema) => [schema.$id, schema]),
27
+ );
28
+ const RAW_SECRET_KEYS = new Set([
29
+ 'accesskey',
30
+ 'accesskeyid',
31
+ 'awsaccesskeyid',
32
+ 'awssecretaccesskey',
33
+ 'awssessiontoken',
34
+ 'clientsecret',
35
+ 'credentials',
36
+ 'password',
37
+ 'privatekey',
38
+ 'secretkey',
39
+ 'sessiontoken',
40
+ 'token',
41
+ ]);
42
+ const DATE_TIME_PATTERN =
43
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
44
+
45
+ export function validateContract(kind, value, context = {}) {
46
+ const schema = SCHEMAS[kind];
47
+ if (!schema) {
48
+ return {
49
+ valid: false,
50
+ errors: [
51
+ {
52
+ path: '$',
53
+ code: 'unknown-kind',
54
+ message: `Unknown contract kind: ${kind}`,
55
+ },
56
+ ],
57
+ };
58
+ }
59
+
60
+ const errors = [];
61
+ findRawSecrets(value, '$', errors);
62
+ validateSchema(schema, value, '$', schema, errors);
63
+ validateContractPaths(kind, value, errors);
64
+ validateCrossRecord(kind, value, context, errors);
65
+ return { valid: errors.length === 0, errors };
66
+ }
67
+
68
+ export function canonicalHash(value) {
69
+ return `sha256:${createHash('sha256')
70
+ .update(canonicalStringify(value))
71
+ .digest('hex')}`;
72
+ }
73
+
74
+ export function canonicalStringify(value) {
75
+ return JSON.stringify(canonicalize(value));
76
+ }
77
+
78
+ function canonicalize(value) {
79
+ if (Array.isArray(value)) {
80
+ return value.map(canonicalize);
81
+ }
82
+ if (isObject(value)) {
83
+ return Object.fromEntries(
84
+ Object.keys(value)
85
+ .sort()
86
+ .map((key) => [key, canonicalize(value[key])]),
87
+ );
88
+ }
89
+ return value;
90
+ }
91
+
92
+ function validateSchema(schema, value, path, rootSchema, errors) {
93
+ if (schema.$ref) {
94
+ if (schema.$ref.endsWith('/safeRelativePath')) {
95
+ addLexicalPathErrors(value, path, errors, false);
96
+ } else if (schema.$ref.endsWith('/relativeOrAbsolutePath')) {
97
+ addLexicalPathErrors(value, path, errors, true);
98
+ }
99
+ const resolved = resolveReference(schema.$ref, rootSchema);
100
+ if (!resolved) {
101
+ add(
102
+ errors,
103
+ path,
104
+ 'invalid-schema-ref',
105
+ `Unknown schema ref ${schema.$ref}`,
106
+ );
107
+ return;
108
+ }
109
+ validateSchema(resolved.schema, value, path, resolved.root, errors);
110
+ }
111
+
112
+ if (schema.allOf) {
113
+ for (const child of schema.allOf) {
114
+ validateSchema(child, value, path, rootSchema, errors);
115
+ }
116
+ }
117
+
118
+ if (schema.oneOf) {
119
+ const matches = schema.oneOf.filter((child) => {
120
+ const branchErrors = [];
121
+ validateSchema(child, value, path, rootSchema, branchErrors);
122
+ return branchErrors.length === 0;
123
+ });
124
+ if (matches.length !== 1) {
125
+ add(
126
+ errors,
127
+ path,
128
+ 'one-of',
129
+ 'Value must match exactly one allowed shape.',
130
+ );
131
+ }
132
+ }
133
+
134
+ if ('const' in schema && !deepEqual(value, schema.const)) {
135
+ add(
136
+ errors,
137
+ path,
138
+ path.endsWith('.schemaVersion') ? 'schema-version' : 'const',
139
+ `Value must equal ${JSON.stringify(schema.const)}.`,
140
+ );
141
+ }
142
+ if (schema.enum && !schema.enum.some((entry) => deepEqual(value, entry))) {
143
+ add(
144
+ errors,
145
+ path,
146
+ 'enum',
147
+ `Value must be one of ${schema.enum.map(JSON.stringify).join(', ')}.`,
148
+ );
149
+ }
150
+
151
+ if (schema.type && !matchesType(value, schema.type)) {
152
+ add(errors, path, 'type', `Value must be ${schema.type}.`);
153
+ return;
154
+ }
155
+
156
+ if (typeof value === 'string') {
157
+ if (schema.minLength !== undefined && value.length < schema.minLength) {
158
+ add(errors, path, 'min-length', 'String is shorter than allowed.');
159
+ }
160
+ if (schema.pattern && !new RegExp(schema.pattern).test(value)) {
161
+ add(
162
+ errors,
163
+ path,
164
+ 'pattern',
165
+ 'String does not match the required pattern.',
166
+ );
167
+ }
168
+ if (schema.format === 'date-time' && !isDateTime(value)) {
169
+ add(errors, path, 'format', 'String must be an ISO 8601 timestamp.');
170
+ }
171
+ if (schema.format === 'uri' && !isUri(value)) {
172
+ add(errors, path, 'format', 'String must be an absolute URI.');
173
+ }
174
+ }
175
+
176
+ if (typeof value === 'number') {
177
+ if (!Number.isFinite(value)) {
178
+ add(errors, path, 'number', 'Number must be finite.');
179
+ }
180
+ if (schema.minimum !== undefined && value < schema.minimum) {
181
+ add(
182
+ errors,
183
+ path,
184
+ 'minimum',
185
+ `Number must be at least ${schema.minimum}.`,
186
+ );
187
+ }
188
+ if (
189
+ schema.exclusiveMinimum !== undefined &&
190
+ value <= schema.exclusiveMinimum
191
+ ) {
192
+ add(
193
+ errors,
194
+ path,
195
+ 'exclusive-minimum',
196
+ `Number must be greater than ${schema.exclusiveMinimum}.`,
197
+ );
198
+ }
199
+ if (schema.maximum !== undefined && value > schema.maximum) {
200
+ add(errors, path, 'maximum', `Number must be at most ${schema.maximum}.`);
201
+ }
202
+ }
203
+
204
+ if (Array.isArray(value)) {
205
+ if (schema.minItems !== undefined && value.length < schema.minItems) {
206
+ add(errors, path, 'min-items', 'Array has too few items.');
207
+ }
208
+ if (schema.uniqueItems) {
209
+ const identities = value.map(canonicalStringify);
210
+ if (new Set(identities).size !== identities.length) {
211
+ add(errors, path, 'unique-items', 'Array items must be unique.');
212
+ }
213
+ }
214
+ if (schema.items) {
215
+ value.forEach((item, index) =>
216
+ validateSchema(
217
+ schema.items,
218
+ item,
219
+ `${path}[${index}]`,
220
+ rootSchema,
221
+ errors,
222
+ ),
223
+ );
224
+ }
225
+ }
226
+
227
+ if (isObject(value)) {
228
+ const properties = schema.properties ?? {};
229
+ for (const required of schema.required ?? []) {
230
+ if (!(required in value)) {
231
+ add(
232
+ errors,
233
+ `${path}.${required}`,
234
+ 'required',
235
+ `Required property ${required} is missing.`,
236
+ );
237
+ }
238
+ }
239
+ for (const [key, child] of Object.entries(properties)) {
240
+ if (key in value) {
241
+ validateSchema(child, value[key], `${path}.${key}`, rootSchema, errors);
242
+ }
243
+ }
244
+ for (const [key, childValue] of Object.entries(value)) {
245
+ if (key in properties) {
246
+ continue;
247
+ }
248
+ if (schema.additionalProperties === false) {
249
+ add(
250
+ errors,
251
+ `${path}.${key}`,
252
+ 'unknown-key',
253
+ `Unknown property ${key}.`,
254
+ );
255
+ } else if (isObject(schema.additionalProperties)) {
256
+ validateSchema(
257
+ schema.additionalProperties,
258
+ childValue,
259
+ `${path}.${key}`,
260
+ rootSchema,
261
+ errors,
262
+ );
263
+ }
264
+ }
265
+ if (schema.propertyNames) {
266
+ for (const key of Object.keys(value)) {
267
+ validateSchema(
268
+ schema.propertyNames,
269
+ key,
270
+ `${path}.${key}`,
271
+ rootSchema,
272
+ errors,
273
+ );
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ function validateContractPaths(kind, value, errors) {
280
+ if (!isObject(value)) {
281
+ return;
282
+ }
283
+
284
+ if (kind === 'run-request') {
285
+ addLexicalPathErrors(value.outputRoot, '$.outputRoot', errors, true);
286
+ if (isObject(value.factBase)) {
287
+ addLexicalPathErrors(
288
+ value.factBase.path,
289
+ '$.factBase.path',
290
+ errors,
291
+ true,
292
+ );
293
+ }
294
+ if (isObject(value.theme)) {
295
+ addLexicalPathErrors(
296
+ value.theme.suppliedBundlePath,
297
+ '$.theme.suppliedBundlePath',
298
+ errors,
299
+ true,
300
+ );
301
+ }
302
+ if (isObject(value.durability) && isObject(value.durability.publish)) {
303
+ validateContractPaths(
304
+ 'publish-request',
305
+ value.durability.publish,
306
+ errors,
307
+ );
308
+ }
309
+ }
310
+
311
+ if (kind === 'publish-request') {
312
+ addLexicalPathErrors(value.siteRoot, '$.siteRoot', errors, true);
313
+ addLexicalPathErrors(value.manifestPath, '$.manifestPath', errors, true);
314
+ }
315
+
316
+ if (kind === 'durability-evidence') {
317
+ addLexicalPathErrors(value.manifestPath, '$.manifestPath', errors, true);
318
+ if (isObject(value.evidence)) {
319
+ addLexicalPathErrors(
320
+ value.evidence.repoRoot,
321
+ '$.evidence.repoRoot',
322
+ errors,
323
+ true,
324
+ );
325
+ addLexicalPathErrors(
326
+ value.evidence.receiptPath,
327
+ '$.evidence.receiptPath',
328
+ errors,
329
+ true,
330
+ );
331
+ if (Array.isArray(value.evidence.paths)) {
332
+ value.evidence.paths.forEach((candidate, index) =>
333
+ addLexicalPathErrors(
334
+ candidate,
335
+ `$.evidence.paths[${index}]`,
336
+ errors,
337
+ false,
338
+ ),
339
+ );
340
+ }
341
+ }
342
+ }
343
+
344
+ if (kind === 'manifest' && Array.isArray(value.artifacts)) {
345
+ value.artifacts.forEach((artifact, index) => {
346
+ if (isObject(artifact) && isObject(artifact.rebuild)) {
347
+ addLexicalPathErrors(
348
+ artifact.rebuild.cwd,
349
+ `$.artifacts[${index}].rebuild.cwd`,
350
+ errors,
351
+ true,
352
+ );
353
+ }
354
+ });
355
+ }
356
+ }
357
+
358
+ function addLexicalPathErrors(value, path, errors, allowAbsolute) {
359
+ if (value === undefined) {
360
+ return;
361
+ }
362
+ if (typeof value !== 'string') {
363
+ return;
364
+ }
365
+
366
+ const result = validatePortablePath(value, { allowAbsolute });
367
+ if (!result.valid) {
368
+ add(errors, path, 'unsafe-path', result.errors[0].message);
369
+ }
370
+ }
371
+
372
+ function resolveReference(reference, rootSchema) {
373
+ if (reference.startsWith('#/')) {
374
+ const parts = reference
375
+ .slice(2)
376
+ .split('/')
377
+ .map((part) => part.replaceAll('~1', '/').replaceAll('~0', '~'));
378
+ let schema = rootSchema;
379
+ for (const part of parts) {
380
+ schema = schema?.[part];
381
+ }
382
+ return schema ? { schema, root: rootSchema } : null;
383
+ }
384
+ const external = SCHEMAS_BY_ID.get(reference);
385
+ return external ? { schema: external, root: external } : null;
386
+ }
387
+
388
+ function validateCrossRecord(kind, value, context, errors) {
389
+ if (!isObject(value)) {
390
+ return;
391
+ }
392
+
393
+ if (kind === 'run-request') {
394
+ const factBase = value.factBase;
395
+ if (isObject(factBase)) {
396
+ const hasPath = typeof factBase.path === 'string';
397
+ const hasSources = Array.isArray(factBase.sources);
398
+ if (
399
+ (factBase.mode === 'supplied' && (!hasPath || hasSources)) ||
400
+ (factBase.mode === 'federated' && (!hasSources || hasPath))
401
+ ) {
402
+ add(
403
+ errors,
404
+ '$.factBase',
405
+ 'fact-base-fields',
406
+ 'Supplied fact bases require only path; federated fact bases require only sources.',
407
+ );
408
+ }
409
+ }
410
+
411
+ const durability = value.durability;
412
+ if (
413
+ isObject(durability) &&
414
+ durability.strategy === 'publish' &&
415
+ !isObject(durability.publish)
416
+ ) {
417
+ add(
418
+ errors,
419
+ '$.durability.publish',
420
+ 'incomplete-publish',
421
+ 'Publish durability requires a complete publish request.',
422
+ );
423
+ }
424
+ if (
425
+ isObject(durability) &&
426
+ durability.strategy !== 'publish' &&
427
+ 'publish' in durability
428
+ ) {
429
+ add(
430
+ errors,
431
+ '$.durability.publish',
432
+ 'unexpected-publish',
433
+ 'Publish settings are allowed only for publish durability.',
434
+ );
435
+ }
436
+
437
+ if (
438
+ isObject(value.privacy) &&
439
+ value.privacy.retainRawArtDirection === true &&
440
+ (!isObject(value.theme) ||
441
+ typeof value.theme.artDirection !== 'string' ||
442
+ value.theme.artDirection.length === 0)
443
+ ) {
444
+ add(
445
+ errors,
446
+ '$.privacy.retainRawArtDirection',
447
+ 'art-direction-required',
448
+ 'Retaining raw art direction requires theme.artDirection.',
449
+ );
450
+ }
451
+ }
452
+
453
+ if (kind === 'manifest') {
454
+ const paths = [];
455
+ for (const artifact of Array.isArray(value.artifacts)
456
+ ? value.artifacts
457
+ : []) {
458
+ if (!isObject(artifact)) {
459
+ continue;
460
+ }
461
+ for (const field of ['contentPath', 'renderedPath']) {
462
+ if (typeof artifact[field] === 'string') {
463
+ paths.push(artifact[field]);
464
+ }
465
+ }
466
+ if (artifact.status === 'built' && typeof artifact.hash !== 'string') {
467
+ add(
468
+ errors,
469
+ '$.artifacts',
470
+ 'built-artifact-hash-required',
471
+ 'Built artifacts require a canonical hash.',
472
+ );
473
+ }
474
+ if (artifact.rebuildable === true && !isObject(artifact.rebuild)) {
475
+ add(
476
+ errors,
477
+ '$.artifacts',
478
+ 'rebuild-metadata-required',
479
+ 'Rebuildable artifacts require rebuild metadata.',
480
+ );
481
+ }
482
+ if (
483
+ value.outcome === 'built-durable' &&
484
+ artifact.status === 'built' &&
485
+ (!Array.isArray(artifact.durableEvidence) ||
486
+ artifact.durableEvidence.length === 0)
487
+ ) {
488
+ add(
489
+ errors,
490
+ '$.artifacts',
491
+ 'durability-evidence-required',
492
+ 'Durable built artifacts require durability evidence.',
493
+ );
494
+ }
495
+ }
496
+ if (new Set(paths).size !== paths.length) {
497
+ add(
498
+ errors,
499
+ '$.artifacts',
500
+ 'duplicate-artifact-path',
501
+ 'Artifact content and rendered paths must be unique.',
502
+ );
503
+ }
504
+
505
+ const expectedImmutable = new Set([
506
+ value.source?.factBasePath,
507
+ 'source/fact-base.md',
508
+ value.theme?.path,
509
+ ...(Array.isArray(value.artifacts)
510
+ ? value.artifacts.flatMap((artifact) => [
511
+ artifact?.contentPath,
512
+ ...(artifact?.status === 'built' &&
513
+ typeof artifact?.renderedPath === 'string'
514
+ ? [artifact.renderedPath]
515
+ : []),
516
+ ])
517
+ : []),
518
+ ]);
519
+ expectedImmutable.delete(undefined);
520
+ const recordedImmutable = isObject(value.immutableHashes)
521
+ ? new Set(Object.keys(value.immutableHashes))
522
+ : new Set();
523
+ if (
524
+ expectedImmutable.size !== recordedImmutable.size ||
525
+ [...expectedImmutable].some((path) => !recordedImmutable.has(path))
526
+ ) {
527
+ add(
528
+ errors,
529
+ '$.immutableHashes',
530
+ 'immutable-package-incomplete',
531
+ 'Manifest immutable hashes must cover the complete retained fact-base, content, theme, and required built artifact package.',
532
+ );
533
+ }
534
+
535
+ const record = context.buildRecord;
536
+ if (isObject(record)) {
537
+ if (value.runId !== record.runId || value.outcome !== record.outcome) {
538
+ add(
539
+ errors,
540
+ '$',
541
+ 'cross-record-mismatch',
542
+ 'Manifest and build record identity or outcome do not match.',
543
+ );
544
+ }
545
+ if (
546
+ isObject(value.buildRecord) &&
547
+ value.buildRecord.hash !== canonicalHash(record)
548
+ ) {
549
+ add(
550
+ errors,
551
+ '$.buildRecord.hash',
552
+ 'hash-mismatch',
553
+ 'Build record hash does not match canonical content.',
554
+ );
555
+ }
556
+ }
557
+ if (
558
+ isObject(context.theme) &&
559
+ isObject(value.theme) &&
560
+ value.theme.hash !== canonicalHash(context.theme)
561
+ ) {
562
+ add(
563
+ errors,
564
+ '$.theme.hash',
565
+ 'hash-mismatch',
566
+ 'Theme hash does not match canonical content.',
567
+ );
568
+ }
569
+ if (isObject(context.runRequest) && isObject(record)) {
570
+ const expected =
571
+ isObject(context.runRequest.theme) &&
572
+ typeof context.runRequest.theme.renderStrategy === 'string'
573
+ ? context.runRequest.theme.renderStrategy
574
+ : 'default-only';
575
+ if (record.renderStrategy !== expected) {
576
+ add(
577
+ errors,
578
+ '$.renderStrategy',
579
+ 'cross-record-mismatch',
580
+ 'Build render strategy does not match the run request.',
581
+ );
582
+ }
583
+ }
584
+ }
585
+
586
+ if (
587
+ kind === 'publish-receipt' &&
588
+ isObject(context.manifest) &&
589
+ Array.isArray(value.artifacts)
590
+ ) {
591
+ const expected = new Map();
592
+ for (const artifact of context.manifest.artifacts ?? []) {
593
+ if (isObject(artifact) && typeof artifact.renderedPath === 'string') {
594
+ expected.set(artifact.renderedPath, artifact.hash);
595
+ }
596
+ }
597
+ for (const artifact of value.artifacts) {
598
+ if (
599
+ isObject(artifact) &&
600
+ expected.get(artifact.relativePath) !== artifact.hash
601
+ ) {
602
+ add(
603
+ errors,
604
+ '$.artifacts',
605
+ 'cross-record-mismatch',
606
+ 'Publish receipt artifact does not match the manifest.',
607
+ );
608
+ }
609
+ }
610
+ }
611
+ }
612
+
613
+ function findRawSecrets(value, path, errors) {
614
+ if (Array.isArray(value)) {
615
+ value.forEach((item, index) =>
616
+ findRawSecrets(item, `${path}[${index}]`, errors),
617
+ );
618
+ return;
619
+ }
620
+ if (!isObject(value)) {
621
+ return;
622
+ }
623
+ for (const [key, child] of Object.entries(value)) {
624
+ const normalized = key.toLowerCase().replaceAll(/[^a-z0-9]/g, '');
625
+ if (RAW_SECRET_KEYS.has(normalized)) {
626
+ add(
627
+ errors,
628
+ `${path}.${key}`,
629
+ 'raw-secret-field',
630
+ `Raw secret field ${key} is forbidden.`,
631
+ );
632
+ }
633
+ findRawSecrets(child, `${path}.${key}`, errors);
634
+ }
635
+ }
636
+
637
+ function matchesType(value, type) {
638
+ switch (type) {
639
+ case 'object':
640
+ return isObject(value);
641
+ case 'array':
642
+ return Array.isArray(value);
643
+ case 'string':
644
+ return typeof value === 'string';
645
+ case 'number':
646
+ return typeof value === 'number' && Number.isFinite(value);
647
+ case 'integer':
648
+ return Number.isInteger(value);
649
+ case 'boolean':
650
+ return typeof value === 'boolean';
651
+ case 'null':
652
+ return value === null;
653
+ default:
654
+ return false;
655
+ }
656
+ }
657
+
658
+ function isObject(value) {
659
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
660
+ }
661
+
662
+ function isDateTime(value) {
663
+ return DATE_TIME_PATTERN.test(value) && !Number.isNaN(Date.parse(value));
664
+ }
665
+
666
+ function isUri(value) {
667
+ try {
668
+ return Boolean(new URL(value).protocol);
669
+ } catch {
670
+ return false;
671
+ }
672
+ }
673
+
674
+ function deepEqual(left, right) {
675
+ return canonicalStringify(left) === canonicalStringify(right);
676
+ }
677
+
678
+ function add(errors, path, code, message) {
679
+ errors.push({ path, code, message });
680
+ }