@ryuenn3123/agentic-senior-core 3.0.16 → 3.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.agent-context/prompts/bootstrap-design.md +31 -4
  2. package/.agent-context/rules/frontend-architecture.md +26 -0
  3. package/.agent-context/state/memory-continuity-benchmark.json +1 -1
  4. package/.cursorrules +1 -1
  5. package/.gemini/instructions.md +7 -1
  6. package/.github/copilot-instructions.md +7 -1
  7. package/.instructions.md +3 -0
  8. package/.windsurfrules +1 -1
  9. package/AGENTS.md +13 -1
  10. package/lib/cli/commands/init.mjs +2 -2
  11. package/lib/cli/memory-continuity.mjs +2 -1
  12. package/lib/cli/project-scaffolder/constants.mjs +1 -0
  13. package/lib/cli/project-scaffolder/design-contract.mjs +523 -171
  14. package/lib/cli/project-scaffolder/prompt-builders.mjs +38 -15
  15. package/lib/cli/project-scaffolder/storage.mjs +0 -2
  16. package/package.json +2 -2
  17. package/scripts/documentation-boundary-audit.mjs +5 -2
  18. package/scripts/frontend-usability-audit.mjs +34 -0
  19. package/scripts/mcp-server/constants.mjs +60 -0
  20. package/scripts/mcp-server/tool-registry.mjs +149 -0
  21. package/scripts/mcp-server/tools.mjs +446 -0
  22. package/scripts/mcp-server.mjs +23 -661
  23. package/scripts/release-gate/audit-checks.mjs +426 -0
  24. package/scripts/release-gate/constants.mjs +53 -0
  25. package/scripts/release-gate/runtime.mjs +63 -0
  26. package/scripts/release-gate/static-checks.mjs +182 -0
  27. package/scripts/release-gate.mjs +12 -771
  28. package/scripts/sync-thin-adapters.mjs +24 -0
  29. package/scripts/ui-design-judge/constants.mjs +24 -0
  30. package/scripts/ui-design-judge/design-execution-summary.mjs +233 -0
  31. package/scripts/ui-design-judge/git-input.mjs +131 -0
  32. package/scripts/ui-design-judge/prompting.mjs +73 -0
  33. package/scripts/ui-design-judge/providers.mjs +102 -0
  34. package/scripts/ui-design-judge/reporting.mjs +181 -0
  35. package/scripts/ui-design-judge/rubric-calibration.mjs +211 -0
  36. package/scripts/ui-design-judge/rubric-goldset.json +188 -0
  37. package/scripts/ui-design-judge.mjs +130 -441
  38. package/scripts/ui-rubric-calibration.mjs +35 -0
  39. package/scripts/validate/config.mjs +98 -0
@@ -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.
@@ -0,0 +1,24 @@
1
+ // @ts-check
2
+
3
+ import { resolve, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ export const REPOSITORY_ROOT = resolve(__dirname, '..', '..');
10
+ export const DESIGN_INTENT_PATH = resolve(REPOSITORY_ROOT, 'docs', 'design-intent.json');
11
+ export const DESIGN_GUIDE_PATH = resolve(REPOSITORY_ROOT, 'docs', 'DESIGN.md');
12
+ export const MAX_DIFF_CHARS = 12000;
13
+ export const UI_FILE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.vue', '.css', '.scss', '.sass']);
14
+ export const DESIGN_EXECUTION_REQUIRED_CAPABILITIES = [
15
+ 'requireSurfacePlan',
16
+ 'requireComponentGraph',
17
+ 'requireViewportMutationPlan',
18
+ 'requireInteractionStateMatrix',
19
+ 'requireContentPriorityMap',
20
+ 'requireTaskFlowNarrative',
21
+ 'requireSignatureMoveRationale',
22
+ 'requireStructuredHandoff',
23
+ 'requireRepoEvidenceAlignment',
24
+ ];
@@ -0,0 +1,233 @@
1
+ // @ts-check
2
+
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import {
5
+ DESIGN_EXECUTION_REQUIRED_CAPABILITIES,
6
+ DESIGN_GUIDE_PATH,
7
+ DESIGN_INTENT_PATH,
8
+ } from './constants.mjs';
9
+
10
+ export function normalizeStringArray(rawValue) {
11
+ if (!Array.isArray(rawValue)) {
12
+ return [];
13
+ }
14
+
15
+ return rawValue
16
+ .map((entryValue) => String(entryValue || '').trim())
17
+ .filter(Boolean);
18
+ }
19
+
20
+ export function loadDesignIntent() {
21
+ if (!existsSync(DESIGN_INTENT_PATH)) {
22
+ return null;
23
+ }
24
+
25
+ try {
26
+ return JSON.parse(readFileSync(DESIGN_INTENT_PATH, 'utf8'));
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ export function loadDesignGuide() {
33
+ if (!existsSync(DESIGN_GUIDE_PATH)) {
34
+ return '';
35
+ }
36
+
37
+ return readFileSync(DESIGN_GUIDE_PATH, 'utf8');
38
+ }
39
+
40
+ function hasRepoEvidenceSummary(designIntentContent) {
41
+ return Boolean(
42
+ designIntentContent?.repoEvidence?.designEvidenceSummary
43
+ && typeof designIntentContent.repoEvidence.designEvidenceSummary === 'object'
44
+ );
45
+ }
46
+
47
+ function summarizeDesignExecutionHandoff(designIntentContent) {
48
+ const designExecutionHandoff = designIntentContent?.designExecutionHandoff
49
+ && typeof designIntentContent.designExecutionHandoff === 'object'
50
+ ? designIntentContent.designExecutionHandoff
51
+ : {};
52
+
53
+ const surfacePlan = Array.isArray(designExecutionHandoff.surfacePlan)
54
+ ? designExecutionHandoff.surfacePlan
55
+ : [];
56
+ const componentGraphNodes = Array.isArray(designExecutionHandoff.componentGraph?.nodes)
57
+ ? designExecutionHandoff.componentGraph.nodes
58
+ : [];
59
+ const componentGraphEdges = Array.isArray(designExecutionHandoff.componentGraph?.edges)
60
+ ? designExecutionHandoff.componentGraph.edges
61
+ : [];
62
+ const interactionStateMatrix = Array.isArray(designExecutionHandoff.interactionStateMatrix)
63
+ ? designExecutionHandoff.interactionStateMatrix
64
+ : [];
65
+ const taskFlowNarrative = normalizeStringArray(designExecutionHandoff.taskFlowNarrative);
66
+ const contentPriorityMap = designExecutionHandoff.contentPriorityMap
67
+ && typeof designExecutionHandoff.contentPriorityMap === 'object'
68
+ ? designExecutionHandoff.contentPriorityMap
69
+ : {};
70
+ const viewportMutationPlan = designExecutionHandoff.viewportMutationPlan
71
+ && typeof designExecutionHandoff.viewportMutationPlan === 'object'
72
+ ? designExecutionHandoff.viewportMutationPlan
73
+ : {};
74
+
75
+ const artifactChecks = [
76
+ { name: 'surfacePlan', present: surfacePlan.length > 0 },
77
+ { name: 'componentGraphNodes', present: componentGraphNodes.length > 1 },
78
+ { name: 'componentGraphEdges', present: componentGraphEdges.length > 0 },
79
+ {
80
+ name: 'contentPriorityMap',
81
+ present: ['primary', 'secondary', 'deferred'].every((bucketKey) => Array.isArray(contentPriorityMap?.[bucketKey]) && contentPriorityMap[bucketKey].length > 0),
82
+ },
83
+ {
84
+ name: 'viewportMutationPlan',
85
+ present: ['mobile', 'tablet', 'desktop'].every((viewportKey) => String(viewportMutationPlan?.[viewportKey] || '').trim().length > 0),
86
+ },
87
+ { name: 'interactionStateMatrix', present: interactionStateMatrix.length > 0 },
88
+ { name: 'taskFlowNarrative', present: taskFlowNarrative.length > 1 },
89
+ { name: 'signatureMoveRationale', present: String(designExecutionHandoff.signatureMoveRationale || '').trim().length > 0 },
90
+ ];
91
+
92
+ const presentArtifacts = artifactChecks.filter((artifactCheck) => artifactCheck.present).map((artifactCheck) => artifactCheck.name);
93
+ const missingArtifacts = artifactChecks.filter((artifactCheck) => !artifactCheck.present).map((artifactCheck) => artifactCheck.name);
94
+ const implementationGuardrails = designExecutionHandoff.implementationGuardrails
95
+ && typeof designExecutionHandoff.implementationGuardrails === 'object'
96
+ ? designExecutionHandoff.implementationGuardrails
97
+ : {};
98
+
99
+ return {
100
+ present: Object.keys(designExecutionHandoff).length > 0,
101
+ version: typeof designExecutionHandoff.version === 'string' ? designExecutionHandoff.version : null,
102
+ handoffReady: (typeof designExecutionHandoff.version === 'string' && designExecutionHandoff.version === 'ui-handoff-v1')
103
+ && missingArtifacts.length === 0
104
+ && implementationGuardrails.requireBuildFromHandoff === true
105
+ && implementationGuardrails.requireGapNotesBeforeFallback === true
106
+ && implementationGuardrails.forbidGenericLayoutFallbackWithoutReason === true,
107
+ artifactCount: presentArtifacts.length,
108
+ presentArtifacts,
109
+ missingArtifacts,
110
+ };
111
+ }
112
+
113
+ export function summarizeDesignExecutionPolicy(designIntentContent) {
114
+ const designExecutionPolicy = designIntentContent?.designExecutionPolicy
115
+ && typeof designIntentContent.designExecutionPolicy === 'object'
116
+ ? designIntentContent.designExecutionPolicy
117
+ : {};
118
+
119
+ const requiredCapabilities = DESIGN_EXECUTION_REQUIRED_CAPABILITIES.map((capability) => ({
120
+ name: capability,
121
+ enabled: designExecutionPolicy[capability] === true,
122
+ }));
123
+ const enabledCapabilities = requiredCapabilities
124
+ .filter((capability) => capability.enabled)
125
+ .map((capability) => capability.name);
126
+ const missingCapabilities = requiredCapabilities
127
+ .filter((capability) => !capability.enabled)
128
+ .map((capability) => capability.name);
129
+ const semanticReviewFocus = normalizeStringArray(designExecutionPolicy.semanticReviewFocus);
130
+ const representationStrategy = typeof designExecutionPolicy.representationStrategy === 'string'
131
+ ? designExecutionPolicy.representationStrategy
132
+ : null;
133
+ const repoEvidenceAvailable = hasRepoEvidenceSummary(designIntentContent);
134
+ const screenshotDependencyForbidden = designExecutionPolicy.forbidScreenshotDependency === true;
135
+ const handoffFormatVersion = typeof designExecutionPolicy.handoffFormatVersion === 'string'
136
+ ? designExecutionPolicy.handoffFormatVersion
137
+ : null;
138
+ const handoffSummary = summarizeDesignExecutionHandoff(designIntentContent);
139
+ const policyPresent = Object.keys(designExecutionPolicy).length > 0;
140
+ const contractReady = policyPresent
141
+ && representationStrategy === 'surface-plan-v1'
142
+ && handoffFormatVersion === 'ui-handoff-v1'
143
+ && missingCapabilities.length === 0
144
+ && semanticReviewFocus.length >= 4
145
+ && screenshotDependencyForbidden
146
+ && handoffSummary.handoffReady
147
+ && repoEvidenceAvailable;
148
+
149
+ const notes = [];
150
+ if (!policyPresent) {
151
+ notes.push('designExecutionPolicy is missing from docs/design-intent.json.');
152
+ }
153
+ if (representationStrategy !== 'surface-plan-v1') {
154
+ notes.push('Structured design execution should declare representationStrategy "surface-plan-v1".');
155
+ }
156
+ if (handoffFormatVersion !== 'ui-handoff-v1') {
157
+ notes.push('Structured design execution should declare handoffFormatVersion "ui-handoff-v1".');
158
+ }
159
+ if (missingCapabilities.length > 0) {
160
+ notes.push(`Structured design execution is missing required capabilities: ${missingCapabilities.join(', ')}.`);
161
+ }
162
+ if (semanticReviewFocus.length < 4) {
163
+ notes.push('Structured design execution should declare semantic review focus dimensions before UI implementation review.');
164
+ }
165
+ if (!screenshotDependencyForbidden) {
166
+ notes.push('Structured design execution must explicitly forbid screenshot dependency as a baseline requirement.');
167
+ }
168
+ if (!handoffSummary.handoffReady) {
169
+ notes.push(`Structured design handoff is incomplete: ${handoffSummary.missingArtifacts.join(', ') || 'missing or invalid handoff metadata'}.`);
170
+ }
171
+ if (!repoEvidenceAvailable) {
172
+ notes.push('repoEvidence.designEvidenceSummary is missing or unreadable.');
173
+ }
174
+ if (notes.length === 0) {
175
+ notes.push('Structured design execution policy is present and ready for contract review.');
176
+ }
177
+
178
+ return {
179
+ policyPresent,
180
+ representationStrategy,
181
+ contractReady,
182
+ screenshotDependencyForbidden,
183
+ repoEvidenceAvailable,
184
+ handoffPresent: handoffSummary.present,
185
+ handoffVersion: handoffSummary.version,
186
+ handoffReady: handoffSummary.handoffReady,
187
+ handoffArtifactCount: handoffSummary.artifactCount,
188
+ presentHandoffArtifacts: handoffSummary.presentArtifacts,
189
+ missingHandoffArtifacts: handoffSummary.missingArtifacts,
190
+ repoEvidenceSummaryVersion: repoEvidenceAvailable
191
+ ? String(designIntentContent.repoEvidence.designEvidenceSummary.summaryVersion || '')
192
+ : null,
193
+ requiredCapabilities: requiredCapabilities.map((capability) => capability.name),
194
+ enabledCapabilities,
195
+ missingCapabilities,
196
+ semanticReviewFocus,
197
+ notes,
198
+ };
199
+ }
200
+
201
+ export function summarizeReviewRubric(designIntentContent) {
202
+ const reviewRubric = designIntentContent?.reviewRubric && typeof designIntentContent.reviewRubric === 'object'
203
+ ? designIntentContent.reviewRubric
204
+ : {};
205
+
206
+ const dimensions = Array.isArray(reviewRubric.dimensions)
207
+ ? reviewRubric.dimensions
208
+ .map((dimension) => ({
209
+ key: String(dimension?.key || '').trim(),
210
+ blockingByDefault: dimension?.blockingByDefault === true,
211
+ question: String(dimension?.question || '').trim(),
212
+ }))
213
+ .filter((dimension) => Boolean(dimension.key))
214
+ : [];
215
+
216
+ return {
217
+ version: typeof reviewRubric.version === 'string' ? reviewRubric.version : null,
218
+ dimensions,
219
+ genericitySignals: normalizeStringArray(reviewRubric.genericitySignals),
220
+ validBoldSignals: normalizeStringArray(reviewRubric.validBoldSignals),
221
+ reportingRules: reviewRubric.reportingRules && typeof reviewRubric.reportingRules === 'object'
222
+ ? {
223
+ mustExplainGenericity: reviewRubric.reportingRules.mustExplainGenericity === true,
224
+ mustSeparateTasteFromFailure: reviewRubric.reportingRules.mustSeparateTasteFromFailure === true,
225
+ contractFidelityOverridesPersonalTaste: reviewRubric.reportingRules.contractFidelityOverridesPersonalTaste === true,
226
+ }
227
+ : {
228
+ mustExplainGenericity: false,
229
+ mustSeparateTasteFromFailure: false,
230
+ contractFidelityOverridesPersonalTaste: false,
231
+ },
232
+ };
233
+ }
@@ -0,0 +1,131 @@
1
+ // @ts-check
2
+
3
+ import { execSync } from 'node:child_process';
4
+ import { extname } from 'node:path';
5
+ import { REPOSITORY_ROOT, UI_FILE_EXTENSIONS } from './constants.mjs';
6
+
7
+ export function detectCiProvider() {
8
+ if (process.env.GITHUB_ACTIONS === 'true') {
9
+ return 'github';
10
+ }
11
+
12
+ if (process.env.GITLAB_CI === 'true') {
13
+ return 'gitlab';
14
+ }
15
+
16
+ return 'local';
17
+ }
18
+
19
+ function collectGitDiff(baseSha, headSha) {
20
+ const execOptions = {
21
+ cwd: REPOSITORY_ROOT,
22
+ encoding: /** @type {'utf-8'} */ ('utf-8'),
23
+ maxBuffer: 1024 * 1024 * 8,
24
+ };
25
+
26
+ return execSync(`git diff "${baseSha}...${headSha}"`, execOptions);
27
+ }
28
+
29
+ function collectGitChangedFiles(baseSha, headSha) {
30
+ const execOptions = {
31
+ cwd: REPOSITORY_ROOT,
32
+ encoding: /** @type {'utf-8'} */ ('utf-8'),
33
+ maxBuffer: 1024 * 1024 * 2,
34
+ };
35
+
36
+ const output = execSync(`git diff --name-only "${baseSha}...${headSha}"`, execOptions);
37
+ return output
38
+ .split(/\r?\n/u)
39
+ .map((filePath) => filePath.trim())
40
+ .filter(Boolean);
41
+ }
42
+
43
+ export function collectPullRequestDiff() {
44
+ if (process.env.PR_DIFF) {
45
+ return process.env.PR_DIFF;
46
+ }
47
+
48
+ const githubBaseSha = process.env.GITHUB_BASE_SHA;
49
+ const githubHeadSha = process.env.GITHUB_HEAD_SHA ?? 'HEAD';
50
+ if (githubBaseSha) {
51
+ return collectGitDiff(githubBaseSha, githubHeadSha);
52
+ }
53
+
54
+ const gitlabBaseSha = process.env.CI_MERGE_REQUEST_DIFF_BASE_SHA;
55
+ const gitlabHeadSha = process.env.CI_COMMIT_SHA ?? 'HEAD';
56
+ if (gitlabBaseSha) {
57
+ return collectGitDiff(gitlabBaseSha, gitlabHeadSha);
58
+ }
59
+
60
+ try {
61
+ return execSync('git diff HEAD~1 HEAD', {
62
+ cwd: REPOSITORY_ROOT,
63
+ encoding: /** @type {'utf-8'} */ ('utf-8'),
64
+ maxBuffer: 1024 * 1024 * 8,
65
+ });
66
+ } catch {
67
+ try {
68
+ const emptyTreeSha = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
69
+ return execSync(`git diff "${emptyTreeSha}" HEAD`, {
70
+ cwd: REPOSITORY_ROOT,
71
+ encoding: /** @type {'utf-8'} */ ('utf-8'),
72
+ maxBuffer: 1024 * 1024 * 8,
73
+ });
74
+ } catch {
75
+ return '';
76
+ }
77
+ }
78
+ }
79
+
80
+ export function collectChangedFiles() {
81
+ if (process.env.PR_DIFF) {
82
+ const filePathSet = new Set();
83
+ for (const diffHeaderMatch of process.env.PR_DIFF.matchAll(/^diff --git a\/(.+?) b\/(.+)$/gm)) {
84
+ filePathSet.add(diffHeaderMatch[2]);
85
+ }
86
+ return Array.from(filePathSet);
87
+ }
88
+
89
+ const githubBaseSha = process.env.GITHUB_BASE_SHA;
90
+ const githubHeadSha = process.env.GITHUB_HEAD_SHA ?? 'HEAD';
91
+ if (githubBaseSha) {
92
+ return collectGitChangedFiles(githubBaseSha, githubHeadSha);
93
+ }
94
+
95
+ const gitlabBaseSha = process.env.CI_MERGE_REQUEST_DIFF_BASE_SHA;
96
+ const gitlabHeadSha = process.env.CI_COMMIT_SHA ?? 'HEAD';
97
+ if (gitlabBaseSha) {
98
+ return collectGitChangedFiles(gitlabBaseSha, gitlabHeadSha);
99
+ }
100
+
101
+ try {
102
+ const output = execSync('git diff --name-only HEAD~1 HEAD', {
103
+ cwd: REPOSITORY_ROOT,
104
+ encoding: /** @type {'utf-8'} */ ('utf-8'),
105
+ maxBuffer: 1024 * 1024 * 2,
106
+ });
107
+ return output.split(/\r?\n/u).map((filePath) => filePath.trim()).filter(Boolean);
108
+ } catch {
109
+ return [];
110
+ }
111
+ }
112
+
113
+ export function isUiRelevantFilePath(filePath) {
114
+ const normalizedFilePath = String(filePath || '').replace(/\\/g, '/').toLowerCase();
115
+ const fileExtension = extname(normalizedFilePath);
116
+
117
+ if (!UI_FILE_EXTENSIONS.has(fileExtension)) {
118
+ return false;
119
+ }
120
+
121
+ return (
122
+ normalizedFilePath.startsWith('src/')
123
+ || normalizedFilePath.startsWith('app/')
124
+ || normalizedFilePath.startsWith('pages/')
125
+ || normalizedFilePath.startsWith('components/')
126
+ || normalizedFilePath.startsWith('styles/')
127
+ || normalizedFilePath.includes('/components/')
128
+ || normalizedFilePath.includes('/screens/')
129
+ || normalizedFilePath.includes('/layouts/')
130
+ );
131
+ }
@@ -0,0 +1,73 @@
1
+ // @ts-check
2
+
3
+ import { MAX_DIFF_CHARS } from './constants.mjs';
4
+
5
+ export function buildSystemPrompt() {
6
+ return [
7
+ 'You are a Principal UI/UX Design Reviewer.',
8
+ 'Compare the changed UI code against the provided design contract.',
9
+ 'Treat docs/design-intent.json as the machine-readable source of truth.',
10
+ 'Treat docs/DESIGN.md as explanatory context, not a generic style guide.',
11
+ 'Treat designExecutionPolicy as the execution contract for how the UI should be planned, structured, and reviewed.',
12
+ 'Treat designExecutionHandoff as the explicit bridge between design intent and implementation decisions.',
13
+ 'Treat reviewRubric as the stable scoring frame for distinctiveness, contract fidelity, visual consistency, heuristic UX quality, and motion discipline.',
14
+ 'Use repoEvidence.designEvidenceSummary as implementation evidence when deciding whether the diff follows the intended system.',
15
+ 'Do not reward generic SaaS defaults or popular template patterns.',
16
+ 'Do not penalize originality when the implementation still aligns with the contract.',
17
+ '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.',
18
+ 'Only flag drift when there is a clear mismatch with the contract, accessibility non-negotiables, or cross-viewport adaptation rules.',
19
+ 'Treat WCAG 2.2 AA failures as hard accessibility drift.',
20
+ '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.',
21
+ 'Check focus visibility, focus appearance, target size, keyboard access, accessible authentication, and status or dynamic state access when the diff touches those surfaces.',
22
+ 'This audit always runs in advisory mode for this repository workflow.',
23
+ 'Focus on color intent, typographic hierarchy, responsive re-layout, purposeful motion, component morphology across states, interaction behavior, and genericity drift.',
24
+ 'If you call something generic, explain the specific genericity signal or anti-pattern that caused that judgment.',
25
+ 'Separate taste from failure. A bold design that follows the contract should not be penalized only because it is unusual.',
26
+ 'Return ONLY one JSON object on a single line prefixed with JSON_VERDICT:.',
27
+ 'Schema:',
28
+ '{"alignmentScore": number|null, "genericityAssessment": {"status": "distinctive|mixed|generic|unclear", "reason": string}, "tasteVsFailureSeparated": boolean, "rubricBreakdown": [{"dimension": string, "score": number|null, "verdict": "strong|acceptable|weak|unclear", "reason": string, "blocking": boolean}], "notes": string[], "findings": [{"area": string, "severity": "high|medium|low", "problem": string, "evidence": string, "recommendation": string, "blockingRecommended": boolean}]}',
29
+ ].join('\n');
30
+ }
31
+
32
+ export function buildUserMessage(designIntentContent, designGuideContent, diffContent, changedUiFiles, designExecutionSummary) {
33
+ const truncatedDiff = diffContent.length > MAX_DIFF_CHARS
34
+ ? `${diffContent.slice(0, MAX_DIFF_CHARS)}\n\n[DIFF TRUNCATED - ${diffContent.length - MAX_DIFF_CHARS} additional characters omitted]`
35
+ : diffContent;
36
+
37
+ return [
38
+ '## Changed UI Files',
39
+ changedUiFiles.length > 0 ? changedUiFiles.map((filePath) => `- ${filePath}`).join('\n') : '- none',
40
+ '',
41
+ '## design-intent.json',
42
+ '```json',
43
+ JSON.stringify(designIntentContent, null, 2),
44
+ '```',
45
+ '',
46
+ '## Review Rubric',
47
+ '```json',
48
+ JSON.stringify(designIntentContent?.reviewRubric || null, null, 2),
49
+ '```',
50
+ '',
51
+ '## Structured Design Handoff',
52
+ '```json',
53
+ JSON.stringify(designIntentContent?.designExecutionHandoff || null, null, 2),
54
+ '```',
55
+ '',
56
+ '## DESIGN.md',
57
+ '```md',
58
+ designGuideContent.trim() || '(missing DESIGN.md)',
59
+ '```',
60
+ '',
61
+ '## Structured Design Execution Summary',
62
+ '```json',
63
+ JSON.stringify(designExecutionSummary, null, 2),
64
+ '```',
65
+ '',
66
+ '## UI Diff',
67
+ '```diff',
68
+ truncatedDiff.trim() || '(no UI diff)',
69
+ '```',
70
+ '',
71
+ 'Judge alignment to the contract. Avoid aesthetic bias toward generic web trends or toward motionless/static outputs.',
72
+ ].join('\n');
73
+ }
@@ -0,0 +1,102 @@
1
+ // @ts-check
2
+
3
+ async function callOpenAiProvider(systemPrompt, userMessage) {
4
+ const selectedModel = process.env.LLM_JUDGE_MODEL ?? 'gpt-4o-mini';
5
+ const apiResponse = await fetch('https://api.openai.com/v1/chat/completions', {
6
+ method: 'POST',
7
+ headers: {
8
+ 'Content-Type': 'application/json',
9
+ Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
10
+ },
11
+ body: JSON.stringify({
12
+ model: selectedModel,
13
+ max_tokens: 2048,
14
+ temperature: 0,
15
+ messages: [
16
+ { role: 'system', content: systemPrompt },
17
+ { role: 'user', content: userMessage },
18
+ ],
19
+ }),
20
+ });
21
+
22
+ if (!apiResponse.ok) {
23
+ const errorBody = await apiResponse.text();
24
+ throw new Error(`OpenAI API returned ${apiResponse.status}: ${errorBody}`);
25
+ }
26
+
27
+ const responsePayload = await apiResponse.json();
28
+ return responsePayload.choices[0].message.content;
29
+ }
30
+
31
+ async function callAnthropicProvider(systemPrompt, userMessage) {
32
+ const selectedModel = process.env.LLM_JUDGE_MODEL ?? 'claude-3-5-haiku-latest';
33
+ const apiResponse = await fetch('https://api.anthropic.com/v1/messages', {
34
+ method: 'POST',
35
+ headers: {
36
+ 'Content-Type': 'application/json',
37
+ 'x-api-key': process.env.ANTHROPIC_API_KEY ?? '',
38
+ 'anthropic-version': '2023-06-01',
39
+ },
40
+ body: JSON.stringify({
41
+ model: selectedModel,
42
+ max_tokens: 2048,
43
+ system: systemPrompt,
44
+ messages: [{ role: 'user', content: userMessage }],
45
+ }),
46
+ });
47
+
48
+ if (!apiResponse.ok) {
49
+ const errorBody = await apiResponse.text();
50
+ throw new Error(`Anthropic API returned ${apiResponse.status}: ${errorBody}`);
51
+ }
52
+
53
+ const responsePayload = await apiResponse.json();
54
+ return responsePayload.content[0].text;
55
+ }
56
+
57
+ async function callGeminiProvider(systemPrompt, userMessage) {
58
+ const selectedModel = process.env.LLM_JUDGE_MODEL ?? 'gemini-2.0-flash';
59
+ const apiKey = process.env.GEMINI_API_KEY ?? '';
60
+ const endpointUrl = `https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${apiKey}`;
61
+
62
+ const apiResponse = await fetch(endpointUrl, {
63
+ method: 'POST',
64
+ headers: { 'Content-Type': 'application/json' },
65
+ body: JSON.stringify({
66
+ system_instruction: { parts: [{ text: systemPrompt }] },
67
+ contents: [{ role: 'user', parts: [{ text: userMessage }] }],
68
+ generationConfig: { temperature: 0, maxOutputTokens: 2048 },
69
+ }),
70
+ });
71
+
72
+ if (!apiResponse.ok) {
73
+ const errorBody = await apiResponse.text();
74
+ throw new Error(`Gemini API returned ${apiResponse.status}: ${errorBody}`);
75
+ }
76
+
77
+ const responsePayload = await apiResponse.json();
78
+ return responsePayload.candidates[0].content.parts[0].text;
79
+ }
80
+
81
+ export function selectAvailableProvider() {
82
+ if (process.env.UI_DESIGN_JUDGE_MOCK_RESPONSE) {
83
+ return {
84
+ providerName: 'mock',
85
+ invokeProvider: async () => process.env.UI_DESIGN_JUDGE_MOCK_RESPONSE,
86
+ };
87
+ }
88
+
89
+ if (process.env.OPENAI_API_KEY) {
90
+ return { providerName: 'openai', invokeProvider: callOpenAiProvider };
91
+ }
92
+
93
+ if (process.env.ANTHROPIC_API_KEY) {
94
+ return { providerName: 'anthropic', invokeProvider: callAnthropicProvider };
95
+ }
96
+
97
+ if (process.env.GEMINI_API_KEY) {
98
+ return { providerName: 'gemini', invokeProvider: callGeminiProvider };
99
+ }
100
+
101
+ return null;
102
+ }