@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryuenn3123/agentic-senior-core",
3
- "version": "3.0.16",
3
+ "version": "3.0.17",
4
4
  "type": "module",
5
5
  "description": "Force your AI Agent to code like a Staff Engineer, not a Junior.",
6
6
  "bin": {
@@ -53,7 +53,10 @@ const BOUNDARY_RULES = [
53
53
  ],
54
54
  trigger(filePath) {
55
55
  return !isDocumentationFilePath(filePath)
56
- && /(api|openapi|contract|controller|route|endpoint)/i.test(filePath);
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 = [
@@ -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.0',
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(designIntentContent, designGuideContent, rawDiff, changedUiFiles);
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
- notes: ['No LLM provider configured. UI design judge skipped provider review and stayed advisory.'],
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
- notes: [`Provider call failed: ${providerErrorMessage}`],
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);