@ryuenn3123/agentic-senior-core 3.0.16 → 3.0.17
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/.agent-context/prompts/bootstrap-design.md +21 -3
- package/.agent-context/rules/frontend-architecture.md +26 -0
- package/.agent-context/state/memory-continuity-benchmark.json +1 -1
- package/.cursorrules +1 -1
- package/.gemini/instructions.md +7 -1
- package/.github/copilot-instructions.md +7 -1
- package/.instructions.md +3 -0
- package/.windsurfrules +1 -1
- package/AGENTS.md +13 -1
- package/lib/cli/commands/init.mjs +2 -2
- package/lib/cli/memory-continuity.mjs +2 -1
- package/lib/cli/project-scaffolder/constants.mjs +1 -0
- package/lib/cli/project-scaffolder/design-contract.mjs +303 -0
- package/lib/cli/project-scaffolder/prompt-builders.mjs +29 -12
- package/package.json +1 -1
- package/scripts/documentation-boundary-audit.mjs +5 -2
- package/scripts/frontend-usability-audit.mjs +23 -0
- package/scripts/release-gate.mjs +22 -0
- package/scripts/sync-thin-adapters.mjs +24 -0
- package/scripts/ui-design-judge.mjs +365 -7
- package/scripts/validate/config.mjs +45 -0
package/package.json
CHANGED
|
@@ -53,7 +53,10 @@ const BOUNDARY_RULES = [
|
|
|
53
53
|
],
|
|
54
54
|
trigger(filePath) {
|
|
55
55
|
return !isDocumentationFilePath(filePath)
|
|
56
|
-
&&
|
|
56
|
+
&& (
|
|
57
|
+
/(api|openapi|controller|route|endpoint)/i.test(filePath)
|
|
58
|
+
|| /api[-_/]?contract/i.test(filePath)
|
|
59
|
+
);
|
|
57
60
|
},
|
|
58
61
|
docsMatcher(filePath) {
|
|
59
62
|
return filePath === '.agent-context/rules/api-docs.md'
|
|
@@ -285,4 +288,4 @@ function runDocumentationBoundaryAudit() {
|
|
|
285
288
|
process.exit(reportPayload.passed ? 0 : 1);
|
|
286
289
|
}
|
|
287
290
|
|
|
288
|
-
runDocumentationBoundaryAudit();
|
|
291
|
+
runDocumentationBoundaryAudit();
|
|
@@ -54,6 +54,14 @@ const REQUIRED_FRONTEND_RULE_SNIPPETS = [
|
|
|
54
54
|
'UI scope trigger signals',
|
|
55
55
|
'visual intent, interaction quality, and conversion clarity',
|
|
56
56
|
'template-only repetitive outputs',
|
|
57
|
+
'Context Hygiene and Reference Boundaries (Mandatory)',
|
|
58
|
+
'Accessibility Split (Mandatory)',
|
|
59
|
+
'Design continuity is opt-in.',
|
|
60
|
+
'repo evidence wins',
|
|
61
|
+
'WCAG 2.2 AA as the hard compliance floor',
|
|
62
|
+
'APCA as an advisory readability model',
|
|
63
|
+
'Hybrid Visual QA Boundaries (Mandatory)',
|
|
64
|
+
'Visual QA must be deterministic-first.',
|
|
57
65
|
'UI Consistency Guardrails (Mandatory)',
|
|
58
66
|
'Content language must stay consistent per screen and flow unless user requests multilingual output.',
|
|
59
67
|
'Text color must remain contrast-safe against its background; no color collisions.',
|
|
@@ -69,6 +77,13 @@ const REQUIRED_BOOTSTRAP_DESIGN_SNIPPETS = [
|
|
|
69
77
|
'crossViewportAdaptation.mutationRules.mobile/tablet/desktop',
|
|
70
78
|
'motionSystem',
|
|
71
79
|
'componentMorphology',
|
|
80
|
+
'accessibilityPolicy',
|
|
81
|
+
'visualQaPolicy',
|
|
82
|
+
'contextHygiene',
|
|
83
|
+
'Design continuity is opt-in.',
|
|
84
|
+
'WCAG 2.2 AA as the blocking baseline',
|
|
85
|
+
'APCA only as advisory perceptual tuning',
|
|
86
|
+
'Hybrid visual QA must stay deterministic-first',
|
|
72
87
|
];
|
|
73
88
|
|
|
74
89
|
const REQUIRED_UI_DESIGN_JUDGE_SNIPPETS = [
|
|
@@ -77,6 +92,9 @@ const REQUIRED_UI_DESIGN_JUDGE_SNIPPETS = [
|
|
|
77
92
|
'Runs only in advisory mode for this repository workflow.',
|
|
78
93
|
'Do not reward generic SaaS defaults or popular template patterns.',
|
|
79
94
|
'UI design judge only evaluates changed UI surfaces.',
|
|
95
|
+
'Deterministic visual diff reported no meaningful drift, so semantic review was skipped.',
|
|
96
|
+
'UI_VISUAL_DIFF_REPORT_JSON',
|
|
97
|
+
'meaningfulDiffViewportCount',
|
|
80
98
|
];
|
|
81
99
|
|
|
82
100
|
const REQUIRED_INSTRUCTIONS_SNIPPETS = [
|
|
@@ -84,6 +102,11 @@ const REQUIRED_INSTRUCTIONS_SNIPPETS = [
|
|
|
84
102
|
'bootstrap-design.md',
|
|
85
103
|
'frontend-architecture.md',
|
|
86
104
|
'do not eagerly load unrelated backend-only rules',
|
|
105
|
+
'valid style context',
|
|
106
|
+
'explicitly approved reference systems',
|
|
107
|
+
'WCAG 2.2 AA as the hard compliance floor',
|
|
108
|
+
'APCA as advisory perceptual tuning only',
|
|
109
|
+
'deterministic screenshot or diff evidence as the first layer',
|
|
87
110
|
];
|
|
88
111
|
|
|
89
112
|
const REQUIRED_DESIGN_EVIDENCE_SNIPPETS = [
|
package/scripts/release-gate.mjs
CHANGED
|
@@ -747,6 +747,28 @@ function runReleaseGate() {
|
|
|
747
747
|
'UI design judge unexpectedly ran in blocking mode during release gate'
|
|
748
748
|
);
|
|
749
749
|
}
|
|
750
|
+
|
|
751
|
+
const hasHybridVisualQaDiagnostics = typeof uiDesignJudgeExecution.report?.summary?.meaningfulDiffViewportCount === 'number'
|
|
752
|
+
&& typeof uiDesignJudgeExecution.report?.deterministicVisual?.reportPresent === 'boolean'
|
|
753
|
+
&& Array.isArray(uiDesignJudgeExecution.report?.deterministicVisual?.requiredViewports)
|
|
754
|
+
&& typeof uiDesignJudgeExecution.report?.semanticJudge?.attempted === 'boolean'
|
|
755
|
+
&& typeof uiDesignJudgeExecution.report?.semanticJudge?.skipped === 'boolean';
|
|
756
|
+
|
|
757
|
+
if (hasHybridVisualQaDiagnostics) {
|
|
758
|
+
pushResult(
|
|
759
|
+
results,
|
|
760
|
+
true,
|
|
761
|
+
'ui-design-judge-hybrid-diagnostics',
|
|
762
|
+
'UI design judge reports deterministic visual QA summary and semantic-review state together'
|
|
763
|
+
);
|
|
764
|
+
} else {
|
|
765
|
+
pushResult(
|
|
766
|
+
results,
|
|
767
|
+
false,
|
|
768
|
+
'ui-design-judge-hybrid-diagnostics',
|
|
769
|
+
'UI design judge is missing deterministic visual QA or semantic-review machine-readable fields'
|
|
770
|
+
);
|
|
771
|
+
}
|
|
750
772
|
}
|
|
751
773
|
|
|
752
774
|
const benchmarkGateExecution = runMachineReadableScript(BENCHMARK_GATE_SCRIPT_PATH);
|
|
@@ -36,6 +36,18 @@ Canonical Snapshot SHA256: ${canonicalHash}
|
|
|
36
36
|
This file is an adapter entrypoint for agent discovery.
|
|
37
37
|
The canonical policy source is [.instructions.md](.instructions.md).
|
|
38
38
|
|
|
39
|
+
If your host stops at this file instead of following the full chain, obey the Critical Bootstrap Floor below before coding.
|
|
40
|
+
|
|
41
|
+
## Critical Bootstrap Floor
|
|
42
|
+
|
|
43
|
+
- If \`.agent-instructions.md\` exists, prefer it immediately after this file because it is the compiled project-specific snapshot.
|
|
44
|
+
- Memory continuity does not replace bootstrap loading. It is host-dependent project memory, not a guarantee that instructions were reloaded for this session.
|
|
45
|
+
- For UI, UX, layout, screen, tailwind, frontend, or redesign requests: load [.agent-context/prompts/bootstrap-design.md](.agent-context/prompts/bootstrap-design.md) and [.agent-context/rules/frontend-architecture.md](.agent-context/rules/frontend-architecture.md) before editing code.
|
|
46
|
+
- For UI scope: if \`docs/DESIGN.md\` or \`docs/design-intent.json\` is missing, materialize or refine them before implementing UI changes.
|
|
47
|
+
- For refactor, improve, clean up, or fix requests: inspect the active rules and propose a plan before editing.
|
|
48
|
+
- For new project or module requests: propose architecture before generating code.
|
|
49
|
+
- For ecosystem, framework, dependency, or Docker claims: perform live web research instead of relying on stale local heuristics.
|
|
50
|
+
|
|
39
51
|
## Mandatory Bootstrap Chain
|
|
40
52
|
|
|
41
53
|
1. Load [.instructions.md](.instructions.md) first as the canonical baseline.
|
|
@@ -66,6 +78,12 @@ Canonical Snapshot SHA256: ${canonicalHash}
|
|
|
66
78
|
|
|
67
79
|
The canonical policy source for this repository is [.instructions.md](../.instructions.md).
|
|
68
80
|
|
|
81
|
+
If your host stops at this file, follow this minimum floor:
|
|
82
|
+
- Read \`.agent-instructions.md\` next when it exists.
|
|
83
|
+
- For UI or redesign requests, load [.agent-context/prompts/bootstrap-design.md](../.agent-context/prompts/bootstrap-design.md) and [.agent-context/rules/frontend-architecture.md](../.agent-context/rules/frontend-architecture.md) before coding.
|
|
84
|
+
- If UI scope and \`docs/DESIGN.md\` or \`docs/design-intent.json\` is missing, materialize them before UI implementation.
|
|
85
|
+
- Memory continuity is host-dependent project memory and does not replace bootstrap loading.
|
|
86
|
+
|
|
69
87
|
## Required Load Order
|
|
70
88
|
|
|
71
89
|
1. Read [.instructions.md](../.instructions.md) first as the canonical baseline.
|
|
@@ -91,6 +109,12 @@ Canonical Snapshot SHA256: ${canonicalHash}
|
|
|
91
109
|
|
|
92
110
|
Canonical policy source: [.instructions.md](../.instructions.md).
|
|
93
111
|
|
|
112
|
+
If your host stops at this file, follow this minimum floor:
|
|
113
|
+
- Read \`.agent-instructions.md\` next when it exists.
|
|
114
|
+
- For UI or redesign requests, load [.agent-context/prompts/bootstrap-design.md](../.agent-context/prompts/bootstrap-design.md) and [.agent-context/rules/frontend-architecture.md](../.agent-context/rules/frontend-architecture.md) before coding.
|
|
115
|
+
- If UI scope and \`docs/DESIGN.md\` or \`docs/design-intent.json\` is missing, materialize them before UI implementation.
|
|
116
|
+
- Memory continuity is host-dependent project memory and does not replace bootstrap loading.
|
|
117
|
+
|
|
94
118
|
## Bootstrap Sequence
|
|
95
119
|
|
|
96
120
|
1. Load [.instructions.md](../.instructions.md) first as the canonical baseline.
|
|
@@ -25,6 +25,8 @@ const DESIGN_INTENT_PATH = resolve(REPOSITORY_ROOT, 'docs', 'design-intent.json'
|
|
|
25
25
|
const DESIGN_GUIDE_PATH = resolve(REPOSITORY_ROOT, 'docs', 'DESIGN.md');
|
|
26
26
|
const MAX_DIFF_CHARS = 12000;
|
|
27
27
|
const UI_FILE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.vue', '.css', '.scss', '.sass']);
|
|
28
|
+
const DEFAULT_VISUAL_DIFF_REPORT_VERSION = 'hybrid-visual-diff-v1';
|
|
29
|
+
const DEFAULT_REQUIRED_VIEWPORTS = ['mobile', 'tablet', 'desktop'];
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* @typedef {{
|
|
@@ -55,6 +57,32 @@ const UI_FILE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.vue', '.css'
|
|
|
55
57
|
* alignmentScore: number | null,
|
|
56
58
|
* driftCount: number,
|
|
57
59
|
* blockingCandidateCount: number,
|
|
60
|
+
* meaningfulDiffViewportCount: number,
|
|
61
|
+
* },
|
|
62
|
+
* deterministicVisual: {
|
|
63
|
+
* reportPresent: boolean,
|
|
64
|
+
* reportVersion: string | null,
|
|
65
|
+
* baselineStrategy: string | null,
|
|
66
|
+
* coverageComplete: boolean,
|
|
67
|
+
* sectionCoverageRequired: boolean,
|
|
68
|
+
* requiredViewports: string[],
|
|
69
|
+
* coveredViewports: string[],
|
|
70
|
+
* missingViewports: string[],
|
|
71
|
+
* requiredSectionTypes: string[],
|
|
72
|
+
* coveredSectionTypes: string[],
|
|
73
|
+
* missingSectionTypes: string[],
|
|
74
|
+
* meaningfulDiffViewports: string[],
|
|
75
|
+
* meaningfulDiffSectionTypes: string[],
|
|
76
|
+
* maskedViewportCount: number,
|
|
77
|
+
* sectionCaptureCount: number,
|
|
78
|
+
* tileCaptureCount: number,
|
|
79
|
+
* semanticEscalationRecommended: boolean,
|
|
80
|
+
* notes: string[],
|
|
81
|
+
* },
|
|
82
|
+
* semanticJudge: {
|
|
83
|
+
* attempted: boolean,
|
|
84
|
+
* skipped: boolean,
|
|
85
|
+
* skipReason: string | null,
|
|
58
86
|
* },
|
|
59
87
|
* malformedVerdict: boolean,
|
|
60
88
|
* providerError: boolean,
|
|
@@ -227,16 +255,259 @@ function loadDesignGuide() {
|
|
|
227
255
|
return readFileSync(DESIGN_GUIDE_PATH, 'utf8');
|
|
228
256
|
}
|
|
229
257
|
|
|
258
|
+
function toFiniteRatio(rawValue) {
|
|
259
|
+
return typeof rawValue === 'number' && Number.isFinite(rawValue)
|
|
260
|
+
? rawValue
|
|
261
|
+
: null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function normalizeStringArray(rawValue) {
|
|
265
|
+
if (!Array.isArray(rawValue)) {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return rawValue
|
|
270
|
+
.map((entryValue) => String(entryValue || '').trim())
|
|
271
|
+
.filter(Boolean);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function loadDeterministicVisualReport() {
|
|
275
|
+
if (process.env.UI_VISUAL_DIFF_REPORT_JSON) {
|
|
276
|
+
try {
|
|
277
|
+
return JSON.parse(process.env.UI_VISUAL_DIFF_REPORT_JSON);
|
|
278
|
+
} catch {
|
|
279
|
+
return {
|
|
280
|
+
malformed: true,
|
|
281
|
+
notes: ['UI_VISUAL_DIFF_REPORT_JSON could not be parsed as JSON.'],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (process.env.UI_VISUAL_DIFF_REPORT_PATH) {
|
|
287
|
+
const reportPath = resolve(REPOSITORY_ROOT, process.env.UI_VISUAL_DIFF_REPORT_PATH);
|
|
288
|
+
if (!existsSync(reportPath)) {
|
|
289
|
+
return {
|
|
290
|
+
malformed: true,
|
|
291
|
+
notes: [`UI_VISUAL_DIFF_REPORT_PATH does not exist: ${process.env.UI_VISUAL_DIFF_REPORT_PATH}`],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
return JSON.parse(readFileSync(reportPath, 'utf8'));
|
|
297
|
+
} catch {
|
|
298
|
+
return {
|
|
299
|
+
malformed: true,
|
|
300
|
+
notes: [`UI_VISUAL_DIFF_REPORT_PATH could not be parsed as JSON: ${process.env.UI_VISUAL_DIFF_REPORT_PATH}`],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function summarizeDeterministicVisualReport(rawVisualReport, designIntentContent) {
|
|
309
|
+
const visualQaPolicy = designIntentContent?.visualQaPolicy && typeof designIntentContent.visualQaPolicy === 'object'
|
|
310
|
+
? designIntentContent.visualQaPolicy
|
|
311
|
+
: {};
|
|
312
|
+
const capturePlan = visualQaPolicy?.capturePlan && typeof visualQaPolicy.capturePlan === 'object'
|
|
313
|
+
? visualQaPolicy.capturePlan
|
|
314
|
+
: {};
|
|
315
|
+
const requiredViewports = normalizeStringArray(visualQaPolicy.requiredViewports);
|
|
316
|
+
const normalizedRequiredViewports = requiredViewports.length > 0 ? requiredViewports : DEFAULT_REQUIRED_VIEWPORTS;
|
|
317
|
+
const requiredSectionTypes = normalizeStringArray(capturePlan.requiredSectionTypes);
|
|
318
|
+
const meaningfulDiffRatioThreshold = toFiniteRatio(visualQaPolicy?.semanticEscalation?.meaningfulDiffRatioThreshold) ?? 0.01;
|
|
319
|
+
const maxUnmaskedDiffRatio = toFiniteRatio(visualQaPolicy?.stability?.maxUnmaskedDiffRatio) ?? 0.005;
|
|
320
|
+
const maxMaskedDiffRatio = toFiniteRatio(visualQaPolicy?.stability?.maxMaskedDiffRatio) ?? 0.02;
|
|
321
|
+
|
|
322
|
+
if (!rawVisualReport) {
|
|
323
|
+
return {
|
|
324
|
+
reportPresent: false,
|
|
325
|
+
reportVersion: null,
|
|
326
|
+
baselineStrategy: visualQaPolicy.baselineStrategy || null,
|
|
327
|
+
coverageComplete: false,
|
|
328
|
+
sectionCoverageRequired: capturePlan.requireSectionCapturesForLongPages === true,
|
|
329
|
+
requiredViewports: normalizedRequiredViewports,
|
|
330
|
+
coveredViewports: [],
|
|
331
|
+
missingViewports: normalizedRequiredViewports,
|
|
332
|
+
requiredSectionTypes,
|
|
333
|
+
coveredSectionTypes: [],
|
|
334
|
+
missingSectionTypes: requiredSectionTypes,
|
|
335
|
+
meaningfulDiffViewports: [],
|
|
336
|
+
meaningfulDiffSectionTypes: [],
|
|
337
|
+
maskedViewportCount: 0,
|
|
338
|
+
sectionCaptureCount: 0,
|
|
339
|
+
tileCaptureCount: 0,
|
|
340
|
+
semanticEscalationRecommended: false,
|
|
341
|
+
notes: ['No deterministic visual diff report was supplied.'],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (rawVisualReport.malformed === true) {
|
|
346
|
+
return {
|
|
347
|
+
reportPresent: false,
|
|
348
|
+
reportVersion: null,
|
|
349
|
+
baselineStrategy: visualQaPolicy.baselineStrategy || null,
|
|
350
|
+
coverageComplete: false,
|
|
351
|
+
sectionCoverageRequired: capturePlan.requireSectionCapturesForLongPages === true,
|
|
352
|
+
requiredViewports: normalizedRequiredViewports,
|
|
353
|
+
coveredViewports: [],
|
|
354
|
+
missingViewports: normalizedRequiredViewports,
|
|
355
|
+
requiredSectionTypes,
|
|
356
|
+
coveredSectionTypes: [],
|
|
357
|
+
missingSectionTypes: requiredSectionTypes,
|
|
358
|
+
meaningfulDiffViewports: [],
|
|
359
|
+
meaningfulDiffSectionTypes: [],
|
|
360
|
+
maskedViewportCount: 0,
|
|
361
|
+
sectionCaptureCount: 0,
|
|
362
|
+
tileCaptureCount: 0,
|
|
363
|
+
semanticEscalationRecommended: true,
|
|
364
|
+
notes: normalizeStringArray(rawVisualReport.notes).length > 0
|
|
365
|
+
? normalizeStringArray(rawVisualReport.notes)
|
|
366
|
+
: ['Deterministic visual diff report was malformed.'],
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const viewportResults = Array.isArray(rawVisualReport.viewportResults)
|
|
371
|
+
? rawVisualReport.viewportResults
|
|
372
|
+
.map((rawViewportResult) => {
|
|
373
|
+
const viewportName = String(rawViewportResult?.viewport || '').trim().toLowerCase();
|
|
374
|
+
const pixelDiffRatio = toFiniteRatio(rawViewportResult?.pixelDiffRatio);
|
|
375
|
+
const maskedPixelDiffRatio = toFiniteRatio(rawViewportResult?.maskedPixelDiffRatio);
|
|
376
|
+
const withinNoiseBudget = typeof rawViewportResult?.withinNoiseBudget === 'boolean'
|
|
377
|
+
? rawViewportResult.withinNoiseBudget
|
|
378
|
+
: (pixelDiffRatio === null || pixelDiffRatio <= maxUnmaskedDiffRatio)
|
|
379
|
+
&& (maskedPixelDiffRatio === null || maskedPixelDiffRatio <= maxMaskedDiffRatio);
|
|
380
|
+
const meaningfulDiff = typeof rawViewportResult?.meaningfulDiff === 'boolean'
|
|
381
|
+
? rawViewportResult.meaningfulDiff
|
|
382
|
+
: (pixelDiffRatio !== null && pixelDiffRatio > meaningfulDiffRatioThreshold)
|
|
383
|
+
|| (maskedPixelDiffRatio !== null && maskedPixelDiffRatio > meaningfulDiffRatioThreshold);
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
viewport: viewportName,
|
|
387
|
+
pixelDiffRatio,
|
|
388
|
+
maskedPixelDiffRatio,
|
|
389
|
+
withinNoiseBudget,
|
|
390
|
+
meaningfulDiff,
|
|
391
|
+
dynamicMaskCategories: normalizeStringArray(rawViewportResult?.dynamicMaskCategories),
|
|
392
|
+
notes: normalizeStringArray(rawViewportResult?.notes),
|
|
393
|
+
};
|
|
394
|
+
})
|
|
395
|
+
.filter((viewportResult) => Boolean(viewportResult.viewport))
|
|
396
|
+
: [];
|
|
397
|
+
const sectionResults = Array.isArray(rawVisualReport.sectionResults)
|
|
398
|
+
? rawVisualReport.sectionResults
|
|
399
|
+
.map((rawSectionResult) => {
|
|
400
|
+
const sectionType = String(rawSectionResult?.sectionType || '').trim().toLowerCase();
|
|
401
|
+
const captureKind = String(rawSectionResult?.captureKind || '').trim().toLowerCase();
|
|
402
|
+
const tileIndex = Number.isInteger(rawSectionResult?.tileIndex) ? rawSectionResult.tileIndex : null;
|
|
403
|
+
const pixelDiffRatio = toFiniteRatio(rawSectionResult?.pixelDiffRatio);
|
|
404
|
+
const maskedPixelDiffRatio = toFiniteRatio(rawSectionResult?.maskedPixelDiffRatio);
|
|
405
|
+
const withinNoiseBudget = typeof rawSectionResult?.withinNoiseBudget === 'boolean'
|
|
406
|
+
? rawSectionResult.withinNoiseBudget
|
|
407
|
+
: (pixelDiffRatio === null || pixelDiffRatio <= maxUnmaskedDiffRatio)
|
|
408
|
+
&& (maskedPixelDiffRatio === null || maskedPixelDiffRatio <= maxMaskedDiffRatio);
|
|
409
|
+
const meaningfulDiff = typeof rawSectionResult?.meaningfulDiff === 'boolean'
|
|
410
|
+
? rawSectionResult.meaningfulDiff
|
|
411
|
+
: (pixelDiffRatio !== null && pixelDiffRatio > meaningfulDiffRatioThreshold)
|
|
412
|
+
|| (maskedPixelDiffRatio !== null && maskedPixelDiffRatio > meaningfulDiffRatioThreshold);
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
sectionType,
|
|
416
|
+
captureKind,
|
|
417
|
+
tileIndex,
|
|
418
|
+
pixelDiffRatio,
|
|
419
|
+
maskedPixelDiffRatio,
|
|
420
|
+
withinNoiseBudget,
|
|
421
|
+
meaningfulDiff,
|
|
422
|
+
notes: normalizeStringArray(rawSectionResult?.notes),
|
|
423
|
+
};
|
|
424
|
+
})
|
|
425
|
+
.filter((sectionResult) => Boolean(sectionResult.sectionType))
|
|
426
|
+
: [];
|
|
427
|
+
|
|
428
|
+
const coveredViewports = Array.from(new Set(viewportResults.map((viewportResult) => viewportResult.viewport)));
|
|
429
|
+
const missingViewports = normalizedRequiredViewports.filter((requiredViewport) => !coveredViewports.includes(requiredViewport));
|
|
430
|
+
const sectionCoverageRequired = capturePlan.requireSectionCapturesForLongPages === true && (
|
|
431
|
+
rawVisualReport.requiresSectionCoverage === true
|
|
432
|
+
|| String(rawVisualReport.pageLengthCategory || '').trim().toLowerCase() === 'long'
|
|
433
|
+
|| sectionResults.length > 0
|
|
434
|
+
);
|
|
435
|
+
const coveredSectionTypes = Array.from(new Set(sectionResults.map((sectionResult) => sectionResult.sectionType)));
|
|
436
|
+
const missingSectionTypes = sectionCoverageRequired
|
|
437
|
+
? requiredSectionTypes.filter((requiredSectionType) => !coveredSectionTypes.includes(requiredSectionType))
|
|
438
|
+
: [];
|
|
439
|
+
const meaningfulDiffViewports = viewportResults
|
|
440
|
+
.filter((viewportResult) => viewportResult.meaningfulDiff)
|
|
441
|
+
.map((viewportResult) => viewportResult.viewport);
|
|
442
|
+
const meaningfulDiffSectionTypes = Array.from(new Set(
|
|
443
|
+
sectionResults
|
|
444
|
+
.filter((sectionResult) => sectionResult.meaningfulDiff)
|
|
445
|
+
.map((sectionResult) => sectionResult.sectionType)
|
|
446
|
+
));
|
|
447
|
+
const maskedViewportCount = viewportResults.filter((viewportResult) => viewportResult.dynamicMaskCategories.length > 0).length;
|
|
448
|
+
const tileCaptureCount = sectionResults.filter((sectionResult) => sectionResult.captureKind === 'tile').length;
|
|
449
|
+
const reportNotes = normalizeStringArray(rawVisualReport.notes);
|
|
450
|
+
|
|
451
|
+
const semanticEscalationRecommended = rawVisualReport?.summary?.semanticEscalationRecommended === true
|
|
452
|
+
|| meaningfulDiffViewports.length > 0
|
|
453
|
+
|| meaningfulDiffSectionTypes.length > 0
|
|
454
|
+
|| (
|
|
455
|
+
visualQaPolicy?.semanticEscalation?.escalateWhenViewportCoverageIncomplete === true
|
|
456
|
+
&& missingViewports.length > 0
|
|
457
|
+
)
|
|
458
|
+
|| (
|
|
459
|
+
sectionCoverageRequired
|
|
460
|
+
&& missingSectionTypes.length > 0
|
|
461
|
+
);
|
|
462
|
+
const fallbackNotes = [];
|
|
463
|
+
if (viewportResults.length === 0) {
|
|
464
|
+
fallbackNotes.push('Deterministic visual diff report did not include viewportResults.');
|
|
465
|
+
}
|
|
466
|
+
if (sectionCoverageRequired && sectionResults.length === 0) {
|
|
467
|
+
fallbackNotes.push('Long-page screenshot coverage was required, but sectionResults were not provided.');
|
|
468
|
+
}
|
|
469
|
+
if (sectionCoverageRequired && missingSectionTypes.length > 0) {
|
|
470
|
+
fallbackNotes.push(`Long-page screenshot coverage is incomplete. Missing section captures: ${missingSectionTypes.join(', ')}.`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
reportPresent: true,
|
|
475
|
+
reportVersion: String(rawVisualReport.reportVersion || DEFAULT_VISUAL_DIFF_REPORT_VERSION),
|
|
476
|
+
baselineStrategy: String(rawVisualReport.baselineStrategy || visualQaPolicy.baselineStrategy || 'deterministic-screenshots'),
|
|
477
|
+
coverageComplete: missingViewports.length === 0 && (!sectionCoverageRequired || missingSectionTypes.length === 0),
|
|
478
|
+
sectionCoverageRequired,
|
|
479
|
+
requiredViewports: normalizedRequiredViewports,
|
|
480
|
+
coveredViewports,
|
|
481
|
+
missingViewports,
|
|
482
|
+
requiredSectionTypes,
|
|
483
|
+
coveredSectionTypes,
|
|
484
|
+
missingSectionTypes,
|
|
485
|
+
meaningfulDiffViewports,
|
|
486
|
+
meaningfulDiffSectionTypes,
|
|
487
|
+
maskedViewportCount,
|
|
488
|
+
sectionCaptureCount: sectionResults.length,
|
|
489
|
+
tileCaptureCount,
|
|
490
|
+
semanticEscalationRecommended,
|
|
491
|
+
notes: reportNotes.length > 0
|
|
492
|
+
? reportNotes
|
|
493
|
+
: fallbackNotes,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
230
497
|
function buildSystemPrompt() {
|
|
231
498
|
return [
|
|
232
499
|
'You are a Principal UI/UX Design Reviewer.',
|
|
233
500
|
'Compare the changed UI code against the provided design contract.',
|
|
234
501
|
'Treat docs/design-intent.json as the machine-readable source of truth.',
|
|
235
502
|
'Treat docs/DESIGN.md as explanatory context, not a generic style guide.',
|
|
503
|
+
'When deterministic visual diff evidence is provided, treat it as the first layer of truth for noise filtering, viewport coverage, long-page section coverage, and meaningful-drift detection.',
|
|
236
504
|
'Do not reward generic SaaS defaults or popular template patterns.',
|
|
237
505
|
'Do not penalize originality when the implementation still aligns with the contract.',
|
|
238
506
|
'Purposeful motion is allowed and can improve quality. Only flag motion when it drifts from the contract, ignores reduced-motion expectations, or adds avoidable performance/accessibility risk.',
|
|
239
507
|
'Only flag drift when there is a clear mismatch with the contract, accessibility non-negotiables, or cross-viewport adaptation rules.',
|
|
508
|
+
'Treat WCAG 2.2 AA failures as hard accessibility drift.',
|
|
509
|
+
'Treat APCA as advisory perceptual tuning only. Do not recommend blocking solely because APCA would prefer a stronger readability adjustment when WCAG hard requirements still pass.',
|
|
510
|
+
'Check focus visibility, focus appearance, target size, keyboard access, accessible authentication, and status or dynamic state access when the diff touches those surfaces.',
|
|
240
511
|
'This audit always runs in advisory mode for this repository workflow.',
|
|
241
512
|
'Focus on color intent, typographic hierarchy, responsive re-layout, purposeful motion, component morphology across states, interaction behavior, and genericity drift.',
|
|
242
513
|
'Return ONLY one JSON object on a single line prefixed with JSON_VERDICT:.',
|
|
@@ -245,7 +516,7 @@ function buildSystemPrompt() {
|
|
|
245
516
|
].join('\n');
|
|
246
517
|
}
|
|
247
518
|
|
|
248
|
-
function buildUserMessage(designIntentContent, designGuideContent, diffContent, changedUiFiles) {
|
|
519
|
+
function buildUserMessage(designIntentContent, designGuideContent, diffContent, changedUiFiles, deterministicVisualSummary) {
|
|
249
520
|
const truncatedDiff = diffContent.length > MAX_DIFF_CHARS
|
|
250
521
|
? `${diffContent.slice(0, MAX_DIFF_CHARS)}\n\n[DIFF TRUNCATED - ${diffContent.length - MAX_DIFF_CHARS} additional characters omitted]`
|
|
251
522
|
: diffContent;
|
|
@@ -264,6 +535,11 @@ function buildUserMessage(designIntentContent, designGuideContent, diffContent,
|
|
|
264
535
|
designGuideContent.trim() || '(missing DESIGN.md)',
|
|
265
536
|
'```',
|
|
266
537
|
'',
|
|
538
|
+
'## Deterministic Visual Diff Summary',
|
|
539
|
+
'```json',
|
|
540
|
+
JSON.stringify(deterministicVisualSummary, null, 2),
|
|
541
|
+
'```',
|
|
542
|
+
'',
|
|
267
543
|
'## UI Diff',
|
|
268
544
|
'```diff',
|
|
269
545
|
truncatedDiff.trim() || '(no UI diff)',
|
|
@@ -416,7 +692,7 @@ function buildReport(partialReport) {
|
|
|
416
692
|
return {
|
|
417
693
|
generatedAt: new Date().toISOString(),
|
|
418
694
|
auditName: 'ui-design-judge',
|
|
419
|
-
schemaVersion: '1.
|
|
695
|
+
schemaVersion: '1.1',
|
|
420
696
|
mode: 'advisory',
|
|
421
697
|
advisoryOnly: true,
|
|
422
698
|
passed: true,
|
|
@@ -430,6 +706,25 @@ function buildReport(partialReport) {
|
|
|
430
706
|
alignmentScore: null,
|
|
431
707
|
driftCount: 0,
|
|
432
708
|
blockingCandidateCount: 0,
|
|
709
|
+
meaningfulDiffViewportCount: 0,
|
|
710
|
+
},
|
|
711
|
+
deterministicVisual: {
|
|
712
|
+
reportPresent: false,
|
|
713
|
+
reportVersion: null,
|
|
714
|
+
baselineStrategy: null,
|
|
715
|
+
coverageComplete: false,
|
|
716
|
+
requiredViewports: [],
|
|
717
|
+
coveredViewports: [],
|
|
718
|
+
missingViewports: [],
|
|
719
|
+
meaningfulDiffViewports: [],
|
|
720
|
+
maskedViewportCount: 0,
|
|
721
|
+
semanticEscalationRecommended: false,
|
|
722
|
+
notes: [],
|
|
723
|
+
},
|
|
724
|
+
semanticJudge: {
|
|
725
|
+
attempted: false,
|
|
726
|
+
skipped: false,
|
|
727
|
+
skipReason: null,
|
|
433
728
|
},
|
|
434
729
|
malformedVerdict: false,
|
|
435
730
|
providerError: false,
|
|
@@ -470,14 +765,53 @@ async function main() {
|
|
|
470
765
|
alignmentScore: null,
|
|
471
766
|
driftCount: 0,
|
|
472
767
|
blockingCandidateCount: 0,
|
|
768
|
+
meaningfulDiffViewportCount: 0,
|
|
473
769
|
},
|
|
474
770
|
notes: ['UI design judge only evaluates changed UI surfaces.'],
|
|
475
771
|
}));
|
|
476
772
|
return;
|
|
477
773
|
}
|
|
478
774
|
|
|
775
|
+
const deterministicVisualSummary = summarizeDeterministicVisualReport(
|
|
776
|
+
loadDeterministicVisualReport(),
|
|
777
|
+
designIntentContent
|
|
778
|
+
);
|
|
779
|
+
const shouldRunSemanticJudge = !deterministicVisualSummary.reportPresent
|
|
780
|
+
|| deterministicVisualSummary.semanticEscalationRecommended;
|
|
781
|
+
|
|
782
|
+
if (!shouldRunSemanticJudge) {
|
|
783
|
+
emitMachineReadableReport(buildReport({
|
|
784
|
+
provider: 'none',
|
|
785
|
+
contractPresent: true,
|
|
786
|
+
summary: {
|
|
787
|
+
changedUiFileCount: changedUiFiles.length,
|
|
788
|
+
alignmentScore: null,
|
|
789
|
+
driftCount: 0,
|
|
790
|
+
blockingCandidateCount: 0,
|
|
791
|
+
meaningfulDiffViewportCount: deterministicVisualSummary.meaningfulDiffViewports.length,
|
|
792
|
+
},
|
|
793
|
+
deterministicVisual: deterministicVisualSummary,
|
|
794
|
+
semanticJudge: {
|
|
795
|
+
attempted: false,
|
|
796
|
+
skipped: true,
|
|
797
|
+
skipReason: 'deterministic-clean',
|
|
798
|
+
},
|
|
799
|
+
notes: [
|
|
800
|
+
'Deterministic visual diff reported no meaningful drift, so semantic review was skipped.',
|
|
801
|
+
...deterministicVisualSummary.notes,
|
|
802
|
+
],
|
|
803
|
+
}));
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
479
807
|
const systemPrompt = buildSystemPrompt();
|
|
480
|
-
const userMessage = buildUserMessage(
|
|
808
|
+
const userMessage = buildUserMessage(
|
|
809
|
+
designIntentContent,
|
|
810
|
+
designGuideContent,
|
|
811
|
+
rawDiff,
|
|
812
|
+
changedUiFiles,
|
|
813
|
+
deterministicVisualSummary
|
|
814
|
+
);
|
|
481
815
|
|
|
482
816
|
const selectedProvider = selectAvailableProvider();
|
|
483
817
|
if (!selectedProvider) {
|
|
@@ -489,8 +823,18 @@ async function main() {
|
|
|
489
823
|
alignmentScore: null,
|
|
490
824
|
driftCount: 0,
|
|
491
825
|
blockingCandidateCount: 0,
|
|
826
|
+
meaningfulDiffViewportCount: deterministicVisualSummary.meaningfulDiffViewports.length,
|
|
492
827
|
},
|
|
493
|
-
|
|
828
|
+
deterministicVisual: deterministicVisualSummary,
|
|
829
|
+
semanticJudge: {
|
|
830
|
+
attempted: false,
|
|
831
|
+
skipped: true,
|
|
832
|
+
skipReason: 'no-provider-configured',
|
|
833
|
+
},
|
|
834
|
+
notes: [
|
|
835
|
+
'No LLM provider configured. UI design judge skipped provider review and stayed advisory.',
|
|
836
|
+
...deterministicVisualSummary.notes,
|
|
837
|
+
],
|
|
494
838
|
}));
|
|
495
839
|
return;
|
|
496
840
|
}
|
|
@@ -512,8 +856,15 @@ async function main() {
|
|
|
512
856
|
alignmentScore: null,
|
|
513
857
|
driftCount: 0,
|
|
514
858
|
blockingCandidateCount: 0,
|
|
859
|
+
meaningfulDiffViewportCount: deterministicVisualSummary.meaningfulDiffViewports.length,
|
|
515
860
|
},
|
|
516
|
-
|
|
861
|
+
deterministicVisual: deterministicVisualSummary,
|
|
862
|
+
semanticJudge: {
|
|
863
|
+
attempted: true,
|
|
864
|
+
skipped: false,
|
|
865
|
+
skipReason: null,
|
|
866
|
+
},
|
|
867
|
+
notes: [`Provider call failed: ${providerErrorMessage}`, ...deterministicVisualSummary.notes],
|
|
517
868
|
passed: true,
|
|
518
869
|
}));
|
|
519
870
|
return;
|
|
@@ -537,11 +888,18 @@ async function main() {
|
|
|
537
888
|
alignmentScore,
|
|
538
889
|
driftCount: findings.length,
|
|
539
890
|
blockingCandidateCount,
|
|
891
|
+
meaningfulDiffViewportCount: deterministicVisualSummary.meaningfulDiffViewports.length,
|
|
892
|
+
},
|
|
893
|
+
deterministicVisual: deterministicVisualSummary,
|
|
894
|
+
semanticJudge: {
|
|
895
|
+
attempted: true,
|
|
896
|
+
skipped: false,
|
|
897
|
+
skipReason: null,
|
|
540
898
|
},
|
|
541
899
|
findings,
|
|
542
900
|
notes: malformed
|
|
543
|
-
? ['LLM response was malformed. Advisory mode kept the audit non-blocking.']
|
|
544
|
-
: notes,
|
|
901
|
+
? ['LLM response was malformed. Advisory mode kept the audit non-blocking.', ...deterministicVisualSummary.notes]
|
|
902
|
+
: [...notes, ...deterministicVisualSummary.notes],
|
|
545
903
|
});
|
|
546
904
|
|
|
547
905
|
emitMachineReadableReport(reportPayload);
|