@open-agent-toolkit/cli 0.2.72 → 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 (165) hide show
  1. package/assets/agents/oat-reviewer.md +4 -3
  2. package/assets/agents/recon-worker.md +42 -8
  3. package/assets/bundle-metadata.json +1 -1
  4. package/assets/docs/cli-utilities/configuration.md +3 -56
  5. package/assets/docs/cli-utilities/tool-packs.md +4 -4
  6. package/assets/docs/contributing/code.md +2 -2
  7. package/assets/docs/contributing/index.md +0 -1
  8. package/assets/docs/reference/cli-reference.md +1 -1
  9. package/assets/docs/reference/troubleshooting.md +17 -40
  10. package/assets/docs/workflows/projects/artifacts.md +16 -34
  11. package/assets/docs/workflows/projects/lifecycle.md +20 -19
  12. package/assets/docs/workflows/skills/explainer-kit.md +180 -440
  13. package/assets/docs/workflows/skills/index.md +1 -2
  14. package/assets/docs/workflows/skills/recon.md +114 -23
  15. package/assets/public-package-versions.json +4 -4
  16. package/assets/skills/explainer-kit/SKILL.md +96 -171
  17. package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
  18. package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
  19. package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
  20. package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
  21. package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
  22. package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
  23. package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
  24. package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
  25. package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
  26. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
  27. package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
  28. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
  29. package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
  30. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
  31. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
  32. package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
  33. package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
  34. package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
  35. package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
  36. package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
  37. package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
  38. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
  39. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
  40. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
  41. package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
  42. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
  43. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
  44. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
  45. package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
  46. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
  47. package/assets/skills/oat-project-complete/SKILL.md +168 -349
  48. package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
  49. package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
  50. package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
  51. package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
  52. package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
  53. package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
  54. package/assets/skills/oat-project-document/SKILL.md +1 -1
  55. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
  56. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  57. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
  58. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
  59. package/assets/skills/oat-project-lite/SKILL.md +1 -1
  60. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
  61. package/assets/skills/oat-project-plan/SKILL.md +5 -6
  62. package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
  63. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
  64. package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
  65. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
  66. package/assets/skills/oat-project-review-receive/SKILL.md +14 -1
  67. package/assets/skills/oat-project-summary/SKILL.md +35 -25
  68. package/assets/skills/oat-wave-execute/SKILL.md +11 -36
  69. package/assets/skills/oat-wave-program/SKILL.md +13 -37
  70. package/assets/skills/recon/SKILL.md +103 -40
  71. package/assets/skills/recon/references/packet-contract.md +150 -67
  72. package/assets/skills/recon/references/profiles.md +38 -16
  73. package/assets/skills/recon/references/worker-contract.md +39 -8
  74. package/assets/skills/recon/scripts/lib/contracts.mjs +703 -55
  75. package/assets/skills/recon/scripts/lib/routing.mjs +588 -0
  76. package/assets/skills/recon/scripts/lib/validated-run.mjs +2 -0
  77. package/assets/skills/recon/scripts/prepare-routing.mjs +88 -0
  78. package/assets/skills/recon/scripts/render-packet.mjs +69 -1
  79. package/assets/skills/recon/scripts/validate-packet.mjs +473 -56
  80. package/assets/skills/subagent-orchestration/SKILL.md +18 -9
  81. package/assets/skills/subagent-orchestration/references/model-selection-principles.md +17 -8
  82. package/dist/commands/config/index.d.ts.map +1 -1
  83. package/dist/commands/config/index.js +9 -113
  84. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  85. package/dist/commands/project/archive/archive-utils.js +32 -184
  86. package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
  87. package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
  88. package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
  89. package/dist/config/oat-config.d.ts +0 -11
  90. package/dist/config/oat-config.d.ts.map +1 -1
  91. package/dist/config/oat-config.js +0 -34
  92. package/dist/config/resolve.d.ts.map +1 -1
  93. package/dist/config/resolve.js +0 -8
  94. package/dist/release/public-package-contract.d.ts.map +1 -1
  95. package/dist/release/public-package-contract.js +2 -10
  96. package/dist/validation/project-state.d.ts.map +1 -1
  97. package/dist/validation/project-state.js +42 -3
  98. package/package.json +2 -2
  99. package/assets/docs/contributing/explainer-kit-verification.md +0 -133
  100. package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
  101. package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
  102. package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
  103. package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
  104. package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
  105. package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
  106. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
  107. package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
  108. package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
  109. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
  110. package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
  111. package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
  112. package/assets/skills/explainer-kit/references/contracts.md +0 -324
  113. package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
  114. package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
  115. package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
  116. package/assets/skills/explainer-kit/references/visual-review.md +0 -71
  117. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
  118. package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
  119. package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
  120. package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
  121. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
  122. package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
  123. package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
  124. package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
  125. package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
  126. package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
  127. package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
  128. package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
  129. package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
  130. package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
  131. package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
  132. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
  133. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
  134. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
  135. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
  136. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
  137. package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
  138. package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
  139. package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
  140. package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
  141. package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
  142. package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
  143. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
  144. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
  145. package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
  146. package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
  147. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
  148. package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
  149. package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
  150. package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
  151. package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
  152. package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
  153. package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
  154. package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
  155. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
  156. package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
  157. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
  158. package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
  159. package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
  160. package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
  161. package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
  162. package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
  163. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
  164. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
  165. 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
  }