@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.
- 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/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-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/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
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import {
|
|
5
|
+
lstat,
|
|
6
|
+
mkdir,
|
|
7
|
+
mkdtemp,
|
|
8
|
+
readFile,
|
|
9
|
+
readdir,
|
|
10
|
+
realpath,
|
|
11
|
+
rename,
|
|
12
|
+
rm,
|
|
13
|
+
stat,
|
|
14
|
+
writeFile,
|
|
15
|
+
} from 'node:fs/promises';
|
|
16
|
+
import {
|
|
17
|
+
basename,
|
|
18
|
+
dirname,
|
|
19
|
+
extname,
|
|
20
|
+
isAbsolute,
|
|
21
|
+
join,
|
|
22
|
+
relative,
|
|
23
|
+
} from 'node:path';
|
|
24
|
+
import { pathToFileURL } from 'node:url';
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
harvestNumericTokens,
|
|
28
|
+
normalizeClaimSubject,
|
|
29
|
+
} from './lib/claim-subject.mjs';
|
|
30
|
+
import { validateContract } from './lib/contracts.mjs';
|
|
31
|
+
import { isFlowFailureStage } from './lib/failure.mjs';
|
|
32
|
+
import { loadRecipe, recipeRequiredNarrative } from './lib/recipes.mjs';
|
|
33
|
+
import { validateSatisfiedRunPackage } from './lib/run-package.mjs';
|
|
34
|
+
import { sanitizeDiagnostic } from './lib/sanitize.mjs';
|
|
35
|
+
|
|
36
|
+
const HASH_PREFIX = 'sha256:';
|
|
37
|
+
const DOCUMENT_EXTENSIONS = new Set(['.md', '.txt', '.html', '.json']);
|
|
38
|
+
const PROJECT_INPUTS = [
|
|
39
|
+
'summary.md',
|
|
40
|
+
'implementation.md',
|
|
41
|
+
'project-log.md',
|
|
42
|
+
'plan.md',
|
|
43
|
+
'discovery.md',
|
|
44
|
+
'spec.md',
|
|
45
|
+
'design.md',
|
|
46
|
+
'orchestration-log.md',
|
|
47
|
+
];
|
|
48
|
+
const PROJECT_EXPLAINER_INPUTS = [
|
|
49
|
+
'plan.md',
|
|
50
|
+
'design.md',
|
|
51
|
+
'spec.md',
|
|
52
|
+
'discovery.md',
|
|
53
|
+
];
|
|
54
|
+
const STATUS_VALUES = new Set([
|
|
55
|
+
'complete',
|
|
56
|
+
'in_progress',
|
|
57
|
+
'merged',
|
|
58
|
+
'deferred',
|
|
59
|
+
'parked',
|
|
60
|
+
'wont_do',
|
|
61
|
+
'closed',
|
|
62
|
+
'open',
|
|
63
|
+
'built',
|
|
64
|
+
'built-needs-review',
|
|
65
|
+
'failed',
|
|
66
|
+
'incomplete',
|
|
67
|
+
'skipped',
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
export async function collectInputs(recipe, inputs) {
|
|
71
|
+
const { files } = await collectInputsWithUnresolved(recipe, inputs);
|
|
72
|
+
return files;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function collectInputsWithUnresolved(recipe, inputs) {
|
|
76
|
+
const recipeId = typeof recipe === 'string' ? recipe : recipe?.id;
|
|
77
|
+
if (inputs.project) {
|
|
78
|
+
const names =
|
|
79
|
+
recipeId === 'project-explainer'
|
|
80
|
+
? PROJECT_EXPLAINER_INPUTS
|
|
81
|
+
: PROJECT_INPUTS;
|
|
82
|
+
return {
|
|
83
|
+
files: await collectAllowlistedFiles(inputs.project, names),
|
|
84
|
+
unresolvedClaims: [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (inputs.program) {
|
|
88
|
+
return collectProgramInputs(inputs);
|
|
89
|
+
}
|
|
90
|
+
if (inputs.documents?.length) {
|
|
91
|
+
const files = [];
|
|
92
|
+
for (const input of inputs.documents) {
|
|
93
|
+
files.push(...(await collectDocumentRoot(input)));
|
|
94
|
+
}
|
|
95
|
+
return { files: uniqueByLocator(files), unresolvedClaims: [] };
|
|
96
|
+
}
|
|
97
|
+
if (inputs.factBasePath) {
|
|
98
|
+
const root = dirname(inputs.factBasePath);
|
|
99
|
+
const file = await readConfinedFile(root, inputs.factBasePath);
|
|
100
|
+
return {
|
|
101
|
+
files: [{ ...file, suppliedFactBase: true }],
|
|
102
|
+
unresolvedClaims: [],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
throw bundleError('Exactly one input mode is required.');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function extractClaims(input) {
|
|
109
|
+
if (input.suppliedFactBase) {
|
|
110
|
+
return { claims: [], unresolvedClaims: [] };
|
|
111
|
+
}
|
|
112
|
+
if (input.unresolvedOnly) {
|
|
113
|
+
return { claims: [], unresolvedClaims: [input.unresolvedClaim] };
|
|
114
|
+
}
|
|
115
|
+
if (input.bytes.includes(0)) {
|
|
116
|
+
return {
|
|
117
|
+
claims: [],
|
|
118
|
+
unresolvedClaims: [
|
|
119
|
+
{
|
|
120
|
+
id: claimId(input.locator, 1, 'unparseable'),
|
|
121
|
+
text: `Could not parse ${input.locator}.`,
|
|
122
|
+
reason: 'missing-evidence',
|
|
123
|
+
citations: [{ sourceId: input.id, locator: `${input.locator}:1-1` }],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const claims = [];
|
|
130
|
+
let heading = basename(input.locator);
|
|
131
|
+
const lines = input.text.split(/\r?\n/);
|
|
132
|
+
for (const [index, raw] of lines.entries()) {
|
|
133
|
+
const text = raw.trim();
|
|
134
|
+
if (!text) continue;
|
|
135
|
+
const headingMatch = text.match(/^#{1,6}\s+(.+)$/);
|
|
136
|
+
if (headingMatch) {
|
|
137
|
+
heading = headingMatch[1].trim();
|
|
138
|
+
if (hasMachineCheckableValue(heading)) {
|
|
139
|
+
const line = index + 1;
|
|
140
|
+
claims.push({
|
|
141
|
+
id: claimId(input.locator, line, heading),
|
|
142
|
+
text: heading,
|
|
143
|
+
status: 'confirmed',
|
|
144
|
+
citations: [
|
|
145
|
+
{ sourceId: input.id, locator: `${input.locator}:${line}-${line}` },
|
|
146
|
+
],
|
|
147
|
+
_subject: heading,
|
|
148
|
+
_section: heading,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (/^(?:---|\|[\s:|-]+\|)$/.test(text)) continue;
|
|
154
|
+
const line = index + 1;
|
|
155
|
+
claims.push({
|
|
156
|
+
id: claimId(input.locator, line, text),
|
|
157
|
+
text,
|
|
158
|
+
status: 'confirmed',
|
|
159
|
+
citations: [
|
|
160
|
+
{ sourceId: input.id, locator: `${input.locator}:${line}-${line}` },
|
|
161
|
+
],
|
|
162
|
+
_subject: normalizeClaimSubject({ text, sectionId: heading }),
|
|
163
|
+
_section: heading,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return { claims, unresolvedClaims: [] };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function indexClaims(claims) {
|
|
170
|
+
const indexed = [];
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
for (const claim of claims) {
|
|
173
|
+
const subject = claim._subject || claim._section || claim.id;
|
|
174
|
+
for (const value of claim.text.match(/\b\d{4}-\d{2}-\d{2}\b/g) ?? []) {
|
|
175
|
+
addIndexed(indexed, seen, {
|
|
176
|
+
subject,
|
|
177
|
+
value,
|
|
178
|
+
kind: 'date',
|
|
179
|
+
claimId: claim.id,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const withoutDates = claim.text.replace(/\b\d{4}-\d{2}-\d{2}\b/g, '');
|
|
183
|
+
for (const value of harvestNumericTokens(withoutDates)) {
|
|
184
|
+
addIndexed(indexed, seen, {
|
|
185
|
+
subject,
|
|
186
|
+
value,
|
|
187
|
+
kind: 'number',
|
|
188
|
+
claimId: claim.id,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
for (const token of claim.text.toLowerCase().match(/[a-z][a-z_-]*/g) ??
|
|
192
|
+
[]) {
|
|
193
|
+
if (STATUS_VALUES.has(token)) {
|
|
194
|
+
addIndexed(indexed, seen, {
|
|
195
|
+
subject,
|
|
196
|
+
value: token,
|
|
197
|
+
kind: 'status',
|
|
198
|
+
claimId: claim.id,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return indexed;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function selectAnchorLedger(claims, recipe) {
|
|
207
|
+
const recipeId = typeof recipe === 'string' ? recipe : recipe.id;
|
|
208
|
+
const name =
|
|
209
|
+
claims
|
|
210
|
+
.map(({ _subject }) => _subject)
|
|
211
|
+
.find((subject) => subject && !/^(?:p\d+|w\d+)$/i.test(subject)) ??
|
|
212
|
+
recipeId;
|
|
213
|
+
const identifiers = [
|
|
214
|
+
...new Set(
|
|
215
|
+
claims.flatMap(
|
|
216
|
+
({ text }) =>
|
|
217
|
+
text.match(/\b(?:p\d{2}|w\d+|wave-\d+|BL-\d{6}-[a-z0-9-]+)\b/gi) ??
|
|
218
|
+
[],
|
|
219
|
+
),
|
|
220
|
+
),
|
|
221
|
+
];
|
|
222
|
+
const indexed = indexClaims(claims);
|
|
223
|
+
return {
|
|
224
|
+
terminology: [name, ...identifiers].slice(0, 12).map((term) => ({ term })),
|
|
225
|
+
numbers: indexed
|
|
226
|
+
.filter(({ kind }) => kind === 'number' || kind === 'date')
|
|
227
|
+
.slice(0, 12)
|
|
228
|
+
.map(({ subject, value }) => ({ subject, value })),
|
|
229
|
+
statuses: indexed
|
|
230
|
+
.filter(({ kind }) => kind === 'status')
|
|
231
|
+
.slice(0, 12)
|
|
232
|
+
.map(({ subject, value }) => ({ subject, value })),
|
|
233
|
+
claims: indexed,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function writeBundle(runRoot, factBase, ledger, theme) {
|
|
238
|
+
await mkdir(runRoot, { recursive: true });
|
|
239
|
+
const temporary = await mkdtemp(join(runRoot, '.bundle-'));
|
|
240
|
+
const source = join(temporary, 'source');
|
|
241
|
+
await mkdir(source, { recursive: true });
|
|
242
|
+
await Promise.all([
|
|
243
|
+
writeJson(join(source, 'fact-base.json'), factBase),
|
|
244
|
+
writeFile(join(source, 'fact-base.md'), factBaseMarkdown(factBase)),
|
|
245
|
+
writeJson(join(source, 'ledger.json'), ledger),
|
|
246
|
+
writeJson(join(temporary, 'theme.resolved.json'), theme),
|
|
247
|
+
]);
|
|
248
|
+
await rm(join(runRoot, 'source'), { recursive: true, force: true });
|
|
249
|
+
await rename(source, join(runRoot, 'source'));
|
|
250
|
+
await rename(
|
|
251
|
+
join(temporary, 'theme.resolved.json'),
|
|
252
|
+
join(runRoot, 'theme.resolved.json'),
|
|
253
|
+
);
|
|
254
|
+
await rm(temporary, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function findReusableRun(outputRoot, recipe, inputHashes) {
|
|
258
|
+
const candidates = [outputRoot];
|
|
259
|
+
try {
|
|
260
|
+
for (const entry of await readdir(outputRoot, { withFileTypes: true })) {
|
|
261
|
+
if (entry.isDirectory()) candidates.push(join(outputRoot, entry.name));
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (error.code !== 'ENOENT') throw error;
|
|
265
|
+
}
|
|
266
|
+
for (const runRoot of candidates) {
|
|
267
|
+
try {
|
|
268
|
+
await validateSatisfiedRunPackage(runRoot, recipe, { inputHashes });
|
|
269
|
+
return runRoot;
|
|
270
|
+
} catch {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function writeFailure(runRoot, stage, cause) {
|
|
278
|
+
if (!isFlowFailureStage(stage)) {
|
|
279
|
+
throw bundleError(`Unsupported failure stage: ${stage}`);
|
|
280
|
+
}
|
|
281
|
+
await mkdir(runRoot, { recursive: true });
|
|
282
|
+
await rm(join(runRoot, 'manifest.json'), { force: true });
|
|
283
|
+
await writeJson(join(runRoot, 'failure.json'), {
|
|
284
|
+
schemaVersion: 'explainer-kit.failure/v1',
|
|
285
|
+
runRootHash: hashBytes(await realpath(runRoot)),
|
|
286
|
+
stage,
|
|
287
|
+
cause: sanitizeDiagnostic(cause),
|
|
288
|
+
at: new Date().toISOString(),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export async function runBundle(argv, io = console) {
|
|
293
|
+
const options = parseArgs(argv);
|
|
294
|
+
if (!options.recipe || !options.theme || !options.out) {
|
|
295
|
+
throw usageError();
|
|
296
|
+
}
|
|
297
|
+
const recipeVersion = options.recipe === 'project-recap' ? '2' : '1';
|
|
298
|
+
const recipe = loadRecipe(options.recipe, recipeVersion);
|
|
299
|
+
await rm(join(options.out, 'failure.json'), { force: true });
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const { files: collected, unresolvedClaims: collectionUnresolvedClaims } =
|
|
303
|
+
await collectInputsWithUnresolved(recipe, {
|
|
304
|
+
project: options.project,
|
|
305
|
+
program: options.program,
|
|
306
|
+
summaries: options.summaries,
|
|
307
|
+
archive: options.archive,
|
|
308
|
+
documents: options.inputs,
|
|
309
|
+
factBasePath: options.factBase,
|
|
310
|
+
});
|
|
311
|
+
const inputHashes = Object.fromEntries(
|
|
312
|
+
collected.map(({ locator, hash }) => [locator, hash]),
|
|
313
|
+
);
|
|
314
|
+
const reusable = await findReusableRun(options.out, recipe, inputHashes);
|
|
315
|
+
if (reusable) {
|
|
316
|
+
const result = { reuse: true, runRoot: reusable, inputHashes };
|
|
317
|
+
io.log(JSON.stringify(result));
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
await rm(join(options.out, 'manifest.json'), { force: true });
|
|
321
|
+
|
|
322
|
+
const supplied = collected.find(({ suppliedFactBase }) => suppliedFactBase);
|
|
323
|
+
let factBase;
|
|
324
|
+
let ledger;
|
|
325
|
+
if (supplied) {
|
|
326
|
+
factBase = JSON.parse(supplied.text);
|
|
327
|
+
const validation = validateContract('fact-base', factBase);
|
|
328
|
+
if (!validation.valid) {
|
|
329
|
+
throw bundleError('Supplied fact base is invalid.');
|
|
330
|
+
}
|
|
331
|
+
const existingInput = factBase.sources.find(
|
|
332
|
+
({ locator, hash }) =>
|
|
333
|
+
locator === supplied.locator && hash === supplied.hash,
|
|
334
|
+
);
|
|
335
|
+
if (existingInput) {
|
|
336
|
+
existingInput.role = 'bundle-input';
|
|
337
|
+
} else {
|
|
338
|
+
factBase.sources.push({
|
|
339
|
+
id: `bundle-input-${createHash('sha256')
|
|
340
|
+
.update(`${supplied.locator}\0${supplied.hash}`)
|
|
341
|
+
.digest('hex')
|
|
342
|
+
.slice(0, 12)}`,
|
|
343
|
+
kind: 'file',
|
|
344
|
+
locator: supplied.locator,
|
|
345
|
+
hash: supplied.hash,
|
|
346
|
+
role: 'bundle-input',
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
const suppliedClaims = factBase.claims.map((claim) => {
|
|
350
|
+
const section =
|
|
351
|
+
claim.sections?.[0] ??
|
|
352
|
+
claim.citations?.[0]?.locator?.split(':')[0] ??
|
|
353
|
+
recipe.id;
|
|
354
|
+
return {
|
|
355
|
+
...claim,
|
|
356
|
+
_subject: normalizeClaimSubject({
|
|
357
|
+
text: claim.text,
|
|
358
|
+
sectionId: section,
|
|
359
|
+
}),
|
|
360
|
+
_section: section,
|
|
361
|
+
};
|
|
362
|
+
});
|
|
363
|
+
const updatedValidation = validateContract('fact-base', factBase);
|
|
364
|
+
if (!updatedValidation.valid) {
|
|
365
|
+
throw bundleError('Supplied fact base input identity is invalid.');
|
|
366
|
+
}
|
|
367
|
+
ledger = selectAnchorLedger(suppliedClaims, recipe);
|
|
368
|
+
} else {
|
|
369
|
+
const extracted = collected.map(extractClaims);
|
|
370
|
+
const internalClaims = extracted.flatMap(({ claims }) => claims);
|
|
371
|
+
factBase = {
|
|
372
|
+
schemaVersion: 'explainer-kit.fact-base/v1',
|
|
373
|
+
generatedAt: new Date().toISOString(),
|
|
374
|
+
mode: 'supplied',
|
|
375
|
+
freshnessPolicy: 'live-wins',
|
|
376
|
+
sources: collected.map(({ id, locator, hash }) => ({
|
|
377
|
+
id,
|
|
378
|
+
kind: 'file',
|
|
379
|
+
locator,
|
|
380
|
+
hash,
|
|
381
|
+
})),
|
|
382
|
+
claims: internalClaims.map(
|
|
383
|
+
({
|
|
384
|
+
_subject: _discardedSubject,
|
|
385
|
+
_section: _discardedSection,
|
|
386
|
+
...claim
|
|
387
|
+
}) => claim,
|
|
388
|
+
),
|
|
389
|
+
unresolvedClaims: [
|
|
390
|
+
...collectionUnresolvedClaims,
|
|
391
|
+
...extracted.flatMap(({ unresolvedClaims }) => unresolvedClaims),
|
|
392
|
+
],
|
|
393
|
+
overrides: [],
|
|
394
|
+
};
|
|
395
|
+
const validation = validateContract('fact-base', factBase);
|
|
396
|
+
if (!validation.valid) {
|
|
397
|
+
throw bundleError(
|
|
398
|
+
`Generated fact base is invalid: ${validation.errors
|
|
399
|
+
.map(({ code }) => code)
|
|
400
|
+
.join(', ')}`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
ledger = selectAnchorLedger(internalClaims, recipe);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const theme = JSON.parse(await readFile(options.theme, 'utf8'));
|
|
407
|
+
if (!validateContract('theme', theme).valid) {
|
|
408
|
+
throw bundleError('Resolved theme is invalid.');
|
|
409
|
+
}
|
|
410
|
+
await writeBundle(options.out, factBase, ledger, theme);
|
|
411
|
+
const result = {
|
|
412
|
+
reuse: false,
|
|
413
|
+
runRoot: options.out,
|
|
414
|
+
inputHashes,
|
|
415
|
+
requiredNarrative: recipeRequiredNarrative(recipe, recipe.floor[0].id),
|
|
416
|
+
};
|
|
417
|
+
io.log(JSON.stringify(result));
|
|
418
|
+
return result;
|
|
419
|
+
} catch (error) {
|
|
420
|
+
await writeFailure(options.out, 'bundle', error);
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function collectAllowlistedFiles(root, names) {
|
|
426
|
+
const files = [];
|
|
427
|
+
for (const name of names) {
|
|
428
|
+
const path = join(root, name);
|
|
429
|
+
try {
|
|
430
|
+
files.push(await readConfinedFile(root, path));
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (error.code !== 'ENOENT') throw error;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (files.length === 0)
|
|
436
|
+
throw bundleError('No allowlisted project inputs found.');
|
|
437
|
+
return files;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function collectProgramInputs({ program, summaries, archive }) {
|
|
441
|
+
const files = [await readConfinedFile(dirname(program), program)];
|
|
442
|
+
const unresolvedClaims = [];
|
|
443
|
+
if (summaries) {
|
|
444
|
+
const selected = new Map();
|
|
445
|
+
for (const entry of await readdir(summaries)) {
|
|
446
|
+
const match = entry.match(/^(\d{8})-(wave-\d+-execution)\.md$/);
|
|
447
|
+
if (!match) continue;
|
|
448
|
+
const prior = selected.get(match[2]);
|
|
449
|
+
if (!prior || match[1] > prior.date) {
|
|
450
|
+
selected.set(match[2], { date: match[1], entry });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
for (const { entry } of [...selected.values()].sort((a, b) =>
|
|
454
|
+
a.entry.localeCompare(b.entry),
|
|
455
|
+
)) {
|
|
456
|
+
files.push(await readConfinedFile(summaries, join(summaries, entry)));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (archive) {
|
|
460
|
+
try {
|
|
461
|
+
const selected = new Map();
|
|
462
|
+
for (const entry of await readdir(archive, { withFileTypes: true })) {
|
|
463
|
+
if (!entry.isDirectory()) continue;
|
|
464
|
+
const dated = entry.name.match(/^(\d{8})-(wave-\d+-execution)$/);
|
|
465
|
+
const direct = entry.name.match(/^(wave-\d+-execution)$/);
|
|
466
|
+
const wave = dated?.[2] ?? direct?.[1];
|
|
467
|
+
if (!wave) continue;
|
|
468
|
+
const date = dated?.[1] ?? '';
|
|
469
|
+
const prior = selected.get(wave);
|
|
470
|
+
if (!prior || date > prior.date) {
|
|
471
|
+
selected.set(wave, { date, entry: entry.name });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
for (const { entry } of [...selected.values()].sort((left, right) =>
|
|
475
|
+
left.entry.localeCompare(right.entry),
|
|
476
|
+
)) {
|
|
477
|
+
const path = join(archive, entry, 'implementation.md');
|
|
478
|
+
try {
|
|
479
|
+
const input = await readConfinedFile(archive, path);
|
|
480
|
+
const summary = finalSummaryInput(input);
|
|
481
|
+
if (summary) {
|
|
482
|
+
files.push(summary);
|
|
483
|
+
} else {
|
|
484
|
+
files.push({
|
|
485
|
+
...input,
|
|
486
|
+
unresolvedOnly: true,
|
|
487
|
+
unresolvedClaim: unreachableWrapperClaim(
|
|
488
|
+
input.locator,
|
|
489
|
+
'it has no Final Summary section',
|
|
490
|
+
input.id,
|
|
491
|
+
),
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
} catch (error) {
|
|
495
|
+
if (error.code !== 'ENOENT') throw error;
|
|
496
|
+
const locator = `${entry}/implementation.md`;
|
|
497
|
+
unresolvedClaims.push(
|
|
498
|
+
unreachableWrapperClaim(locator, 'implementation.md is missing'),
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
} catch (error) {
|
|
503
|
+
if (error.code !== 'ENOENT') throw error;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return { files: uniqueByLocator(files), unresolvedClaims };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function collectDocumentRoot(root) {
|
|
510
|
+
const rootStats = await lstat(root);
|
|
511
|
+
if (rootStats.isSymbolicLink()) {
|
|
512
|
+
await assertContained(dirname(root), root);
|
|
513
|
+
}
|
|
514
|
+
if (rootStats.isFile()) {
|
|
515
|
+
if (!DOCUMENT_EXTENSIONS.has(extname(root).toLowerCase())) return [];
|
|
516
|
+
return [await readConfinedFile(dirname(root), root)];
|
|
517
|
+
}
|
|
518
|
+
if (!rootStats.isDirectory()) return [];
|
|
519
|
+
const files = [];
|
|
520
|
+
async function visit(directory) {
|
|
521
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
522
|
+
const path = join(directory, entry.name);
|
|
523
|
+
const resolved = await assertContained(root, path);
|
|
524
|
+
const resolvedStats = await stat(resolved);
|
|
525
|
+
if (resolvedStats.isDirectory()) {
|
|
526
|
+
await visit(path);
|
|
527
|
+
} else if (
|
|
528
|
+
resolvedStats.isFile() &&
|
|
529
|
+
DOCUMENT_EXTENSIONS.has(extname(entry.name).toLowerCase())
|
|
530
|
+
) {
|
|
531
|
+
files.push(await readConfinedFile(root, path));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
await visit(root);
|
|
536
|
+
return files;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function readConfinedFile(root, path) {
|
|
540
|
+
const resolved = await assertContained(root, path);
|
|
541
|
+
const bytes = await readFile(resolved);
|
|
542
|
+
const locator = relative(await realpath(root), resolved).replaceAll(
|
|
543
|
+
'\\',
|
|
544
|
+
'/',
|
|
545
|
+
);
|
|
546
|
+
return {
|
|
547
|
+
id: `source-${createHash('sha256').update(locator).digest('hex').slice(0, 12)}`,
|
|
548
|
+
locator,
|
|
549
|
+
hash: hashBytes(bytes),
|
|
550
|
+
bytes,
|
|
551
|
+
text: bytes.toString('utf8'),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function assertContained(root, path) {
|
|
556
|
+
const canonicalRoot = await realpath(root);
|
|
557
|
+
const canonicalPath = await realpath(path);
|
|
558
|
+
const child = relative(canonicalRoot, canonicalPath);
|
|
559
|
+
if (
|
|
560
|
+
child === '..' ||
|
|
561
|
+
child.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) ||
|
|
562
|
+
isAbsolute(child)
|
|
563
|
+
) {
|
|
564
|
+
throw bundleError(`Input escapes its declared root: ${path}`);
|
|
565
|
+
}
|
|
566
|
+
return canonicalPath;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function addIndexed(indexed, seen, entry) {
|
|
570
|
+
const key = `${entry.subject}\0${entry.value}\0${entry.kind}`;
|
|
571
|
+
if (seen.has(key)) return;
|
|
572
|
+
seen.add(key);
|
|
573
|
+
indexed.push(entry);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function hasMachineCheckableValue(text) {
|
|
577
|
+
if (/\b\d+(?:\.\d+)?%?\b/.test(text)) return true;
|
|
578
|
+
return (
|
|
579
|
+
text
|
|
580
|
+
.toLowerCase()
|
|
581
|
+
.match(/[a-z][a-z_-]*/g)
|
|
582
|
+
?.some((token) => STATUS_VALUES.has(token)) ?? false
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function claimId(locator, line, text) {
|
|
587
|
+
return `claim-${createHash('sha256')
|
|
588
|
+
.update(`${locator}\0${line}\0${text}`)
|
|
589
|
+
.digest('hex')
|
|
590
|
+
.slice(0, 16)}`;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function factBaseMarkdown(factBase) {
|
|
594
|
+
return [
|
|
595
|
+
'# Fact base',
|
|
596
|
+
'',
|
|
597
|
+
...factBase.claims.map(({ text }) => `- ${text}`),
|
|
598
|
+
...(factBase.unresolvedClaims.length
|
|
599
|
+
? [
|
|
600
|
+
'',
|
|
601
|
+
'## Unresolved claims',
|
|
602
|
+
'',
|
|
603
|
+
...factBase.unresolvedClaims.map(({ text }) => `- ${text}`),
|
|
604
|
+
]
|
|
605
|
+
: []),
|
|
606
|
+
'',
|
|
607
|
+
].join('\n');
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function parseArgs(argv) {
|
|
611
|
+
const options = { inputs: [] };
|
|
612
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
613
|
+
const key = argv[index];
|
|
614
|
+
if (key === '--inputs') {
|
|
615
|
+
while (argv[index + 1] && !argv[index + 1].startsWith('--')) {
|
|
616
|
+
options.inputs.push(argv[(index += 1)]);
|
|
617
|
+
}
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const name = {
|
|
621
|
+
'--recipe': 'recipe',
|
|
622
|
+
'--project': 'project',
|
|
623
|
+
'--program': 'program',
|
|
624
|
+
'--summaries': 'summaries',
|
|
625
|
+
'--archive': 'archive',
|
|
626
|
+
'--fact-base': 'factBase',
|
|
627
|
+
'--theme': 'theme',
|
|
628
|
+
'--out': 'out',
|
|
629
|
+
}[key];
|
|
630
|
+
if (!name || !argv[index + 1]) throw usageError();
|
|
631
|
+
options[name] = argv[(index += 1)];
|
|
632
|
+
}
|
|
633
|
+
const modes = [
|
|
634
|
+
Boolean(options.project),
|
|
635
|
+
Boolean(options.program),
|
|
636
|
+
options.inputs.length > 0,
|
|
637
|
+
Boolean(options.factBase),
|
|
638
|
+
].filter(Boolean).length;
|
|
639
|
+
if (modes !== 1) throw usageError();
|
|
640
|
+
return options;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function hashBytes(bytes) {
|
|
644
|
+
return `${HASH_PREFIX}${createHash('sha256').update(bytes).digest('hex')}`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function finalSummaryInput(input) {
|
|
648
|
+
const lines = input.text.split(/\r?\n/);
|
|
649
|
+
const start = lines.findIndex((line) =>
|
|
650
|
+
/^#{1,6}\s+Final Summary(?:\s+\(for PR\/docs\))?\s*$/i.test(line.trim()),
|
|
651
|
+
);
|
|
652
|
+
if (start === -1) return null;
|
|
653
|
+
let end = lines.length;
|
|
654
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
655
|
+
if (/^#{1,2}\s+/.test(lines[index].trim())) {
|
|
656
|
+
end = index;
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const text = `${lines.slice(start, end).join('\n').trim()}\n`;
|
|
661
|
+
const bytes = Buffer.from(text);
|
|
662
|
+
return { ...input, bytes, text, hash: hashBytes(bytes) };
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function unreachableWrapperClaim(locator, detail, sourceId) {
|
|
666
|
+
return {
|
|
667
|
+
id: claimId(locator, 1, detail),
|
|
668
|
+
text: `Archived wrapper input ${locator} is unreachable because ${detail}.`,
|
|
669
|
+
reason: 'missing-evidence',
|
|
670
|
+
citations: sourceId ? [{ sourceId, locator: `${locator}:1-1` }] : [],
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async function writeJson(path, value) {
|
|
675
|
+
await mkdir(dirname(path), { recursive: true });
|
|
676
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function uniqueByLocator(files) {
|
|
680
|
+
const unique = new Map();
|
|
681
|
+
for (const file of files) {
|
|
682
|
+
const existing = unique.get(file.locator);
|
|
683
|
+
if (!existing) {
|
|
684
|
+
unique.set(file.locator, file);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (existing.id === file.id && existing.hash === file.hash) continue;
|
|
688
|
+
throw bundleError(
|
|
689
|
+
`Ambiguous document locator collision: ${file.locator} resolves to different input identities.`,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
return [...unique.values()];
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function usageError() {
|
|
696
|
+
return bundleError(
|
|
697
|
+
'Usage: bundle.mjs --recipe <id> (--project <dir> | --program <file> --summaries <dir> --archive <dir> | --inputs <path>... | --fact-base <path>) --theme <resolved-json> --out <run-root>',
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function bundleError(message) {
|
|
702
|
+
const error = new Error(message);
|
|
703
|
+
error.code = 'E_BUNDLE';
|
|
704
|
+
return error;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
708
|
+
runBundle(process.argv.slice(2)).catch((error) => {
|
|
709
|
+
console.error(error.message);
|
|
710
|
+
process.exitCode = 1;
|
|
711
|
+
});
|
|
712
|
+
}
|