@open-agent-toolkit/cli 0.2.72 → 0.2.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/assets/agents/oat-reviewer.md +4 -3
  2. package/assets/agents/recon-worker.md +42 -8
  3. package/assets/bundle-metadata.json +1 -1
  4. package/assets/docs/cli-utilities/configuration.md +3 -56
  5. package/assets/docs/cli-utilities/tool-packs.md +4 -4
  6. package/assets/docs/contributing/code.md +2 -2
  7. package/assets/docs/contributing/index.md +0 -1
  8. package/assets/docs/reference/cli-reference.md +1 -1
  9. package/assets/docs/reference/troubleshooting.md +17 -40
  10. package/assets/docs/workflows/projects/artifacts.md +16 -34
  11. package/assets/docs/workflows/projects/lifecycle.md +20 -19
  12. package/assets/docs/workflows/skills/explainer-kit.md +180 -440
  13. package/assets/docs/workflows/skills/index.md +1 -2
  14. package/assets/docs/workflows/skills/recon.md +114 -23
  15. package/assets/public-package-versions.json +4 -4
  16. package/assets/skills/explainer-kit/SKILL.md +96 -171
  17. package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
  18. package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
  19. package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
  20. package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
  21. package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
  22. package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
  23. package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
  24. package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
  25. package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
  26. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
  27. package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
  28. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
  29. package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
  30. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
  31. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
  32. package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
  33. package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
  34. package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
  35. package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
  36. package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
  37. package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
  38. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
  39. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
  40. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
  41. package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
  42. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
  43. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
  44. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
  45. package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
  46. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
  47. package/assets/skills/oat-project-complete/SKILL.md +168 -349
  48. package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
  49. package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
  50. package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
  51. package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
  52. package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
  53. package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
  54. package/assets/skills/oat-project-document/SKILL.md +1 -1
  55. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
  56. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  57. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
  58. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
  59. package/assets/skills/oat-project-lite/SKILL.md +1 -1
  60. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
  61. package/assets/skills/oat-project-plan/SKILL.md +5 -6
  62. package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
  63. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
  64. package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
  65. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
  66. package/assets/skills/oat-project-review-receive/SKILL.md +14 -1
  67. package/assets/skills/oat-project-summary/SKILL.md +35 -25
  68. package/assets/skills/oat-wave-execute/SKILL.md +11 -36
  69. package/assets/skills/oat-wave-program/SKILL.md +13 -37
  70. package/assets/skills/recon/SKILL.md +103 -40
  71. package/assets/skills/recon/references/packet-contract.md +150 -67
  72. package/assets/skills/recon/references/profiles.md +38 -16
  73. package/assets/skills/recon/references/worker-contract.md +39 -8
  74. package/assets/skills/recon/scripts/lib/contracts.mjs +703 -55
  75. package/assets/skills/recon/scripts/lib/routing.mjs +588 -0
  76. package/assets/skills/recon/scripts/lib/validated-run.mjs +2 -0
  77. package/assets/skills/recon/scripts/prepare-routing.mjs +88 -0
  78. package/assets/skills/recon/scripts/render-packet.mjs +69 -1
  79. package/assets/skills/recon/scripts/validate-packet.mjs +473 -56
  80. package/assets/skills/subagent-orchestration/SKILL.md +18 -9
  81. package/assets/skills/subagent-orchestration/references/model-selection-principles.md +17 -8
  82. package/dist/commands/config/index.d.ts.map +1 -1
  83. package/dist/commands/config/index.js +9 -113
  84. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  85. package/dist/commands/project/archive/archive-utils.js +32 -184
  86. package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
  87. package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
  88. package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
  89. package/dist/config/oat-config.d.ts +0 -11
  90. package/dist/config/oat-config.d.ts.map +1 -1
  91. package/dist/config/oat-config.js +0 -34
  92. package/dist/config/resolve.d.ts.map +1 -1
  93. package/dist/config/resolve.js +0 -8
  94. package/dist/release/public-package-contract.d.ts.map +1 -1
  95. package/dist/release/public-package-contract.js +2 -10
  96. package/dist/validation/project-state.d.ts.map +1 -1
  97. package/dist/validation/project-state.js +42 -3
  98. package/package.json +2 -2
  99. package/assets/docs/contributing/explainer-kit-verification.md +0 -133
  100. package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
  101. package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
  102. package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
  103. package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
  104. package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
  105. package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
  106. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
  107. package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
  108. package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
  109. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
  110. package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
  111. package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
  112. package/assets/skills/explainer-kit/references/contracts.md +0 -324
  113. package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
  114. package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
  115. package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
  116. package/assets/skills/explainer-kit/references/visual-review.md +0 -71
  117. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
  118. package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
  119. package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
  120. package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
  121. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
  122. package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
  123. package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
  124. package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
  125. package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
  126. package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
  127. package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
  128. package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
  129. package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
  130. package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
  131. package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
  132. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
  133. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
  134. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
  135. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
  136. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
  137. package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
  138. package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
  139. package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
  140. package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
  141. package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
  142. package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
  143. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
  144. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
  145. package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
  146. package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
  147. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
  148. package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
  149. package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
  150. package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
  151. package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
  152. package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
  153. package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
  154. package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
  155. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
  156. package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
  157. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
  158. package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
  159. package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
  160. package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
  161. package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
  162. package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
  163. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
  164. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
  165. package/dist/commands/project/archive/explainer-terminal-evidence.js +0 -37
@@ -1,12 +1,5 @@
1
- import { readFile, stat } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
-
4
- import { assertBrowserProbeSession } from './browser-runtime.mjs';
5
- import { writeJsonAtomic } from './fs-safe.mjs';
6
1
  import { findUnpinnedResourceRefs } from './html-safety.mjs';
7
2
  import { decodeBrowserPng } from './png.mjs';
8
- import { recipeFloor, recipeRequiredNarrative } from './recipes.mjs';
9
- import { cohesionEvidenceFromLedger } from './visual-review.mjs';
10
3
 
11
4
  const VOID_ELEMENTS = new Set([
12
5
  'area',
@@ -28,145 +21,8 @@ const INLINE_ASSET_VIOLATION_PATTERN =
28
21
  /<link\b|@import\b|url\(\s*["']?(?!data:|#)/i;
29
22
  const TOKEN_PATTERN = /{{\s*[A-Z][A-Z0-9_]*\s*}}/g;
30
23
  const ARROW_KEYS = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
31
- const MAX_SCREENSHOT_BYTES = 20 * 1024 * 1024;
32
- const INLINE_DIAGRAM_PATTERN =
33
- /<svg\b(?=[^>]*(?:\bclass\s*=\s*["'][^"']*\bdiagram\b|\baria-label\s*=\s*["'][^"']*(?:architecture|diagram)))[^>]*>/i;
34
- const STRUCTURED_BLOCK_PATTERNS = [
35
- /<table\b/i,
36
- /<(?:ul|ol)\b/i,
37
- /<aside\b[^>]*\bclass\s*=\s*["'][^"']*\bcallout\b/i,
38
- /<blockquote\b/i,
39
- /<figure\b/i,
40
- ];
41
24
 
42
25
  export const REPRESENTATIVE_WIDTHS = Object.freeze([320, 768, 1440]);
43
- export const GUIDELINE_WARNING_IDS = Object.freeze({
44
- narrativeCoverage: 'guideline-narrative-coverage-missing',
45
- architectureDiagram: 'guideline-architecture-diagram-missing',
46
- structuredDepth: 'guideline-structured-depth-missing',
47
- expansionProfileLimit: 'expansion-profile-limit-exceeded',
48
- expansionArtifactLimit: 'expansion-artifact-limit-exceeded',
49
- expansionTypeLimit: 'expansion-type-limit-exceeded',
50
- });
51
- const EXPANSION_WARNING_BY_REJECTION_REASON = new Map([
52
- ['profile-limit', GUIDELINE_WARNING_IDS.expansionProfileLimit],
53
- ['recipe-limit', GUIDELINE_WARNING_IDS.expansionArtifactLimit],
54
- ['type-limit', GUIDELINE_WARNING_IDS.expansionTypeLimit],
55
- ]);
56
- export const RENDER_WARNING_IDS = Object.freeze({
57
- unsupportedDiagram: 'render-unsupported-diagram',
58
- headingDepthJump: 'render-heading-depth-jump',
59
- timelineEntryShape: 'render-timeline-entry-shape',
60
- legacyRawHtmlEscaped: 'render-legacy-raw-html-escaped',
61
- });
62
- export const RENDER_QA_WARNING_IDS = Object.freeze({
63
- documentOverflow: 'render-qa-document-overflow',
64
- innerContainerOverflow: 'render-qa-inner-container-overflow',
65
- viewportClipping: 'render-qa-viewport-clipping',
66
- headingReadability: 'render-qa-heading-unreadable',
67
- animationsEnabled: 'render-qa-animations-enabled',
68
- reducedMotion: 'render-qa-reduced-motion',
69
- keyboardNavigation: 'render-qa-keyboard-navigation',
70
- themeToggle: 'render-qa-theme-toggle',
71
- deckNoJsLayout: 'render-qa-deck-no-js-layout',
72
- deckPrintLayout: 'render-qa-deck-print-layout',
73
- skippedNoProbe: 'render-qa-skipped-no-probe',
74
- });
75
- const RENDER_QA_WARNING_BY_CODE = new Map([
76
- ['viewport-overflow', RENDER_QA_WARNING_IDS.documentOverflow],
77
- ['inner-x-overflow', RENDER_QA_WARNING_IDS.innerContainerOverflow],
78
- ['viewport-clipping', RENDER_QA_WARNING_IDS.viewportClipping],
79
- ['heading-readability', RENDER_QA_WARNING_IDS.headingReadability],
80
- ['animations-enabled', RENDER_QA_WARNING_IDS.animationsEnabled],
81
- ['reduced-motion', RENDER_QA_WARNING_IDS.reducedMotion],
82
- ['keyboard-navigation', RENDER_QA_WARNING_IDS.keyboardNavigation],
83
- ['theme-toggle', RENDER_QA_WARNING_IDS.themeToggle],
84
- ['deck-no-js-layout', RENDER_QA_WARNING_IDS.deckNoJsLayout],
85
- ['deck-print-layout', RENDER_QA_WARNING_IDS.deckPrintLayout],
86
- ]);
87
-
88
- export function renderQaWarningIds(issues) {
89
- if (!Array.isArray(issues)) {
90
- throw new TypeError('Render QA issues must be an array.');
91
- }
92
- return [
93
- ...new Set(
94
- issues
95
- .map(({ code }) => RENDER_QA_WARNING_BY_CODE.get(code))
96
- .filter(Boolean),
97
- ),
98
- ];
99
- }
100
-
101
- // Degradation findings are prefixed rather than looked up so a newly added
102
- // render warning code surfaces instead of being silently dropped.
103
- export function renderWarningIds(warnings) {
104
- if (!Array.isArray(warnings)) {
105
- throw new TypeError('Render warnings must be an array.');
106
- }
107
- if (warnings.some(({ code } = {}) => typeof code !== 'string' || !code)) {
108
- throw new TypeError('Every render warning requires a string code.');
109
- }
110
- return [...new Set(warnings.map(({ code }) => `render-${code}`))];
111
- }
112
-
113
- export function checkGuidelines({ recipe, artifacts, expansion } = {}) {
114
- if (!Array.isArray(artifacts)) {
115
- throw new TypeError('Guideline checker artifacts must be an array.');
116
- }
117
- if (
118
- artifacts.some(
119
- (artifact) =>
120
- !isPlainObject(artifact) ||
121
- typeof artifactId(artifact) !== 'string' ||
122
- typeof artifact.type !== 'string' ||
123
- typeof artifact.html !== 'string',
124
- )
125
- ) {
126
- throw new TypeError(
127
- 'Guideline checker artifacts require id, type, and HTML.',
128
- );
129
- }
130
-
131
- const floor = recipeFloor(recipe);
132
- const builtById = new Map(
133
- artifacts.map((artifact) => [artifactId(artifact), artifact]),
134
- );
135
- const narrativeFloor = floor.filter(
136
- (artifact) => recipeRequiredNarrative(recipe, artifact.id).length > 0,
137
- );
138
- const warnings = new Set();
139
-
140
- const missesNarrative = narrativeFloor.some((floorArtifact) => {
141
- const built = builtById.get(floorArtifact.id);
142
- const required = recipeRequiredNarrative(recipe, floorArtifact.id);
143
- return (
144
- !built ||
145
- required.some((sectionId) => !hasElementId(built.html, sectionId))
146
- );
147
- });
148
- if (missesNarrative) {
149
- warnings.add(GUIDELINE_WARNING_IDS.narrativeCoverage);
150
- }
151
-
152
- const hasDiagram =
153
- artifacts.some(({ type }) => type === 'diagram') ||
154
- artifacts.some(({ html }) => INLINE_DIAGRAM_PATTERN.test(html));
155
- if (!hasDiagram) {
156
- warnings.add(GUIDELINE_WARNING_IDS.architectureDiagram);
157
- }
158
-
159
- const hasStructuredDepth = narrativeFloor.some((floorArtifact) => {
160
- const html = builtById.get(floorArtifact.id)?.html ?? '';
161
- return STRUCTURED_BLOCK_PATTERNS.some((pattern) => pattern.test(html));
162
- });
163
- if (narrativeFloor.length > 0 && !hasStructuredDepth) {
164
- warnings.add(GUIDELINE_WARNING_IDS.structuredDepth);
165
- }
166
-
167
- addExpansionWarnings(warnings, expansion);
168
- return { valid: true, warnings: [...warnings] };
169
- }
170
26
 
171
27
  export function checkSourceDumping({
172
28
  authoredText,
@@ -243,10 +99,6 @@ export const BROWSER_PROBE_EVALUATE = `(() => {
243
99
  scrollWidth: element.scrollWidth
244
100
  }))
245
101
  .slice(0, 20);
246
- // Ancestry inside a horizontal scroller is not reachability: scrollLeft only
247
- // ranges over 0..scrollWidth-clientWidth, so content sitting at a negative
248
- // content offset or past the scrollable extent can never be scrolled into
249
- // view and stays a genuine clipping defect.
250
102
  const TOLERANCE = 2;
251
103
  const scrollReachable = (element) => {
252
104
  const rect = element.getBoundingClientRect();
@@ -279,9 +131,6 @@ export const BROWSER_PROBE_EVALUATE = `(() => {
279
131
  };
280
132
  })
281
133
  .slice(0, 20);
282
- // A heading in a collapsed panel or an aria-hidden subtree is deliberately
283
- // not presented, so it is out of scope rather than unreadable. Visually
284
- // hidden accessibility text still renders a box and stays in scope.
285
134
  const presented = (element) => {
286
135
  const rendered = typeof element.checkVisibility === 'function'
287
136
  ? element.checkVisibility({
@@ -306,13 +155,6 @@ export const BROWSER_PROBE_EVALUATE = `(() => {
306
155
  fontSize: Number.parseFloat(getComputedStyle(heading).fontSize)
307
156
  }))
308
157
  .slice(0, 20);
309
- // Reduced-motion styling conventionally collapses transitions to a token
310
- // 0.01ms rather than 0s so transitionend still fires. That is suppressed
311
- // motion, not active motion, so anything under a millisecond counts as
312
- // disabled while a perceptible duration still reports.
313
- // Motion is just as visible on a generated ::before/::after box as on the
314
- // element itself, so all three are inspected. A pseudo-element that
315
- // generates no content cannot animate and is skipped.
316
158
  const PERCEPTIBLE_SECONDS = 0.001;
317
159
  const motionless = (element, pseudo) => {
318
160
  const style = getComputedStyle(element, pseudo);
@@ -372,13 +214,9 @@ export function checkHtmlStructure({
372
214
 
373
215
  for (const denied of denylist.filter(Boolean)) {
374
216
  if (html.toLocaleLowerCase().includes(denied.toLocaleLowerCase())) {
375
- add(
376
- 'denylisted-string',
377
- 'Artifact contains a configured denylisted string.',
378
- {
379
- value: denied,
380
- },
381
- );
217
+ add('denylisted-string', 'Artifact contains a denylisted string.', {
218
+ value: denied,
219
+ });
382
220
  }
383
221
  }
384
222
 
@@ -394,7 +232,6 @@ export function checkHtmlStructure({
394
232
  for (const imbalance of findTagImbalances(html)) {
395
233
  add('tag-balance', imbalance);
396
234
  }
397
-
398
235
  checkHeadings(html, add);
399
236
  checkLinks(html, add);
400
237
 
@@ -419,7 +256,7 @@ export function checkHtmlStructure({
419
256
  if (missing.length > 0) {
420
257
  add(
421
258
  'deck-keyboard',
422
- 'Deck keyboard navigation must support both horizontal and vertical arrow pairs.',
259
+ 'Deck keyboard navigation must support both arrow pairs.',
423
260
  { missing },
424
261
  );
425
262
  }
@@ -436,23 +273,25 @@ export function checkArtifactCohesion(artifacts, { ledger = null } = {}) {
436
273
  const groups = ['terminology', 'numericClaims', 'statuses'];
437
274
  const expected = ledger
438
275
  ? {
439
- terminology: new Map(
440
- (ledger.terminology ?? []).map(({ term }) => [term, term]),
441
- ),
442
- numericClaims: new Map(
443
- (ledger.numbers ?? []).map(({ subject, value }) => [subject, value]),
444
- ),
445
- statuses: new Map(
446
- (ledger.statuses ?? []).map(({ subject, value }) => [subject, value]),
447
- ),
276
+ terminology: (ledger.terminology ?? []).map(({ term }) => ({
277
+ claim: term,
278
+ value: term,
279
+ })),
280
+ numericClaims: (ledger.numbers ?? []).map(({ subject, value }) => ({
281
+ claim: subject,
282
+ value,
283
+ })),
284
+ statuses: (ledger.statuses ?? []).map(({ subject, value }) => ({
285
+ claim: subject,
286
+ value,
287
+ })),
448
288
  }
449
289
  : null;
450
290
 
451
- if (expected && groups.some((group) => expected[group].size === 0)) {
291
+ if (expected && groups.every((group) => expected[group].length === 0)) {
452
292
  issues.push({
453
293
  code: 'cohesion-ledger-empty',
454
- message:
455
- 'Adaptive recap cohesion requires non-empty terminology, numeric, and status ledger entries.',
294
+ message: 'Cohesion requires at least one ledger entry.',
456
295
  });
457
296
  }
458
297
 
@@ -466,34 +305,26 @@ export function checkArtifactCohesion(artifacts, { ledger = null } = {}) {
466
305
  );
467
306
  }
468
307
  for (const [claim, value] of Object.entries(values)) {
469
- const normalized = normalizeClaim(value);
470
- const prior = claims.get(claim);
471
- if (!prior) {
472
- claims.set(claim, {
473
- normalized,
474
- value,
475
- artifactId: artifact.id,
476
- });
477
- } else if (prior.normalized !== normalized) {
478
- issues.push({
479
- code: `cohesion-${group}`,
480
- message: `Artifact set disagrees on ${group}.${claim}.`,
481
- claim,
482
- values: [
483
- { artifactId: prior.artifactId, value: prior.value },
484
- { artifactId: artifact.id, value },
485
- ],
486
- });
487
- }
308
+ addObservedValues(claims, issues, group, claim, value, artifact.id);
309
+ }
310
+ if (group === 'terminology') continue;
311
+ for (const entry of artifact?.cohesion?.claims ?? []) {
312
+ if (!claimKindMatchesGroup(entry?.kind, group)) continue;
313
+ if (typeof entry?.subject !== 'string') continue;
314
+ addObservedValues(
315
+ claims,
316
+ issues,
317
+ group,
318
+ entry.subject,
319
+ entry.value,
320
+ artifact.id,
321
+ );
488
322
  }
489
323
  }
490
324
  if (expected) {
491
- for (const [claim, expectedValue] of expected[group]) {
325
+ for (const { claim, value: expectedValue } of expected[group]) {
492
326
  const observed = claims.get(claim);
493
- if (
494
- !observed ||
495
- observed.normalized !== normalizeClaim(expectedValue)
496
- ) {
327
+ if (!observed?.normalized.has(normalizeClaim(expectedValue))) {
497
328
  issues.push({
498
329
  code: 'cohesion-claim-unobserved',
499
330
  message: `Rendered artifacts do not observably support ${group}.${claim}.`,
@@ -527,33 +358,15 @@ export function checkArtifactCohesion(artifacts, { ledger = null } = {}) {
527
358
  export async function runBrowserProbes({
528
359
  artifacts,
529
360
  probe,
530
- browserSession,
531
361
  widths = REPRESENTATIVE_WIDTHS,
532
- evidenceRoot,
533
- requireEvidence = false,
534
362
  onProbeResult,
535
363
  }) {
536
364
  if (!Array.isArray(artifacts) || artifacts.length === 0) {
537
365
  throw new TypeError('Browser QA requires at least one artifact.');
538
366
  }
539
- if (browserSession !== undefined && probe !== undefined) {
540
- throw new TypeError(
541
- 'Browser QA accepts either a trusted browser session or a bare non-retaining probe, not both.',
542
- );
543
- }
544
- const session =
545
- browserSession === undefined
546
- ? null
547
- : assertBrowserProbeSession(browserSession, { allowFixture: true });
548
- const resolvedProbe = session?.probe ?? probe;
549
- if (typeof resolvedProbe !== 'function') {
367
+ if (typeof probe !== 'function') {
550
368
  throw new TypeError('Browser QA requires a probe callback.');
551
369
  }
552
- if (evidenceRoot && !session) {
553
- throw new TypeError(
554
- 'A trusted browser session is required for retained evidence.',
555
- );
556
- }
557
370
  if (onProbeResult !== undefined && typeof onProbeResult !== 'function') {
558
371
  throw new TypeError('Browser QA probe observer must be a callback.');
559
372
  }
@@ -566,21 +379,10 @@ export async function runBrowserProbes({
566
379
  }
567
380
 
568
381
  const issues = [];
569
- const evidence = [];
570
382
  let probes = 0;
571
383
  for (const artifact of artifacts) {
572
- const evidenceId = browserEvidenceId(artifact.id);
573
384
  for (const width of widths) {
574
385
  for (const scenario of browserScenarios(artifact)) {
575
- const viewport = viewportName(width);
576
- const screenshotPath =
577
- evidenceRoot && scenario === 'default'
578
- ? `qa/browser/${evidenceId}/${viewport}.png`
579
- : undefined;
580
- const metricsPath =
581
- evidenceRoot && scenario === 'default'
582
- ? `qa/browser/${evidenceId}/${viewport}.json`
583
- : undefined;
584
386
  const request = {
585
387
  artifact,
586
388
  scenario,
@@ -594,9 +396,6 @@ export async function runBrowserProbes({
594
396
  artifact.type === 'deck' && scenario === 'default'
595
397
  ? { tab: true, arrows: [...ARROW_KEYS] }
596
398
  : { tab: true },
597
- ...(screenshotPath && {
598
- screenshotPath: join(evidenceRoot, screenshotPath),
599
- }),
600
399
  ...(scenario === 'no-js' && { javascriptEnabled: false }),
601
400
  ...(scenario === 'print' && { media: 'print' }),
602
401
  ...(artifact.type === 'deck' &&
@@ -617,62 +416,22 @@ export async function runBrowserProbes({
617
416
  },
618
417
  }),
619
418
  };
620
- let result;
621
- try {
622
- result = await resolvedProbe(request);
623
- } catch (cause) {
624
- const error = new Error(
625
- `Browser evidence callback failed: ${cause?.message ?? String(cause)}`,
626
- { cause },
627
- );
628
- error.code = 'E_VISUAL_REVIEW';
629
- throw error;
630
- }
419
+ const result = await probe(request);
631
420
  probes += 1;
632
421
  validateProbeResult(result, artifact.id, width, request);
633
422
  if (onProbeResult) {
634
- try {
635
- await onProbeResult(
636
- structuredClone({
637
- artifactId: artifact.id,
638
- artifactType: artifact.type,
639
- scenario,
640
- viewport: request.viewport,
641
- result,
642
- }),
643
- );
644
- } catch (cause) {
645
- const error = new Error(
646
- `Browser evidence observer failed: ${cause?.message ?? String(cause)}`,
647
- { cause },
648
- );
649
- error.code = 'E_VISUAL_REVIEW';
650
- throw error;
651
- }
423
+ await onProbeResult(
424
+ structuredClone({
425
+ artifactId: artifact.id,
426
+ artifactType: artifact.type,
427
+ scenario,
428
+ viewport: request.viewport,
429
+ result,
430
+ }),
431
+ );
652
432
  }
653
433
 
654
434
  const context = { artifactId: artifact.id, width, scenario };
655
- if (screenshotPath && metricsPath) {
656
- const retained = await retainBrowserEvidence({
657
- evidenceRoot,
658
- artifactId: artifact.id,
659
- viewport,
660
- viewportSize: request.viewport,
661
- screenshotPath,
662
- metricsPath,
663
- result,
664
- browserSession: session,
665
- });
666
- if (retained.valid) {
667
- evidence.push(retained.evidence);
668
- } else if (requireEvidence) {
669
- issues.push({
670
- ...context,
671
- code: 'browser-evidence-missing',
672
- message: retained.message,
673
- });
674
- }
675
- }
676
435
  if (result.pageOverflowX) {
677
436
  issues.push({
678
437
  ...context,
@@ -742,8 +501,7 @@ export async function runBrowserProbes({
742
501
  issues.push({
743
502
  ...context,
744
503
  code: 'theme-toggle',
745
- message:
746
- 'Switchable theme control must operate by keyboard and persist the alternate mode.',
504
+ message: 'Switchable theme control did not pass.',
747
505
  });
748
506
  }
749
507
  if (
@@ -754,8 +512,7 @@ export async function runBrowserProbes({
754
512
  issues.push({
755
513
  ...context,
756
514
  code: 'deck-no-js-layout',
757
- message:
758
- 'No-JS deck must use vertical flow with x-axis auto containment.',
515
+ message: 'No-JS deck layout did not pass.',
759
516
  });
760
517
  }
761
518
  if (
@@ -766,177 +523,25 @@ export async function runBrowserProbes({
766
523
  issues.push({
767
524
  ...context,
768
525
  code: 'deck-print-layout',
769
- message: 'Print deck must use its separate vertical print cascade.',
526
+ message: 'Print deck layout did not pass.',
770
527
  });
771
528
  }
772
529
  }
773
530
  }
774
531
  }
775
532
 
776
- return {
777
- valid: issues.length === 0,
778
- issues,
779
- probes,
780
- ...(evidenceRoot && { evidence }),
781
- };
782
- }
783
-
784
- export async function auditArtifactSet({
785
- artifacts,
786
- denylist = [],
787
- browserProbe,
788
- browserSession,
789
- widths,
790
- evidenceRoot,
791
- requireBrowserEvidence = false,
792
- onProbeResult,
793
- setPlan,
794
- }) {
795
- if (!Array.isArray(artifacts) || artifacts.length === 0) {
796
- throw new TypeError('Render QA requires at least one artifact.');
797
- }
798
-
799
- const artifactsWithCohesion = cohesionEvidenceFromLedger(artifacts, setPlan);
800
- const structural = artifactsWithCohesion.map((artifact) => ({
801
- id: artifact.id,
802
- ...checkHtmlStructure({ ...artifact, denylist }),
803
- }));
804
- const cohesion = checkArtifactCohesion(artifactsWithCohesion, {
805
- ...(setPlan?.recipe?.id === 'project-recap' &&
806
- Object.values(setPlan.ledger ?? {}).some(
807
- (entries) => Array.isArray(entries) && entries.length > 0,
808
- ) && {
809
- ledger: setPlan.ledger,
810
- }),
811
- });
812
- const browserProvider = browserSession ?? browserProbe;
813
- const browser = browserProvider
814
- ? await runBrowserProbes({
815
- artifacts,
816
- ...(browserSession ? { browserSession } : { probe: browserProbe }),
817
- ...(widths && { widths }),
818
- ...(evidenceRoot && { evidenceRoot }),
819
- ...(requireBrowserEvidence && { requireEvidence: true }),
820
- ...(onProbeResult !== undefined && { onProbeResult }),
821
- })
822
- : null;
823
- const issues = [
824
- ...structural.flatMap((artifact) => artifact.issues),
825
- ...cohesion.issues,
826
- ...(browser?.issues ?? []),
827
- ];
828
-
829
- return {
830
- valid: issues.length === 0,
831
- issues,
832
- artifacts: structural,
833
- cohesion,
834
- browser,
835
- };
836
- }
837
-
838
- async function retainBrowserEvidence({
839
- evidenceRoot,
840
- artifactId: artifactIdentifier,
841
- viewport,
842
- viewportSize,
843
- screenshotPath,
844
- metricsPath,
845
- result,
846
- browserSession,
847
- }) {
848
- let screenshot;
849
- try {
850
- screenshot = await stat(join(evidenceRoot, screenshotPath));
851
- } catch {
852
- return {
853
- valid: false,
854
- message: `Browser screenshot evidence is missing for ${artifactIdentifier} at ${viewportSize.width}px.`,
855
- };
856
- }
857
- if (
858
- !screenshot.isFile() ||
859
- screenshot.size === 0 ||
860
- screenshot.size > MAX_SCREENSHOT_BYTES
861
- ) {
862
- return {
863
- valid: false,
864
- message: `Browser screenshot evidence for ${artifactIdentifier} at ${viewportSize.width}px is empty or exceeds ${MAX_SCREENSHOT_BYTES} bytes.`,
865
- };
866
- }
867
- const screenshotBytes = await readFile(join(evidenceRoot, screenshotPath));
868
- const decoded = decodedPng(screenshotBytes);
869
- if (
870
- !decoded ||
871
- decoded.width !== viewportSize.width ||
872
- decoded.height !== viewportSize.height
873
- ) {
874
- return {
875
- valid: false,
876
- message: `Browser screenshot evidence for ${artifactIdentifier} at ${viewportSize.width}px must be a viewport-matched PNG.`,
877
- };
878
- }
879
- await writeJsonAtomic(evidenceRoot, metricsPath, {
880
- schemaVersion: 'explainer-kit.browser-evidence/v2',
881
- artifactId: artifactIdentifier,
882
- viewport,
883
- viewportSize,
884
- scenario: 'default',
885
- runtime: structuredClone(browserSession.runtime),
886
- capture: structuredClone(browserSession.capture),
887
- captureIdentity: browserSession.captureIdentity,
888
- screenshotPath,
889
- metrics: structuredClone(result),
890
- });
891
- return {
892
- valid: true,
893
- evidence: {
894
- artifactId: artifactIdentifier,
895
- viewport,
896
- width: viewportSize.width,
897
- height: viewportSize.height,
898
- screenshotPath,
899
- decodedScreenshotHash: decoded.decodedHash,
900
- metricsPath,
901
- runtime: structuredClone(browserSession.runtime),
902
- captureIdentity: browserSession.captureIdentity,
903
- },
904
- };
533
+ return { valid: issues.length === 0, issues, probes };
905
534
  }
906
535
 
907
536
  export function pngDimensions(bytes) {
908
- const decoded = decodedPng(bytes);
909
- return decoded ? { width: decoded.width, height: decoded.height } : null;
910
- }
911
-
912
- function decodedPng(bytes) {
913
537
  try {
914
- return decodeBrowserPng(bytes);
538
+ const decoded = decodeBrowserPng(bytes);
539
+ return { width: decoded.width, height: decoded.height };
915
540
  } catch {
916
541
  return null;
917
542
  }
918
543
  }
919
544
 
920
- function browserEvidenceId(value) {
921
- if (
922
- typeof value !== 'string' ||
923
- !/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/.test(value)
924
- ) {
925
- throw new TypeError('Browser evidence requires a safe artifact id.');
926
- }
927
- return value;
928
- }
929
-
930
- function viewportName(width) {
931
- return (
932
- {
933
- 320: 'mobile',
934
- 768: 'tablet',
935
- 1440: 'desktop',
936
- }[width] ?? `viewport-${width}`
937
- );
938
- }
939
-
940
545
  function checkHeadings(html, add) {
941
546
  const headings = [
942
547
  ...html.matchAll(/<h([1-6])\b[^>]*>([\s\S]*?)<\/h\1\s*>/gi),
@@ -944,7 +549,6 @@ function checkHeadings(html, add) {
944
549
  level: Number(match[1]),
945
550
  text: visibleText(match[2]),
946
551
  }));
947
-
948
552
  if (
949
553
  headings.length === 0 ||
950
554
  headings.filter(({ level }) => level === 1).length > 1
@@ -1016,9 +620,7 @@ function findTagImbalances(html) {
1016
620
  );
1017
621
  }
1018
622
  }
1019
- for (const tag of stack.reverse()) {
1020
- issues.push(`Tag <${tag}> is not closed.`);
1021
- }
623
+ for (const tag of stack.reverse()) issues.push(`Tag <${tag}> is not closed.`);
1022
624
  return issues;
1023
625
  }
1024
626
 
@@ -1029,10 +631,47 @@ function visibleText(value) {
1029
631
  .trim();
1030
632
  }
1031
633
 
634
+ function addObservedValues(claims, issues, group, claim, value, artifactId) {
635
+ const items = Array.isArray(value) ? value : [value];
636
+ for (const item of items) {
637
+ const normalized = normalizeClaim(item);
638
+ const prior = claims.get(claim);
639
+ if (!prior) {
640
+ claims.set(claim, {
641
+ normalized: new Set([normalized]),
642
+ value: item,
643
+ artifactId,
644
+ });
645
+ continue;
646
+ }
647
+ if (group === 'terminology' && !prior.normalized.has(normalized)) {
648
+ issues.push({
649
+ code: `cohesion-${group}`,
650
+ message: `Artifact set disagrees on ${group}.${claim}.`,
651
+ claim,
652
+ values: [
653
+ { artifactId: prior.artifactId, value: prior.value },
654
+ { artifactId, value: item },
655
+ ],
656
+ });
657
+ }
658
+ prior.normalized.add(normalized);
659
+ }
660
+ }
661
+
662
+ function claimKindMatchesGroup(kind, group) {
663
+ if (group === 'statuses') return kind === 'status';
664
+ return kind === 'number' || kind === 'date';
665
+ }
666
+
1032
667
  function normalizeClaim(value) {
1033
668
  if (typeof value === 'number') return `number:${value}`;
1034
669
  if (typeof value === 'string') {
1035
- const compact = value.trim().replace(/\s+/g, ' ').toLocaleLowerCase();
670
+ const compact = value
671
+ .trim()
672
+ .replace(/\s+/g, ' ')
673
+ .replaceAll(',', '')
674
+ .toLocaleLowerCase();
1036
675
  if (/^[+-]?(?:\d+\.?\d*|\.\d+)$/.test(compact)) {
1037
676
  return `number:${Number(compact)}`;
1038
677
  }
@@ -1097,43 +736,6 @@ function representativeHeight(width) {
1097
736
  return 900;
1098
737
  }
1099
738
 
1100
- function artifactId(artifact) {
1101
- return artifact?.id ?? artifact?.artifactId;
1102
- }
1103
-
1104
- function hasElementId(html, id) {
1105
- const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1106
- return new RegExp(`\\bid\\s*=\\s*["']${escaped}["']`, 'i').test(html);
1107
- }
1108
-
1109
- function addExpansionWarnings(warnings, expansion) {
1110
- if (expansion === undefined) return;
1111
- if (
1112
- !isPlainObject(expansion) ||
1113
- !Array.isArray(expansion.errors) ||
1114
- !Array.isArray(expansion.rejected) ||
1115
- !Array.isArray(expansion.warnings)
1116
- ) {
1117
- throw new TypeError(
1118
- 'Guideline checker expansion must be an evaluated proposal result.',
1119
- );
1120
- }
1121
- if (expansion.valid !== true || expansion.errors.length > 0) {
1122
- throw new TypeError(
1123
- 'Guideline checker cannot convert expansion proposal errors into warnings.',
1124
- );
1125
- }
1126
-
1127
- const knownWarnings = new Set(EXPANSION_WARNING_BY_REJECTION_REASON.values());
1128
- for (const warning of expansion.warnings) {
1129
- if (knownWarnings.has(warning)) warnings.add(warning);
1130
- }
1131
- for (const rejected of expansion.rejected) {
1132
- const warning = EXPANSION_WARNING_BY_REJECTION_REASON.get(rejected?.reason);
1133
- if (warning) warnings.add(warning);
1134
- }
1135
- }
1136
-
1137
739
  function isPlainObject(value) {
1138
740
  return typeof value === 'object' && value !== null && !Array.isArray(value);
1139
741
  }