@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
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
|
|
8
|
+
import { writeFailure } from './bundle.mjs';
|
|
9
|
+
import {
|
|
10
|
+
createBrowserProbeSession,
|
|
11
|
+
RUNTIME_UNAVAILABLE_REASONS,
|
|
12
|
+
} from './lib/browser-runtime.mjs';
|
|
13
|
+
import {
|
|
14
|
+
harvestNumericTokens,
|
|
15
|
+
normalizeClaimSubject,
|
|
16
|
+
} from './lib/claim-subject.mjs';
|
|
17
|
+
import { validateHtmlSafety } from './lib/html-safety.mjs';
|
|
18
|
+
import {
|
|
19
|
+
BROWSER_PROBE_EVALUATE,
|
|
20
|
+
checkArtifactCohesion,
|
|
21
|
+
checkHtmlStructure,
|
|
22
|
+
checkSourceDumping,
|
|
23
|
+
pngDimensions,
|
|
24
|
+
REPRESENTATIVE_WIDTHS,
|
|
25
|
+
runBrowserProbes,
|
|
26
|
+
} from './lib/qa.mjs';
|
|
27
|
+
import { loadRecipe, recipeRequiredNarrative } from './lib/recipes.mjs';
|
|
28
|
+
import { sanitizeDiagnostic } from './lib/sanitize.mjs';
|
|
29
|
+
|
|
30
|
+
const STATUS_VALUES = new Set([
|
|
31
|
+
'complete',
|
|
32
|
+
'in_progress',
|
|
33
|
+
'merged',
|
|
34
|
+
'deferred',
|
|
35
|
+
'parked',
|
|
36
|
+
'wont_do',
|
|
37
|
+
'closed',
|
|
38
|
+
'open',
|
|
39
|
+
'built',
|
|
40
|
+
'built-needs-review',
|
|
41
|
+
'failed',
|
|
42
|
+
'incomplete',
|
|
43
|
+
'skipped',
|
|
44
|
+
]);
|
|
45
|
+
const COVERED_CLAIM_BLOCKS = new Set([
|
|
46
|
+
'h1',
|
|
47
|
+
'h2',
|
|
48
|
+
'h3',
|
|
49
|
+
'h4',
|
|
50
|
+
'h5',
|
|
51
|
+
'h6',
|
|
52
|
+
'tr',
|
|
53
|
+
'li',
|
|
54
|
+
'p',
|
|
55
|
+
]);
|
|
56
|
+
const CLAIM_LABEL_ELEMENTS = new Set([
|
|
57
|
+
'h1',
|
|
58
|
+
'h2',
|
|
59
|
+
'h3',
|
|
60
|
+
'h4',
|
|
61
|
+
'h5',
|
|
62
|
+
'h6',
|
|
63
|
+
'dt',
|
|
64
|
+
'label',
|
|
65
|
+
'b',
|
|
66
|
+
'strong',
|
|
67
|
+
]);
|
|
68
|
+
const IGNORED_RESIDUAL_ELEMENTS = new Set(['script', 'style']);
|
|
69
|
+
const VOID_HTML_ELEMENTS = new Set([
|
|
70
|
+
'area',
|
|
71
|
+
'base',
|
|
72
|
+
'br',
|
|
73
|
+
'col',
|
|
74
|
+
'embed',
|
|
75
|
+
'hr',
|
|
76
|
+
'img',
|
|
77
|
+
'input',
|
|
78
|
+
'link',
|
|
79
|
+
'meta',
|
|
80
|
+
'param',
|
|
81
|
+
'source',
|
|
82
|
+
'track',
|
|
83
|
+
'wbr',
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
export function extractRenderedClaims(html) {
|
|
87
|
+
const terminology = {};
|
|
88
|
+
const numericClaims = {};
|
|
89
|
+
const statuses = {};
|
|
90
|
+
const claims = [];
|
|
91
|
+
const seenClaims = new Set();
|
|
92
|
+
const sections = extractSections(html);
|
|
93
|
+
|
|
94
|
+
function harvestValues(valuesText, subject) {
|
|
95
|
+
const addClaim = (value, kind) => {
|
|
96
|
+
const key = `${subject}\0${value}\0${kind}`;
|
|
97
|
+
if (seenClaims.has(key)) return;
|
|
98
|
+
seenClaims.add(key);
|
|
99
|
+
if (kind === 'status') {
|
|
100
|
+
statuses[subject] = value;
|
|
101
|
+
} else {
|
|
102
|
+
numericClaims[subject] = value;
|
|
103
|
+
}
|
|
104
|
+
claims.push({ subject, value, kind });
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
for (const value of valuesText.match(/\b\d{4}-\d{2}-\d{2}\b/g) ?? []) {
|
|
108
|
+
addClaim(value, 'date');
|
|
109
|
+
}
|
|
110
|
+
const withoutDates = valuesText.replace(/\b\d{4}-\d{2}-\d{2}\b/g, '');
|
|
111
|
+
for (const value of harvestNumericTokens(withoutDates)) {
|
|
112
|
+
addClaim(value, 'number');
|
|
113
|
+
}
|
|
114
|
+
for (const token of valuesText.toLowerCase().match(/[a-z][a-z_-]*/g) ??
|
|
115
|
+
[]) {
|
|
116
|
+
if (STATUS_VALUES.has(token)) addClaim(token, 'status');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const section of sections) {
|
|
121
|
+
const blockPattern = /<(h[1-6]|tr|li|p)\b[^>]*>([\s\S]*?)<\/\1>/gi;
|
|
122
|
+
for (const match of section.html.matchAll(blockPattern)) {
|
|
123
|
+
const [, tag, blockHtml] = match;
|
|
124
|
+
const text = htmlText(blockHtml);
|
|
125
|
+
if (!text) continue;
|
|
126
|
+
const isHeading = tag.toLowerCase().startsWith('h');
|
|
127
|
+
if (isHeading) {
|
|
128
|
+
terminology[text] = text;
|
|
129
|
+
}
|
|
130
|
+
const rowCells =
|
|
131
|
+
tag.toLowerCase() === 'tr'
|
|
132
|
+
? [...blockHtml.matchAll(/<t[hd]\b[^>]*>([\s\S]*?)<\/t[hd]>/gi)]
|
|
133
|
+
.map((cell) => htmlText(cell[1]))
|
|
134
|
+
.filter(Boolean)
|
|
135
|
+
: [];
|
|
136
|
+
const subject = normalizeClaimSubject({
|
|
137
|
+
text,
|
|
138
|
+
rowSubject: isHeading ? text : rowCells[0],
|
|
139
|
+
sectionId: section.id,
|
|
140
|
+
});
|
|
141
|
+
const valuesText =
|
|
142
|
+
rowCells.length > 1 ? rowCells.slice(1).join(' ') : text;
|
|
143
|
+
harvestValues(valuesText, subject);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const residual of extractResidualClaimSegments(
|
|
147
|
+
section.html,
|
|
148
|
+
section.id,
|
|
149
|
+
)) {
|
|
150
|
+
harvestValues(residual.text, residual.subject);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { terminology, numericClaims, statuses, claims };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function containsLedgerTerm(text, term) {
|
|
157
|
+
if (
|
|
158
|
+
typeof text !== 'string' ||
|
|
159
|
+
typeof term !== 'string' ||
|
|
160
|
+
term.length === 0
|
|
161
|
+
) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
165
|
+
return new RegExp(`(?<![A-Za-z0-9_-])${escaped}(?![A-Za-z0-9_-])`).test(text);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function extractResidualClaimSegments(html, sectionId) {
|
|
169
|
+
const segments = [];
|
|
170
|
+
const stack = [
|
|
171
|
+
{
|
|
172
|
+
tag: '#section',
|
|
173
|
+
subject: null,
|
|
174
|
+
labelText: null,
|
|
175
|
+
coveredContext: false,
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
let coveredDepth = 0;
|
|
179
|
+
let ignoredDepth = 0;
|
|
180
|
+
|
|
181
|
+
const nearestSubject = () =>
|
|
182
|
+
stack.findLast((frame) => frame.subject)?.subject ?? sectionId;
|
|
183
|
+
const addSegment = (text, subject = nearestSubject()) => {
|
|
184
|
+
const normalized = htmlText(text);
|
|
185
|
+
if (normalized) {
|
|
186
|
+
segments.push({ text: normalized, subject });
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const closeFrame = () => {
|
|
190
|
+
const frame = stack.pop();
|
|
191
|
+
if (!frame || frame.tag === '#section') return;
|
|
192
|
+
|
|
193
|
+
if (frame.labelText !== null) {
|
|
194
|
+
const labelText = htmlText(frame.labelText);
|
|
195
|
+
const subject = normalizeClaimSubject({
|
|
196
|
+
text: labelText,
|
|
197
|
+
sectionId: null,
|
|
198
|
+
});
|
|
199
|
+
if (subject) {
|
|
200
|
+
stack.at(-1).subject = subject;
|
|
201
|
+
if (!frame.coveredContext && ignoredDepth === 0) {
|
|
202
|
+
addSegment(frame.labelText, subject);
|
|
203
|
+
}
|
|
204
|
+
} else if (!frame.coveredContext && ignoredDepth === 0) {
|
|
205
|
+
addSegment(frame.labelText);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (frame.covered) coveredDepth -= 1;
|
|
209
|
+
if (frame.ignored) ignoredDepth -= 1;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
for (const match of String(html).matchAll(
|
|
213
|
+
/<!--[\s\S]*?-->|<![^>]*>|<\/?[a-z][^>]*>|[^<]+/gi,
|
|
214
|
+
)) {
|
|
215
|
+
const token = match[0];
|
|
216
|
+
const close = token.match(/^<\/([a-z][\w:-]*)\s*>/i);
|
|
217
|
+
if (close) {
|
|
218
|
+
while (stack.length > 1 && stack.at(-1).tag !== close[1].toLowerCase()) {
|
|
219
|
+
closeFrame();
|
|
220
|
+
}
|
|
221
|
+
if (stack.length > 1) closeFrame();
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const open = token.match(/^<([a-z][\w:-]*)\b/i);
|
|
226
|
+
if (open) {
|
|
227
|
+
const tag = open[1].toLowerCase();
|
|
228
|
+
const covered = COVERED_CLAIM_BLOCKS.has(tag);
|
|
229
|
+
const ignored = IGNORED_RESIDUAL_ELEMENTS.has(tag);
|
|
230
|
+
stack.push({
|
|
231
|
+
tag,
|
|
232
|
+
subject: null,
|
|
233
|
+
labelText: CLAIM_LABEL_ELEMENTS.has(tag) ? '' : null,
|
|
234
|
+
covered,
|
|
235
|
+
ignored,
|
|
236
|
+
coveredContext: coveredDepth > 0 || covered,
|
|
237
|
+
});
|
|
238
|
+
if (covered) coveredDepth += 1;
|
|
239
|
+
if (ignored) ignoredDepth += 1;
|
|
240
|
+
if (VOID_HTML_ELEMENTS.has(tag) || /\/\s*>$/.test(token)) closeFrame();
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (ignoredDepth > 0) continue;
|
|
245
|
+
const labels = stack.filter((frame) => frame.labelText !== null);
|
|
246
|
+
if (labels.length > 0) {
|
|
247
|
+
for (const label of labels) label.labelText += token;
|
|
248
|
+
} else if (coveredDepth === 0) {
|
|
249
|
+
addSegment(token);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
while (stack.length > 1) closeFrame();
|
|
254
|
+
return segments;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function verifyRun({
|
|
258
|
+
runRoot,
|
|
259
|
+
recipe,
|
|
260
|
+
rung = 'none',
|
|
261
|
+
screenshots,
|
|
262
|
+
artifactSha256,
|
|
263
|
+
visualVerdict,
|
|
264
|
+
visualNotes,
|
|
265
|
+
headlessRuntimeOptions,
|
|
266
|
+
}) {
|
|
267
|
+
const pagePath = join(runRoot, 'site/index.html');
|
|
268
|
+
let html;
|
|
269
|
+
try {
|
|
270
|
+
html = await readFile(pagePath, 'utf8');
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (error.code === 'ENOENT') {
|
|
273
|
+
await rm(join(runRoot, 'qa/result.json'), { force: true });
|
|
274
|
+
await writeFailure(runRoot, 'authoring', 'site/index.html is missing.');
|
|
275
|
+
throw verifyError(
|
|
276
|
+
'verify-authoring-missing',
|
|
277
|
+
'site/index.html is missing.',
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const recipeVersion = recipe === 'project-recap' ? '2' : '1';
|
|
285
|
+
const recipeContract = loadRecipe(recipe, recipeVersion);
|
|
286
|
+
const artifact = recipeContract.floor[0];
|
|
287
|
+
const [ledger, factBase, shell] = await Promise.all([
|
|
288
|
+
readJson(join(runRoot, 'source/ledger.json')),
|
|
289
|
+
readJson(join(runRoot, 'source/fact-base.json')),
|
|
290
|
+
readFile(
|
|
291
|
+
new URL(`../templates/${artifact.template}.html`, import.meta.url),
|
|
292
|
+
'utf8',
|
|
293
|
+
),
|
|
294
|
+
]);
|
|
295
|
+
const requiredSections = recipeRequiredNarrative(
|
|
296
|
+
recipeContract,
|
|
297
|
+
artifact.id,
|
|
298
|
+
);
|
|
299
|
+
const sections = extractSections(html);
|
|
300
|
+
const sectionById = new Map(
|
|
301
|
+
sections.map((section) => [section.id, section]),
|
|
302
|
+
);
|
|
303
|
+
const structure = checkHtmlStructure({
|
|
304
|
+
id: artifact.id,
|
|
305
|
+
type: artifact.type,
|
|
306
|
+
html,
|
|
307
|
+
});
|
|
308
|
+
const safety = validateHtmlSafety({
|
|
309
|
+
html,
|
|
310
|
+
shell,
|
|
311
|
+
shellName: artifact.template,
|
|
312
|
+
});
|
|
313
|
+
const sourceDumping = checkSourceDumping({
|
|
314
|
+
authoredText: htmlText(html),
|
|
315
|
+
authoredSections: sections.map(({ id, html: sectionHtml }) => ({
|
|
316
|
+
id,
|
|
317
|
+
text: htmlText(sectionHtml),
|
|
318
|
+
})),
|
|
319
|
+
sourceTexts: [
|
|
320
|
+
...factBase.claims.map(({ text }) => text),
|
|
321
|
+
...factBase.unresolvedClaims.map(({ text }) => text),
|
|
322
|
+
],
|
|
323
|
+
});
|
|
324
|
+
const renderedClaims = extractRenderedClaims(html);
|
|
325
|
+
const plainText = htmlText(html);
|
|
326
|
+
for (const { term } of ledger.terminology ?? []) {
|
|
327
|
+
if (containsLedgerTerm(plainText, term)) {
|
|
328
|
+
renderedClaims.terminology[term] = term;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const cohesion = checkArtifactCohesion(
|
|
332
|
+
[{ id: artifact.id, cohesion: renderedClaims }],
|
|
333
|
+
{ ledger },
|
|
334
|
+
);
|
|
335
|
+
const expectedClaims = new Set(
|
|
336
|
+
(ledger.claims ?? []).map(
|
|
337
|
+
({ subject, value, kind }) => `${subject}\0${value}\0${kind}`,
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
const untracedClaims = renderedClaims.claims.filter(
|
|
341
|
+
({ subject, value, kind }) =>
|
|
342
|
+
!expectedClaims.has(`${subject}\0${value}\0${kind}`),
|
|
343
|
+
);
|
|
344
|
+
const missingSections = requiredSections.filter((id) => {
|
|
345
|
+
const section = sectionById.get(id);
|
|
346
|
+
return !section || htmlText(section.html).length === 0;
|
|
347
|
+
});
|
|
348
|
+
const parseIssues = [
|
|
349
|
+
...structure.issues.filter(({ code }) => code === 'tag-balance'),
|
|
350
|
+
...safety.errors.filter((code) => code === 'malformed-html'),
|
|
351
|
+
];
|
|
352
|
+
const checks = {
|
|
353
|
+
parse: checkResult(parseIssues),
|
|
354
|
+
requiredNarrative: checkResult(
|
|
355
|
+
missingSections.map((id) => `required-section-missing-or-empty:${id}`),
|
|
356
|
+
),
|
|
357
|
+
structure: checkResult(
|
|
358
|
+
structure.issues.filter(({ code }) => code !== 'tag-balance'),
|
|
359
|
+
),
|
|
360
|
+
sourceDumping: checkResult(sourceDumping.issues),
|
|
361
|
+
shellScripts: checkResult(
|
|
362
|
+
safety.errors.filter((code) => code !== 'malformed-html'),
|
|
363
|
+
),
|
|
364
|
+
ledgerToPage: checkResult(cohesion.issues),
|
|
365
|
+
pageToLedger: checkResult(
|
|
366
|
+
untracedClaims.map(
|
|
367
|
+
({ subject, value }) => `verify-claim-untraced:${subject}:${value}`,
|
|
368
|
+
),
|
|
369
|
+
),
|
|
370
|
+
};
|
|
371
|
+
const pageHash = hashText(html);
|
|
372
|
+
const browserSafetyPassed = ['parse', 'structure', 'shellScripts'].every(
|
|
373
|
+
(check) => checks[check].status === 'pass',
|
|
374
|
+
);
|
|
375
|
+
const browserResult =
|
|
376
|
+
!browserSafetyPassed && rung !== 'none'
|
|
377
|
+
? await downgradeToNone(
|
|
378
|
+
runRoot,
|
|
379
|
+
'browser-blocked-by-static-safety-checks',
|
|
380
|
+
)
|
|
381
|
+
: rung === 'host'
|
|
382
|
+
? await verifyHostRung({
|
|
383
|
+
runRoot,
|
|
384
|
+
screenshots,
|
|
385
|
+
expectedArtifactHash: pageHash,
|
|
386
|
+
artifactSha256,
|
|
387
|
+
visualVerdict,
|
|
388
|
+
visualNotes,
|
|
389
|
+
})
|
|
390
|
+
: rung === 'playwright'
|
|
391
|
+
? await verifyPlaywrightRung({
|
|
392
|
+
runRoot,
|
|
393
|
+
artifact: { ...artifact, html },
|
|
394
|
+
headlessRuntimeOptions,
|
|
395
|
+
})
|
|
396
|
+
: await downgradeToNone(
|
|
397
|
+
runRoot,
|
|
398
|
+
RUNTIME_UNAVAILABLE_REASONS.disabled,
|
|
399
|
+
);
|
|
400
|
+
const result = {
|
|
401
|
+
artifactSha256: pageHash,
|
|
402
|
+
checks,
|
|
403
|
+
...browserResult,
|
|
404
|
+
};
|
|
405
|
+
await mkdir(join(runRoot, 'qa'), { recursive: true });
|
|
406
|
+
await rm(join(runRoot, 'failure.json'), { force: true });
|
|
407
|
+
await writeFile(
|
|
408
|
+
join(runRoot, 'qa/result.json'),
|
|
409
|
+
`${JSON.stringify(result, null, 2)}\n`,
|
|
410
|
+
);
|
|
411
|
+
return result;
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (error.code?.startsWith('verify-')) throw error;
|
|
414
|
+
await writeFailure(runRoot, 'verify', error);
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export async function runVerify(argv, io = console) {
|
|
420
|
+
const options = parseArgs(argv);
|
|
421
|
+
const result = await verifyRun(options);
|
|
422
|
+
io.log(JSON.stringify({ runRoot: options.runRoot, rung: result.rung }));
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function extractSections(html) {
|
|
427
|
+
const sections = [];
|
|
428
|
+
for (const match of html.matchAll(
|
|
429
|
+
/<section\b[^>]*\bid\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)<\/section>/gi,
|
|
430
|
+
)) {
|
|
431
|
+
sections.push({ id: match[1], html: match[2] });
|
|
432
|
+
}
|
|
433
|
+
return sections;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function htmlText(html) {
|
|
437
|
+
return String(html)
|
|
438
|
+
.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, ' ')
|
|
439
|
+
.replace(/<[^>]+>/g, ' ')
|
|
440
|
+
.replace(/&(?:nbsp|#160);/gi, ' ')
|
|
441
|
+
.replace(/&/gi, '&')
|
|
442
|
+
.replace(/</gi, '<')
|
|
443
|
+
.replace(/>/gi, '>')
|
|
444
|
+
.replace(/"/gi, '"')
|
|
445
|
+
.replace(/'|'/gi, "'")
|
|
446
|
+
.replace(/\s+/g, ' ')
|
|
447
|
+
.trim();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function checkResult(issues) {
|
|
451
|
+
return issues.length === 0
|
|
452
|
+
? { status: 'pass' }
|
|
453
|
+
: {
|
|
454
|
+
status: 'fail',
|
|
455
|
+
cause: issues
|
|
456
|
+
.map((issue) =>
|
|
457
|
+
typeof issue === 'string'
|
|
458
|
+
? issue
|
|
459
|
+
: `${issue.code}:${issue.message}`,
|
|
460
|
+
)
|
|
461
|
+
.join('; '),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function verifyHostRung({
|
|
466
|
+
runRoot,
|
|
467
|
+
screenshots,
|
|
468
|
+
expectedArtifactHash,
|
|
469
|
+
artifactSha256,
|
|
470
|
+
visualVerdict,
|
|
471
|
+
visualNotes,
|
|
472
|
+
}) {
|
|
473
|
+
if (!['pass', 'findings'].includes(visualVerdict)) {
|
|
474
|
+
throw verifyError(
|
|
475
|
+
'verify-host-visual-verdict-required',
|
|
476
|
+
'The host rung requires --visual-verdict pass|findings after screenshot inspection.',
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
if (artifactSha256 !== expectedArtifactHash) {
|
|
480
|
+
return downgradeToNone(runRoot, 'host-artifact-hash-mismatch');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const captures = await readHostScreenshots(screenshots);
|
|
484
|
+
if (!captures) {
|
|
485
|
+
return downgradeToNone(runRoot, 'host-screenshot-invalid');
|
|
486
|
+
}
|
|
487
|
+
await clearCanonicalScreenshots(runRoot);
|
|
488
|
+
await mkdir(join(runRoot, 'qa'), { recursive: true });
|
|
489
|
+
await Promise.all(
|
|
490
|
+
captures.map(({ path, bytes }) => writeFile(join(runRoot, path), bytes)),
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
const notes =
|
|
494
|
+
visualNotes?.trim() ||
|
|
495
|
+
`Host inspection reported ${visualVerdict} at 320, 768, and 1440 pixels.`;
|
|
496
|
+
return {
|
|
497
|
+
rung: 'host',
|
|
498
|
+
screenshots: captures.map(({ path }) => path),
|
|
499
|
+
visual: {
|
|
500
|
+
verdict: visualVerdict,
|
|
501
|
+
...(visualVerdict === 'findings' && { findings: [notes] }),
|
|
502
|
+
notes,
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function verifyPlaywrightRung({
|
|
508
|
+
runRoot,
|
|
509
|
+
artifact,
|
|
510
|
+
headlessRuntimeOptions,
|
|
511
|
+
}) {
|
|
512
|
+
let session;
|
|
513
|
+
try {
|
|
514
|
+
session = await createBrowserProbeSession(headlessRuntimeOptions);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
return downgradeToNone(
|
|
517
|
+
runRoot,
|
|
518
|
+
`playwright-launch-failed:${sanitizeDiagnostic(error)}`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
if (!session.available) return downgradeToNone(runRoot, session.reason);
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
await clearCanonicalScreenshots(runRoot);
|
|
525
|
+
const probes = await runBrowserProbes({
|
|
526
|
+
artifacts: [artifact],
|
|
527
|
+
probe: (request) =>
|
|
528
|
+
session.probe({
|
|
529
|
+
...request,
|
|
530
|
+
evaluate: BROWSER_PROBE_EVALUATE,
|
|
531
|
+
...(request.scenario === 'default' && {
|
|
532
|
+
screenshotPath: join(runRoot, `qa/${request.viewport.width}.png`),
|
|
533
|
+
}),
|
|
534
|
+
}),
|
|
535
|
+
});
|
|
536
|
+
const screenshots = REPRESENTATIVE_WIDTHS.map((width) => `qa/${width}.png`);
|
|
537
|
+
const findings = probes.issues.map(formatBrowserFinding);
|
|
538
|
+
return {
|
|
539
|
+
rung: 'playwright',
|
|
540
|
+
screenshots,
|
|
541
|
+
visual:
|
|
542
|
+
findings.length === 0
|
|
543
|
+
? {
|
|
544
|
+
verdict: 'pass',
|
|
545
|
+
notes: 'Playwright probes passed at all representative widths.',
|
|
546
|
+
}
|
|
547
|
+
: { verdict: 'findings', findings },
|
|
548
|
+
};
|
|
549
|
+
} catch (error) {
|
|
550
|
+
return downgradeToNone(
|
|
551
|
+
runRoot,
|
|
552
|
+
`playwright-probe-failed:${sanitizeDiagnostic(error)}`,
|
|
553
|
+
);
|
|
554
|
+
} finally {
|
|
555
|
+
await session.close();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function formatBrowserFinding({ code, width, scenario, message }) {
|
|
560
|
+
return [code, width && `${width}px`, scenario, message]
|
|
561
|
+
.filter(Boolean)
|
|
562
|
+
.join(':');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function readHostScreenshots(screenshots) {
|
|
566
|
+
if (typeof screenshots !== 'string' || screenshots.length === 0) return null;
|
|
567
|
+
try {
|
|
568
|
+
const captures = await Promise.all(
|
|
569
|
+
REPRESENTATIVE_WIDTHS.map(async (width) => {
|
|
570
|
+
const bytes = await readFile(join(screenshots, `${width}.png`));
|
|
571
|
+
const dimensions = pngDimensions(bytes);
|
|
572
|
+
if (!dimensions || dimensions.width !== width) {
|
|
573
|
+
throw new Error('invalid screenshot');
|
|
574
|
+
}
|
|
575
|
+
return { path: `qa/${width}.png`, bytes };
|
|
576
|
+
}),
|
|
577
|
+
);
|
|
578
|
+
return captures;
|
|
579
|
+
} catch {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function clearCanonicalScreenshots(runRoot) {
|
|
585
|
+
await Promise.all(
|
|
586
|
+
REPRESENTATIVE_WIDTHS.map((width) =>
|
|
587
|
+
rm(join(runRoot, `qa/${width}.png`), { force: true }),
|
|
588
|
+
),
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function downgradeToNone(runRoot, reason) {
|
|
593
|
+
await clearCanonicalScreenshots(runRoot);
|
|
594
|
+
return {
|
|
595
|
+
rung: 'none',
|
|
596
|
+
reason,
|
|
597
|
+
visual: { verdict: 'none' },
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function parseArgs(argv) {
|
|
602
|
+
const options = { rung: 'none' };
|
|
603
|
+
for (let index = 0; index < argv.length; index += 2) {
|
|
604
|
+
const field = {
|
|
605
|
+
'--run-root': 'runRoot',
|
|
606
|
+
'--recipe': 'recipe',
|
|
607
|
+
'--rung': 'rung',
|
|
608
|
+
'--screenshots': 'screenshots',
|
|
609
|
+
'--artifact-sha256': 'artifactSha256',
|
|
610
|
+
'--visual-verdict': 'visualVerdict',
|
|
611
|
+
'--visual-notes': 'visualNotes',
|
|
612
|
+
}[argv[index]];
|
|
613
|
+
if (!field || !argv[index + 1]) throw usageError();
|
|
614
|
+
options[field] = argv[index + 1];
|
|
615
|
+
}
|
|
616
|
+
if (!options.runRoot || !options.recipe) throw usageError();
|
|
617
|
+
if (!['none', 'host', 'playwright'].includes(options.rung))
|
|
618
|
+
throw usageError();
|
|
619
|
+
return options;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async function readJson(path) {
|
|
623
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function hashText(text) {
|
|
627
|
+
return `sha256:${createHash('sha256').update(text).digest('hex')}`;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function usageError() {
|
|
631
|
+
return verifyError(
|
|
632
|
+
'verify-usage',
|
|
633
|
+
'Usage: verify.mjs --run-root <dir> --recipe <id> [--rung none|playwright | --rung host --screenshots <dir> --artifact-sha256 <hash> --visual-verdict pass|findings [--visual-notes <text>]]',
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function verifyError(code, message) {
|
|
638
|
+
const error = new Error(message);
|
|
639
|
+
error.code = code;
|
|
640
|
+
return error;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
644
|
+
runVerify(process.argv.slice(2)).catch((error) => {
|
|
645
|
+
console.error(`${error.code ?? 'verify-error'}: ${error.message}`);
|
|
646
|
+
process.exitCode = 1;
|
|
647
|
+
});
|
|
648
|
+
}
|