@open-agent-toolkit/cli 0.2.73 → 0.2.75
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
|
@@ -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
|
-
|
|
377
|
-
|
|
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
|
|
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:
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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.
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
}
|