@open-agent-toolkit/cli 0.2.73 → 0.2.74

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 (149) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/configuration.md +3 -56
  3. package/assets/docs/cli-utilities/tool-packs.md +4 -4
  4. package/assets/docs/contributing/code.md +2 -2
  5. package/assets/docs/contributing/index.md +0 -1
  6. package/assets/docs/reference/cli-reference.md +1 -1
  7. package/assets/docs/reference/troubleshooting.md +17 -40
  8. package/assets/docs/workflows/projects/artifacts.md +16 -34
  9. package/assets/docs/workflows/projects/lifecycle.md +20 -19
  10. package/assets/docs/workflows/skills/explainer-kit.md +180 -440
  11. package/assets/docs/workflows/skills/index.md +1 -2
  12. package/assets/public-package-versions.json +4 -4
  13. package/assets/skills/explainer-kit/SKILL.md +96 -171
  14. package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
  15. package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
  16. package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
  17. package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
  18. package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
  19. package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
  20. package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
  21. package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
  22. package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
  23. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
  24. package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
  25. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
  26. package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
  27. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
  28. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
  29. package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
  30. package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
  31. package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
  32. package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
  33. package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
  34. package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
  35. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
  36. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
  37. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
  38. package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
  39. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
  40. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
  41. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
  42. package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
  43. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
  44. package/assets/skills/oat-project-complete/SKILL.md +168 -349
  45. package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
  46. package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
  47. package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
  48. package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
  49. package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
  50. package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
  51. package/assets/skills/oat-project-document/SKILL.md +1 -1
  52. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
  53. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  54. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
  55. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
  56. package/assets/skills/oat-project-lite/SKILL.md +1 -1
  57. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
  58. package/assets/skills/oat-project-plan/SKILL.md +5 -6
  59. package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
  60. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
  61. package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
  62. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
  63. package/assets/skills/oat-project-summary/SKILL.md +35 -25
  64. package/assets/skills/oat-wave-execute/SKILL.md +11 -36
  65. package/assets/skills/oat-wave-program/SKILL.md +13 -37
  66. package/dist/commands/config/index.d.ts.map +1 -1
  67. package/dist/commands/config/index.js +9 -113
  68. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  69. package/dist/commands/project/archive/archive-utils.js +32 -184
  70. package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
  71. package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
  72. package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
  73. package/dist/config/oat-config.d.ts +0 -11
  74. package/dist/config/oat-config.d.ts.map +1 -1
  75. package/dist/config/oat-config.js +0 -34
  76. package/dist/config/resolve.d.ts.map +1 -1
  77. package/dist/config/resolve.js +0 -8
  78. package/dist/release/public-package-contract.d.ts.map +1 -1
  79. package/dist/release/public-package-contract.js +2 -10
  80. package/dist/validation/project-state.d.ts.map +1 -1
  81. package/dist/validation/project-state.js +42 -3
  82. package/package.json +2 -2
  83. package/assets/docs/contributing/explainer-kit-verification.md +0 -133
  84. package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
  85. package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
  86. package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
  87. package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
  88. package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
  89. package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
  90. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
  91. package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
  92. package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
  93. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
  94. package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
  95. package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
  96. package/assets/skills/explainer-kit/references/contracts.md +0 -324
  97. package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
  98. package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
  99. package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
  100. package/assets/skills/explainer-kit/references/visual-review.md +0 -71
  101. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
  102. package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
  103. package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
  104. package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
  105. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
  106. package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
  107. package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
  108. package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
  109. package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
  110. package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
  111. package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
  112. package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
  113. package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
  114. package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
  115. package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
  116. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
  117. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
  118. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
  119. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
  120. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
  121. package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
  122. package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
  123. package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
  124. package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
  125. package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
  126. package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
  127. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
  128. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
  129. package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
  130. package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
  131. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
  132. package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
  133. package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
  134. package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
  135. package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
  136. package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
  137. package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
  138. package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
  139. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
  140. package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
  141. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
  142. package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
  143. package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
  144. package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
  145. package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
  146. package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
  147. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
  148. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
  149. package/dist/commands/project/archive/explainer-terminal-evidence.js +0 -37
@@ -1,16 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
 
3
- const RECIPE_SCHEMA_V2 = 'explainer-kit.recipe/v2';
4
- const RECIPE_V2_REQUIRED_ROOT_KEYS = [
5
- 'discoveryLimits',
6
- 'expansion',
7
- 'floor',
8
- 'id',
9
- 'schemaVersion',
10
- 'sourceRoles',
11
- 'version',
12
- ];
13
- const RECIPE_V2_ROOT_KEYS = [...RECIPE_V2_REQUIRED_ROOT_KEYS, 'fallback'];
3
+ const RECIPE_SCHEMA = 'explainer-kit.recipe/v2';
4
+ const ROOT_KEYS = ['floor', 'id', 'schemaVersion', 'sourceRoles', 'version'];
14
5
  const SOURCE_ROLE_KEYS = [
15
6
  'accepts',
16
7
  'maxBindings',
@@ -18,31 +9,7 @@ const SOURCE_ROLE_KEYS = [
18
9
  'required',
19
10
  'role',
20
11
  ];
21
- const FLOOR_KEYS = [
22
- 'authoring',
23
- 'briefRef',
24
- 'id',
25
- 'required',
26
- 'requiredNarrative',
27
- 'template',
28
- 'type',
29
- ];
30
- const EXPANSION_KEYS = ['limits', 'profiles'];
31
- const PROFILE_REQUIRED_KEYS = [
32
- 'authoring',
33
- 'briefRef',
34
- 'maxCount',
35
- 'profileId',
36
- 'type',
37
- ];
38
- const PROFILE_KEYS = [
39
- ...PROFILE_REQUIRED_KEYS,
40
- 'allowedJustificationKinds',
41
- 'shell',
42
- ];
43
- const EXPANSION_LIMIT_KEYS = ['maxArtifacts', 'maxPerType'];
44
- const DISCOVERY_LIMIT_KEYS = ['consecutiveNoNewFindingsRounds', 'maxRounds'];
45
- const FALLBACK_KEYS = ['authoring', 'mode', 'scope', 'selection'];
12
+ const FLOOR_KEYS = ['briefRef', 'id', 'requiredNarrative', 'template', 'type'];
46
13
  const SOURCE_KINDS = new Set([
47
14
  'file',
48
15
  'directory',
@@ -58,17 +25,15 @@ const ARTIFACT_TYPES = new Set([
58
25
  'deck',
59
26
  'catalog',
60
27
  ]);
61
- const AUTHORING_TYPES = new Set(['markdown', 'html']);
62
28
  const SAFE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
63
29
  const RECIPE_FILES = [
64
30
  'project-explainer.json',
65
- 'project-recap.v1.json',
66
31
  'project-recap.v2.json',
67
32
  'engineer-tour.json',
68
33
  'program-recap.json',
69
34
  ];
70
35
 
71
- const RECIPES = new Map(
36
+ export const RECIPES = new Map(
72
37
  RECIPE_FILES.map((file) => {
73
38
  const recipe = JSON.parse(
74
39
  readFileSync(new URL(`../../recipes/${file}`, import.meta.url), 'utf8'),
@@ -88,361 +53,27 @@ export function loadRecipe(id, version) {
88
53
  return structuredClone(recipe);
89
54
  }
90
55
 
91
- export function selectRecipeAuthoring(recipe, mode = 'artistic') {
92
- const selected = structuredClone(recipe);
93
- if (mode === 'artistic') {
94
- return selected;
95
- }
96
- if (selected.fallback?.mode !== mode) {
97
- const error = new Error(
98
- `Recipe ${selected.id}@${selected.version} does not support recap mode ${mode}`,
99
- );
100
- error.code = 'E_RECIPE_UNSUPPORTED';
101
- throw error;
102
- }
103
-
104
- selected.floor = selected.floor.map((artifact) => ({
105
- ...artifact,
106
- authoring: selected.fallback.authoring,
107
- }));
108
- selected.expansion.profiles = selected.expansion.profiles.map(
109
- ({ shell: _shell, ...profile }) => ({
110
- ...profile,
111
- authoring: selected.fallback.authoring,
112
- }),
113
- );
114
- return selected;
115
- }
116
-
117
- export function resolveDiagramRenderingRoute(recipe, artifact, diagrams) {
118
- if (
119
- !recipe ||
120
- typeof recipe !== 'object' ||
121
- !artifact ||
122
- typeof artifact !== 'object' ||
123
- !Array.isArray(diagrams)
124
- ) {
125
- throw new TypeError(
126
- 'Diagram routing requires a recipe, artifact, and diagram analyses.',
127
- );
128
- }
129
- const supported = diagrams.filter(({ valid }) => valid === true);
130
- if (
131
- supported.length === 0 ||
132
- supported.every(({ inlineSupported }) => inlineSupported === true)
133
- ) {
134
- return 'inline';
135
- }
136
- return artifact.authoring === 'html' ? 'artistic' : 'reject';
137
- }
138
-
139
56
  export function recipeFloor(recipe) {
140
- if (recipe?.schemaVersion === RECIPE_SCHEMA_V2) {
141
- return structuredClone(recipe.floor);
142
- }
143
- throw new TypeError('Recipe has an unsupported schemaVersion');
144
- }
145
-
146
- export function recipeExpansion(recipe) {
147
- if (recipe?.schemaVersion === RECIPE_SCHEMA_V2) {
148
- return structuredClone(recipe.expansion);
149
- }
150
- throw new TypeError('Recipe has an unsupported schemaVersion');
151
- }
152
-
153
- export function recipeRequiredNarrative(recipe, artifactId) {
154
- if (recipe?.schemaVersion === RECIPE_SCHEMA_V2) {
155
- const artifact = recipe.floor.find(({ id }) => id === artifactId);
156
- return [...(artifact?.requiredNarrative ?? [])];
157
- }
158
- throw new TypeError('Recipe has an unsupported schemaVersion');
159
- }
160
-
161
- export function evaluateExpansionProposals(recipe, proposals) {
162
- const floorIds = new Set(recipeFloor(recipe).map(({ id }) => id));
163
- const expansion = recipeExpansion(recipe);
164
- const profiles = new Map(
165
- expansion.profiles.map((profile) => [profile.profileId, profile]),
166
- );
167
- const errors = [];
168
- const accepted = [];
169
- const rejected = [];
170
- const warnings = new Set();
171
- const seenIds = new Set();
172
- const acceptedByProfile = new Map();
173
- const acceptedByType = new Map();
174
- const maxPerType = new Map(Object.entries(expansion.limits.maxPerType ?? {}));
175
-
176
- if (!Array.isArray(proposals)) {
177
- return {
178
- valid: false,
179
- accepted,
180
- rejected,
181
- warnings: [],
182
- errors: ['Expansion proposals must be an array'],
183
- };
184
- }
185
-
186
- for (const proposal of proposals) {
187
- if (
188
- !isObject(proposal) ||
189
- !hasExactKeys(proposal, ['id', 'profileId', 'rationale'])
190
- ) {
191
- errors.push(
192
- 'Expansion proposals require only id, profileId, and rationale',
193
- );
194
- continue;
195
- }
196
- if (!SAFE_ID.test(proposal.id)) {
197
- errors.push(`Unsafe expansion artifact id: ${proposal.id}`);
198
- continue;
199
- }
200
- if (floorIds.has(proposal.id)) {
201
- errors.push(`Expansion artifact id collides with floor: ${proposal.id}`);
202
- continue;
203
- }
204
- if (seenIds.has(proposal.id)) {
205
- errors.push(`Duplicate expansion artifact id: ${proposal.id}`);
206
- continue;
207
- }
208
- seenIds.add(proposal.id);
209
-
210
- const profile = profiles.get(proposal.profileId);
211
- if (!profile) {
212
- errors.push(`Unknown expansion profile: ${proposal.profileId}`);
213
- continue;
214
- }
215
- if (
216
- typeof proposal.rationale !== 'string' ||
217
- proposal.rationale.length === 0
218
- ) {
219
- errors.push(`Expansion proposal ${proposal.id} requires a rationale`);
220
- continue;
221
- }
222
-
223
- const profileCount = acceptedByProfile.get(profile.profileId) ?? 0;
224
- if (profileCount >= profile.maxCount) {
225
- rejected.push({
226
- ...structuredClone(proposal),
227
- status: 'rejected',
228
- reason: 'profile-limit',
229
- });
230
- warnings.add('expansion-profile-limit-exceeded');
231
- continue;
232
- }
233
- // A declared type cap binds across profiles, so proposals cannot spread
234
- // over sibling profiles that share one artifact type to evade it.
235
- const typeCount = acceptedByType.get(profile.type) ?? 0;
236
- if (
237
- maxPerType.has(profile.type) &&
238
- typeCount >= maxPerType.get(profile.type)
239
- ) {
240
- rejected.push({
241
- ...structuredClone(proposal),
242
- status: 'rejected',
243
- reason: 'type-limit',
244
- });
245
- warnings.add('expansion-type-limit-exceeded');
246
- continue;
247
- }
248
- if (accepted.length >= expansion.limits.maxArtifacts) {
249
- rejected.push({
250
- ...structuredClone(proposal),
251
- status: 'rejected',
252
- reason: 'recipe-limit',
253
- });
254
- warnings.add('expansion-artifact-limit-exceeded');
255
- continue;
256
- }
257
-
258
- accepted.push({
259
- ...structuredClone(proposal),
260
- status: 'accepted',
261
- profile: structuredClone(profile),
262
- });
263
- acceptedByProfile.set(profile.profileId, profileCount + 1);
264
- acceptedByType.set(profile.type, typeCount + 1);
265
- }
266
-
267
- return {
268
- valid: errors.length === 0,
269
- accepted,
270
- rejected,
271
- warnings: [...warnings],
272
- errors,
273
- };
274
- }
275
-
276
- export function validateSourceBindings(recipe, bindings) {
277
- const errors = [];
278
- if (!Array.isArray(bindings)) {
279
- return { valid: false, errors: ['Source bindings must be an array'] };
280
- }
281
-
282
- const roles = new Map(recipe.sourceRoles.map((role) => [role.role, role]));
283
- const sourceSets = new Map();
284
- for (const binding of bindings) {
285
- const role = roles.get(binding?.role);
286
- if (!role) {
287
- errors.push(`Unknown source role: ${binding?.role}`);
288
- continue;
289
- }
290
- const sets = sourceSets.get(role.role) ?? new Set();
291
- sets.add(binding.sourceSetId ?? Symbol());
292
- sourceSets.set(role.role, sets);
293
- if (!role.accepts.includes(binding.kind)) {
294
- errors.push(
295
- `Source role ${role.role} does not accept kind ${binding.kind}`,
296
- );
297
- }
298
- }
299
-
300
- for (const role of recipe.sourceRoles) {
301
- const count = sourceSets.get(role.role)?.size ?? 0;
302
- if (role.required && count < role.minBindings) {
303
- errors.push(`Missing required source role: ${role.role}`);
304
- }
305
- if (count > role.maxBindings) {
306
- errors.push(
307
- `Source role ${role.role} allows at most ${role.maxBindings} binding`,
308
- );
309
- }
310
- }
311
- return { valid: errors.length === 0, errors };
312
- }
313
-
314
- export function validateContentModel(recipe, contentModel) {
315
- const errors = [];
316
- if (!isObject(contentModel)) {
317
- return { valid: false, errors: ['Content model must be an object'] };
318
- }
319
-
320
- if (
321
- !recipeFloor(recipe).some(
322
- (artifact) => artifact.id === contentModel.artifactId,
323
- )
324
- ) {
325
- errors.push(`Unknown recipe artifact: ${contentModel.artifactId}`);
326
- }
327
- if (!Array.isArray(contentModel.sections)) {
328
- errors.push('Content model sections must be an array');
329
- return { valid: false, errors };
330
- }
331
-
332
- const sectionCounts = new Map();
333
- for (const section of contentModel.sections) {
334
- if (
335
- !isObject(section) ||
336
- typeof section.id !== 'string' ||
337
- typeof section.content !== 'string'
338
- ) {
339
- errors.push('Narrative sections require string id and content');
340
- continue;
341
- }
342
- sectionCounts.set(section.id, (sectionCounts.get(section.id) ?? 0) + 1);
343
- if (/<\s*script\b/i.test(section.content)) {
344
- errors.push(
345
- `Narrative section ${section.id} contains raw script content`,
346
- );
347
- }
348
- }
349
-
350
- for (const sectionId of recipeRequiredNarrative(
351
- recipe,
352
- contentModel.artifactId,
353
- )) {
354
- const count = sectionCounts.get(sectionId) ?? 0;
355
- if (count > 1) {
356
- errors.push(`Duplicate narrative section: ${sectionId}`);
357
- }
358
- }
359
- return { valid: errors.length === 0, errors };
360
- }
361
-
362
- export function validatePlannedPortfolio(recipe, portfolio) {
363
- const errors = [];
364
- if (!Array.isArray(portfolio)) {
365
- return { valid: false, errors: ['Planned portfolio must be an array'] };
366
- }
367
- const floorIds = new Set(recipeFloor(recipe).map(({ id }) => id));
368
- const expansion = recipeExpansion(recipe);
369
- const profiles = new Map(
370
- expansion.profiles.map((profile) => [profile.profileId, profile]),
57
+ assert(
58
+ recipe?.schemaVersion === RECIPE_SCHEMA,
59
+ 'Recipe has an unsupported schemaVersion',
371
60
  );
372
- const counts = new Map();
373
- let optionalCount = 0;
374
-
375
- for (const artifact of portfolio) {
376
- if (floorIds.has(artifact?.artifactId)) continue;
377
- optionalCount += 1;
378
- const profile = profiles.get(artifact?.profileId);
379
- if (!profile) {
380
- errors.push(`Unknown planned profile: ${artifact?.profileId}`);
381
- continue;
382
- }
383
- counts.set(profile.profileId, (counts.get(profile.profileId) ?? 0) + 1);
384
- if (!isObject(artifact.justification)) {
385
- errors.push(
386
- `Optional artifact ${artifact.artifactId} requires a source-backed justification`,
387
- );
388
- continue;
389
- }
390
- if (
391
- profile.allowedJustificationKinds !== undefined &&
392
- !profile.allowedJustificationKinds.includes(artifact.justification.kind)
393
- ) {
394
- errors.push(
395
- `Optional artifact ${artifact.artifactId} justification ${artifact.justification.kind} is not allowed for ${profile.profileId}`,
396
- );
397
- }
398
- }
399
- if (optionalCount > expansion.limits.maxArtifacts) {
400
- errors.push('Planned portfolio exceeds the recipe optional-artifact limit');
401
- }
402
- for (const [profileId, count] of counts) {
403
- if (count > profiles.get(profileId).maxCount) {
404
- errors.push(`Planned portfolio exceeds the ${profileId} profile limit`);
405
- }
406
- }
407
- return { valid: errors.length === 0, errors };
61
+ return structuredClone(recipe.floor);
408
62
  }
409
63
 
410
- export function shouldStopDiscovery(recipe, findingsByRound) {
411
- if (
412
- !Array.isArray(findingsByRound) ||
413
- findingsByRound.some((count) => !Number.isInteger(count) || count < 0)
414
- ) {
415
- throw new TypeError(
416
- 'Discovery round findings must be an array of non-negative integers',
417
- );
418
- }
419
-
420
- const { consecutiveNoNewFindingsRounds, maxRounds } = recipe.discoveryLimits;
421
- if (findingsByRound.length >= maxRounds) {
422
- return true;
423
- }
424
- if (findingsByRound.length < consecutiveNoNewFindingsRounds) {
425
- return false;
426
- }
427
- return findingsByRound
428
- .slice(-consecutiveNoNewFindingsRounds)
429
- .every((count) => count === 0);
64
+ export function recipeRequiredNarrative(recipe, artifactId) {
65
+ const artifact = recipeFloor(recipe).find(({ id }) => id === artifactId);
66
+ return [...(artifact?.requiredNarrative ?? [])];
430
67
  }
431
68
 
432
- export function validateRecipe(recipe, file = 'recipe') {
69
+ function validateRecipe(recipe, file) {
433
70
  assertObject(recipe, `${file} recipe`);
434
- if (recipe.schemaVersion === RECIPE_SCHEMA_V2) {
435
- assertAllowedKeys(
436
- recipe,
437
- RECIPE_V2_ROOT_KEYS,
438
- RECIPE_V2_REQUIRED_ROOT_KEYS,
439
- `${file} recipe`,
440
- );
441
- validateV2Shape(recipe, file);
442
- } else {
443
- assert(false, `${file} has unsupported schemaVersion`);
444
- }
445
- assertNonEmptyString(recipe.id, `${file} id`);
71
+ assertExactKeys(recipe, ROOT_KEYS, `${file} recipe`);
72
+ assert(
73
+ recipe.schemaVersion === RECIPE_SCHEMA,
74
+ `${file} has unsupported schemaVersion`,
75
+ );
76
+ assertSafeId(recipe.id, `${file} id`);
446
77
  assertNonEmptyString(recipe.version, `${file} version`);
447
78
 
448
79
  assert(
@@ -453,7 +84,7 @@ export function validateRecipe(recipe, file = 'recipe') {
453
84
  for (const role of recipe.sourceRoles) {
454
85
  assertObject(role, `${file} source role`);
455
86
  assertExactKeys(role, SOURCE_ROLE_KEYS, `${file} source role`);
456
- assertNonEmptyString(role.role, `${file} source role name`);
87
+ assertSafeId(role.role, `${file} source role name`);
457
88
  roleNames.push(role.role);
458
89
  assert(typeof role.required === 'boolean', `${file} role required`);
459
90
  assert(
@@ -469,34 +100,9 @@ export function validateRecipe(recipe, file = 'recipe') {
469
100
  role.maxBindings >= role.minBindings,
470
101
  `${file} role has invalid binding limits`,
471
102
  );
472
- assert(
473
- role.required === role.minBindings > 0,
474
- `${file} role required flag must match minBindings`,
475
- );
476
103
  }
477
104
  assertUnique(roleNames, `${file} source role names`);
478
105
 
479
- if ('fallback' in recipe) {
480
- assert(
481
- recipe.id === 'project-recap',
482
- `${file} fallback is allowed only for project-recap`,
483
- );
484
- assertObject(recipe.fallback, `${file} fallback`);
485
- assertExactKeys(recipe.fallback, FALLBACK_KEYS, `${file} fallback`);
486
- assert(
487
- recipe.fallback.mode === 'deterministic-markdown' &&
488
- recipe.fallback.selection === 'explicit' &&
489
- recipe.fallback.authoring === 'markdown' &&
490
- recipe.fallback.scope === 'portfolio',
491
- `${file} has an unsupported fallback policy`,
492
- );
493
- }
494
-
495
- validateDiscoveryLimits(recipe.discoveryLimits, file);
496
- return recipe;
497
- }
498
-
499
- function validateV2Shape(recipe, file) {
500
106
  assert(
501
107
  Array.isArray(recipe.floor) && recipe.floor.length > 0,
502
108
  `${file} floor must be a non-empty array`,
@@ -511,139 +117,31 @@ function validateV2Shape(recipe, file) {
511
117
  ARTIFACT_TYPES.has(artifact.type),
512
118
  `${file} floor has unsupported type`,
513
119
  );
514
- assert(
515
- AUTHORING_TYPES.has(artifact.authoring),
516
- `${file} floor has unsupported authoring`,
517
- );
518
120
  assertNonEmptyString(artifact.template, `${file} floor template`);
519
121
  assertNonEmptyString(artifact.briefRef, `${file} floor briefRef`);
520
- assert(typeof artifact.required === 'boolean', `${file} floor required`);
521
122
  assertUniqueNonEmptyStrings(
522
123
  artifact.requiredNarrative,
523
124
  `${file} floor requiredNarrative`,
524
125
  );
525
126
  }
526
127
  assertUnique(floorIds, `${file} floor ids`);
527
-
528
- assertObject(recipe.expansion, `${file} expansion`);
529
- assertExactKeys(recipe.expansion, EXPANSION_KEYS, `${file} expansion`);
530
- assert(
531
- Array.isArray(recipe.expansion.profiles),
532
- `${file} expansion profiles must be an array`,
533
- );
534
- const profileIds = [];
535
- for (const profile of recipe.expansion.profiles) {
536
- assertObject(profile, `${file} expansion profile`);
537
- assertAllowedKeys(
538
- profile,
539
- PROFILE_KEYS,
540
- PROFILE_REQUIRED_KEYS,
541
- `${file} expansion profile`,
542
- );
543
- assertSafeId(profile.profileId, `${file} expansion profileId`);
544
- profileIds.push(profile.profileId);
545
- assert(
546
- !floorIds.includes(profile.profileId),
547
- `${file} expansion profileId collides with floor id`,
548
- );
549
- assert(
550
- ARTIFACT_TYPES.has(profile.type),
551
- `${file} expansion profile has unsupported type`,
552
- );
553
- assert(
554
- AUTHORING_TYPES.has(profile.authoring),
555
- `${file} expansion profile has unsupported authoring`,
556
- );
557
- assertNonEmptyString(
558
- profile.briefRef,
559
- `${file} expansion profile briefRef`,
560
- );
561
- assertFiniteCount(profile.maxCount, `${file} expansion profile maxCount`);
562
- if ('allowedJustificationKinds' in profile) {
563
- assertUniqueNonEmptyStrings(
564
- profile.allowedJustificationKinds,
565
- `${file} expansion profile allowedJustificationKinds`,
566
- );
567
- }
568
- if (profile.authoring === 'html' || 'shell' in profile) {
569
- assertNonEmptyString(
570
- profile.shell,
571
- `${file} expansion profile ${profile.profileId} shell`,
572
- );
573
- }
574
- }
575
- assertUnique(profileIds, `${file} expansion profileIds`);
576
-
577
- assertObject(recipe.expansion.limits, `${file} expansion limits`);
578
- assertAllowedKeys(
579
- recipe.expansion.limits,
580
- EXPANSION_LIMIT_KEYS,
581
- ['maxArtifacts'],
582
- `${file} expansion limits`,
583
- );
584
- assertFiniteCount(
585
- recipe.expansion.limits.maxArtifacts,
586
- `${file} expansion maxArtifacts`,
587
- );
588
- if ('maxPerType' in recipe.expansion.limits) {
589
- assertObject(
590
- recipe.expansion.limits.maxPerType,
591
- `${file} expansion maxPerType`,
592
- );
593
- for (const [type, limit] of Object.entries(
594
- recipe.expansion.limits.maxPerType,
595
- )) {
596
- assert(
597
- ARTIFACT_TYPES.has(type),
598
- `${file} expansion maxPerType has unsupported type`,
599
- );
600
- assertFiniteCount(limit, `${file} expansion maxPerType ${type}`);
601
- }
602
- }
603
- }
604
-
605
- function validateDiscoveryLimits(discoveryLimits, file) {
606
- assertObject(discoveryLimits, `${file} discoveryLimits`);
607
- assertExactKeys(
608
- discoveryLimits,
609
- DISCOVERY_LIMIT_KEYS,
610
- `${file} discoveryLimits`,
611
- );
612
- assert(
613
- discoveryLimits.consecutiveNoNewFindingsRounds === 2,
614
- `${file} discovery must stop after exactly two no-new-findings rounds`,
615
- );
616
- assert(
617
- Number.isInteger(discoveryLimits.maxRounds) &&
618
- discoveryLimits.maxRounds >= 2,
619
- `${file} discovery maxRounds must be an integer of at least two`,
620
- );
128
+ return recipe;
621
129
  }
622
130
 
623
131
  function assertObject(value, label) {
624
- assert(isObject(value), `${label} must be an object`);
625
- }
626
-
627
- function assertExactKeys(value, expected, label) {
628
- const matches = hasExactKeys(value, expected);
629
- assert(matches, `${label} has unknown or missing keys`);
630
- }
631
-
632
- function assertAllowedKeys(value, allowed, required, label) {
633
- const actual = Object.keys(value);
634
132
  assert(
635
- actual.every((key) => allowed.includes(key)) &&
636
- required.every((key) => key in value),
637
- `${label} has unknown or missing keys`,
133
+ typeof value === 'object' && value !== null && !Array.isArray(value),
134
+ `${label} must be an object`,
638
135
  );
639
136
  }
640
137
 
641
- function hasExactKeys(value, expected) {
138
+ function assertExactKeys(value, expected, label) {
642
139
  const actual = Object.keys(value).sort();
643
140
  const sortedExpected = [...expected].sort();
644
- return (
141
+ assert(
645
142
  actual.length === sortedExpected.length &&
646
- actual.every((key, index) => key === sortedExpected[index])
143
+ actual.every((key, index) => key === sortedExpected[index]),
144
+ `${label} has unknown or missing keys`,
647
145
  );
648
146
  }
649
147
 
@@ -659,21 +157,12 @@ function assertSafeId(value, label) {
659
157
  assert(SAFE_ID.test(value), `${label} must be a safe slug`);
660
158
  }
661
159
 
662
- function assertFiniteCount(value, label) {
663
- assert(
664
- Number.isInteger(value) && Number.isFinite(value) && value >= 0,
665
- `${label} must be a finite non-negative integer`,
666
- );
667
- }
668
-
669
160
  function assertUniqueNonEmptyStrings(value, label) {
670
161
  assert(
671
162
  Array.isArray(value) && value.length > 0,
672
163
  `${label} must be a non-empty array`,
673
164
  );
674
- for (const entry of value) {
675
- assertNonEmptyString(entry, label);
676
- }
165
+ for (const entry of value) assertNonEmptyString(entry, label);
677
166
  assertUnique(value, label);
678
167
  }
679
168
 
@@ -682,11 +171,5 @@ function assertUnique(values, label) {
682
171
  }
683
172
 
684
173
  function assert(condition, message) {
685
- if (!condition) {
686
- throw new TypeError(message);
687
- }
688
- }
689
-
690
- function isObject(value) {
691
- return typeof value === 'object' && value !== null && !Array.isArray(value);
174
+ if (!condition) throw new TypeError(message);
692
175
  }