@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.
- package/assets/agents/oat-reviewer.md +4 -3
- package/assets/agents/recon-worker.md +42 -8
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/configuration.md +3 -56
- package/assets/docs/cli-utilities/tool-packs.md +4 -4
- package/assets/docs/contributing/code.md +2 -2
- package/assets/docs/contributing/index.md +0 -1
- package/assets/docs/reference/cli-reference.md +1 -1
- package/assets/docs/reference/troubleshooting.md +17 -40
- package/assets/docs/workflows/projects/artifacts.md +16 -34
- package/assets/docs/workflows/projects/lifecycle.md +20 -19
- package/assets/docs/workflows/skills/explainer-kit.md +180 -440
- package/assets/docs/workflows/skills/index.md +1 -2
- package/assets/docs/workflows/skills/recon.md +114 -23
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +96 -171
- package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
- package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
- package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
- package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
- package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
- package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
- package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
- package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
- package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
- package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
- package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
- package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
- package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
- package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
- package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
- package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
- package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
- package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
- package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
- package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
- package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
- package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
- package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
- package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
- package/assets/skills/oat-project-complete/SKILL.md +168 -349
- package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
- package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
- package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
- package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
- package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
- package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
- package/assets/skills/oat-project-document/SKILL.md +1 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-lite/SKILL.md +1 -1
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-plan/SKILL.md +5 -6
- package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-review-receive/SKILL.md +14 -1
- package/assets/skills/oat-project-summary/SKILL.md +35 -25
- package/assets/skills/oat-wave-execute/SKILL.md +11 -36
- package/assets/skills/oat-wave-program/SKILL.md +13 -37
- package/assets/skills/recon/SKILL.md +103 -40
- package/assets/skills/recon/references/packet-contract.md +150 -67
- package/assets/skills/recon/references/profiles.md +38 -16
- package/assets/skills/recon/references/worker-contract.md +39 -8
- package/assets/skills/recon/scripts/lib/contracts.mjs +703 -55
- package/assets/skills/recon/scripts/lib/routing.mjs +588 -0
- package/assets/skills/recon/scripts/lib/validated-run.mjs +2 -0
- package/assets/skills/recon/scripts/prepare-routing.mjs +88 -0
- package/assets/skills/recon/scripts/render-packet.mjs +69 -1
- package/assets/skills/recon/scripts/validate-packet.mjs +473 -56
- package/assets/skills/subagent-orchestration/SKILL.md +18 -9
- package/assets/skills/subagent-orchestration/references/model-selection-principles.md +17 -8
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +9 -113
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +32 -184
- package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
- package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
- package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
- package/dist/config/oat-config.d.ts +0 -11
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +0 -34
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +0 -8
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +2 -10
- package/dist/validation/project-state.d.ts.map +1 -1
- package/dist/validation/project-state.js +42 -3
- package/package.json +2 -2
- package/assets/docs/contributing/explainer-kit-verification.md +0 -133
- package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
- package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
- package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
- package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
- package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
- package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
- package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
- package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
- package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
- package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
- package/assets/skills/explainer-kit/references/contracts.md +0 -324
- package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
- package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
- package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
- package/assets/skills/explainer-kit/references/visual-review.md +0 -71
- package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
- package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
- package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
- package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
- package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
- package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
- package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
- package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
- package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
- package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
- package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
- package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
- package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
- package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
- package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
- package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
- package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
- package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
- package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
- package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
- package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
- package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
- package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
- package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
- package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
- package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
- package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
- package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
- package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
- package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
- package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
- package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
- package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
- package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
- package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
- package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
- package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
- package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
- 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
|
|
4
|
-
const
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
69
|
+
function validateRecipe(recipe, file) {
|
|
433
70
|
assertObject(recipe, `${file} recipe`);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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
|
-
|
|
636
|
-
|
|
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
|
|
138
|
+
function assertExactKeys(value, expected, label) {
|
|
642
139
|
const actual = Object.keys(value).sort();
|
|
643
140
|
const sortedExpected = [...expected].sort();
|
|
644
|
-
|
|
141
|
+
assert(
|
|
645
142
|
actual.length === sortedExpected.length &&
|
|
646
|
-
|
|
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
|
}
|