@ryuenn3123/agentic-senior-core 3.0.9 → 3.0.11
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 +27 -12
- package/.agent-context/prompts/init-project.md +7 -0
- package/.agent-context/rules/frontend-architecture.md +1 -0
- package/.agent-context/state/memory-continuity-benchmark.json +1 -1
- package/.cursorrules +1 -1
- package/.gemini/instructions.md +1 -1
- package/.github/copilot-instructions.md +1 -1
- package/.windsurfrules +1 -1
- package/AGENTS.md +1 -1
- package/lib/cli/commands/init.mjs +4 -0
- package/lib/cli/commands/upgrade.mjs +4 -0
- package/lib/cli/compiler.mjs +15 -0
- package/lib/cli/detector.mjs +118 -2
- package/lib/cli/project-scaffolder.mjs +225 -30
- package/package.json +1 -1
- package/scripts/frontend-usability-audit.mjs +55 -0
- package/scripts/release-gate.mjs +33 -0
- package/scripts/ui-design-judge.mjs +560 -0
- package/scripts/validate.mjs +81 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ui-design-judge.mjs
|
|
6
|
+
*
|
|
7
|
+
* Advisory-first UI design contract judge.
|
|
8
|
+
*
|
|
9
|
+
* Repo-internal workflow audit; no user-facing runtime modes.
|
|
10
|
+
* Compares changed UI diffs against docs/design-intent.json and docs/DESIGN.md.
|
|
11
|
+
* Runs only in advisory mode for this repository workflow.
|
|
12
|
+
* Emits JSON to stdout for release-gate and CI consumption.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execSync } from 'node:child_process';
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { resolve, dirname, extname } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = dirname(__filename);
|
|
22
|
+
const REPOSITORY_ROOT = resolve(__dirname, '..');
|
|
23
|
+
|
|
24
|
+
const DESIGN_INTENT_PATH = resolve(REPOSITORY_ROOT, 'docs', 'design-intent.json');
|
|
25
|
+
const DESIGN_GUIDE_PATH = resolve(REPOSITORY_ROOT, 'docs', 'DESIGN.md');
|
|
26
|
+
const MAX_DIFF_CHARS = 12000;
|
|
27
|
+
const UI_FILE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.vue', '.css', '.scss', '.sass']);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {{
|
|
31
|
+
* area: string,
|
|
32
|
+
* severity: string,
|
|
33
|
+
* problem: string,
|
|
34
|
+
* evidence: string,
|
|
35
|
+
* recommendation: string,
|
|
36
|
+
* blockingRecommended: boolean,
|
|
37
|
+
* }} DriftFinding
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {{
|
|
42
|
+
* generatedAt: string,
|
|
43
|
+
* auditName: string,
|
|
44
|
+
* schemaVersion: string,
|
|
45
|
+
* mode: 'advisory',
|
|
46
|
+
* advisoryOnly: boolean,
|
|
47
|
+
* passed: boolean,
|
|
48
|
+
* skipped: boolean,
|
|
49
|
+
* skipReason: string | null,
|
|
50
|
+
* provider: string,
|
|
51
|
+
* ciProvider: string,
|
|
52
|
+
* contractPresent: boolean,
|
|
53
|
+
* summary: {
|
|
54
|
+
* changedUiFileCount: number,
|
|
55
|
+
* alignmentScore: number | null,
|
|
56
|
+
* driftCount: number,
|
|
57
|
+
* blockingCandidateCount: number,
|
|
58
|
+
* },
|
|
59
|
+
* malformedVerdict: boolean,
|
|
60
|
+
* providerError: boolean,
|
|
61
|
+
* findings: DriftFinding[],
|
|
62
|
+
* notes: string[],
|
|
63
|
+
* }} UiDesignJudgeReport
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
function detectCiProvider() {
|
|
67
|
+
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
68
|
+
return 'github';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (process.env.GITLAB_CI === 'true') {
|
|
72
|
+
return 'gitlab';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return 'local';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeSeverity(rawSeverityValue) {
|
|
79
|
+
const normalizedSeverityValue = String(rawSeverityValue || '').trim().toLowerCase();
|
|
80
|
+
|
|
81
|
+
if (['critical', 'high', 'medium', 'low'].includes(normalizedSeverityValue)) {
|
|
82
|
+
return normalizedSeverityValue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (normalizedSeverityValue === 'major') {
|
|
86
|
+
return 'high';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (normalizedSeverityValue === 'minor' || normalizedSeverityValue === 'info') {
|
|
90
|
+
return 'low';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return 'low';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function collectGitDiff(baseSha, headSha) {
|
|
97
|
+
const execOptions = {
|
|
98
|
+
cwd: REPOSITORY_ROOT,
|
|
99
|
+
encoding: /** @type {'utf-8'} */ ('utf-8'),
|
|
100
|
+
maxBuffer: 1024 * 1024 * 8,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return execSync(`git diff "${baseSha}...${headSha}"`, execOptions);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function collectGitChangedFiles(baseSha, headSha) {
|
|
107
|
+
const execOptions = {
|
|
108
|
+
cwd: REPOSITORY_ROOT,
|
|
109
|
+
encoding: /** @type {'utf-8'} */ ('utf-8'),
|
|
110
|
+
maxBuffer: 1024 * 1024 * 2,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const output = execSync(`git diff --name-only "${baseSha}...${headSha}"`, execOptions);
|
|
114
|
+
return output
|
|
115
|
+
.split(/\r?\n/u)
|
|
116
|
+
.map((filePath) => filePath.trim())
|
|
117
|
+
.filter(Boolean);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function collectPullRequestDiff() {
|
|
121
|
+
if (process.env.PR_DIFF) {
|
|
122
|
+
return process.env.PR_DIFF;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const githubBaseSha = process.env.GITHUB_BASE_SHA;
|
|
126
|
+
const githubHeadSha = process.env.GITHUB_HEAD_SHA ?? 'HEAD';
|
|
127
|
+
if (githubBaseSha) {
|
|
128
|
+
return collectGitDiff(githubBaseSha, githubHeadSha);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const gitlabBaseSha = process.env.CI_MERGE_REQUEST_DIFF_BASE_SHA;
|
|
132
|
+
const gitlabHeadSha = process.env.CI_COMMIT_SHA ?? 'HEAD';
|
|
133
|
+
if (gitlabBaseSha) {
|
|
134
|
+
return collectGitDiff(gitlabBaseSha, gitlabHeadSha);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
return execSync('git diff HEAD~1 HEAD', {
|
|
139
|
+
cwd: REPOSITORY_ROOT,
|
|
140
|
+
encoding: /** @type {'utf-8'} */ ('utf-8'),
|
|
141
|
+
maxBuffer: 1024 * 1024 * 8,
|
|
142
|
+
});
|
|
143
|
+
} catch {
|
|
144
|
+
try {
|
|
145
|
+
const emptyTreeSha = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
146
|
+
return execSync(`git diff "${emptyTreeSha}" HEAD`, {
|
|
147
|
+
cwd: REPOSITORY_ROOT,
|
|
148
|
+
encoding: /** @type {'utf-8'} */ ('utf-8'),
|
|
149
|
+
maxBuffer: 1024 * 1024 * 8,
|
|
150
|
+
});
|
|
151
|
+
} catch {
|
|
152
|
+
return '';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function collectChangedFiles() {
|
|
158
|
+
if (process.env.PR_DIFF) {
|
|
159
|
+
const filePathSet = new Set();
|
|
160
|
+
for (const diffHeaderMatch of process.env.PR_DIFF.matchAll(/^diff --git a\/(.+?) b\/(.+)$/gm)) {
|
|
161
|
+
filePathSet.add(diffHeaderMatch[2]);
|
|
162
|
+
}
|
|
163
|
+
return Array.from(filePathSet);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const githubBaseSha = process.env.GITHUB_BASE_SHA;
|
|
167
|
+
const githubHeadSha = process.env.GITHUB_HEAD_SHA ?? 'HEAD';
|
|
168
|
+
if (githubBaseSha) {
|
|
169
|
+
return collectGitChangedFiles(githubBaseSha, githubHeadSha);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const gitlabBaseSha = process.env.CI_MERGE_REQUEST_DIFF_BASE_SHA;
|
|
173
|
+
const gitlabHeadSha = process.env.CI_COMMIT_SHA ?? 'HEAD';
|
|
174
|
+
if (gitlabBaseSha) {
|
|
175
|
+
return collectGitChangedFiles(gitlabBaseSha, gitlabHeadSha);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const output = execSync('git diff --name-only HEAD~1 HEAD', {
|
|
180
|
+
cwd: REPOSITORY_ROOT,
|
|
181
|
+
encoding: /** @type {'utf-8'} */ ('utf-8'),
|
|
182
|
+
maxBuffer: 1024 * 1024 * 2,
|
|
183
|
+
});
|
|
184
|
+
return output.split(/\r?\n/u).map((filePath) => filePath.trim()).filter(Boolean);
|
|
185
|
+
} catch {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isUiRelevantFilePath(filePath) {
|
|
191
|
+
const normalizedFilePath = String(filePath || '').replace(/\\/g, '/').toLowerCase();
|
|
192
|
+
const fileExtension = extname(normalizedFilePath);
|
|
193
|
+
|
|
194
|
+
if (!UI_FILE_EXTENSIONS.has(fileExtension)) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
normalizedFilePath.startsWith('src/')
|
|
200
|
+
|| normalizedFilePath.startsWith('app/')
|
|
201
|
+
|| normalizedFilePath.startsWith('pages/')
|
|
202
|
+
|| normalizedFilePath.startsWith('components/')
|
|
203
|
+
|| normalizedFilePath.startsWith('styles/')
|
|
204
|
+
|| normalizedFilePath.includes('/components/')
|
|
205
|
+
|| normalizedFilePath.includes('/screens/')
|
|
206
|
+
|| normalizedFilePath.includes('/layouts/')
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function loadDesignIntent() {
|
|
211
|
+
if (!existsSync(DESIGN_INTENT_PATH)) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
return JSON.parse(readFileSync(DESIGN_INTENT_PATH, 'utf8'));
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function loadDesignGuide() {
|
|
223
|
+
if (!existsSync(DESIGN_GUIDE_PATH)) {
|
|
224
|
+
return '';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return readFileSync(DESIGN_GUIDE_PATH, 'utf8');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function buildSystemPrompt() {
|
|
231
|
+
return [
|
|
232
|
+
'You are a Principal UI/UX Design Reviewer.',
|
|
233
|
+
'Compare the changed UI code against the provided design contract.',
|
|
234
|
+
'Treat docs/design-intent.json as the machine-readable source of truth.',
|
|
235
|
+
'Treat docs/DESIGN.md as explanatory context, not a generic style guide.',
|
|
236
|
+
'Do not reward generic SaaS defaults or popular template patterns.',
|
|
237
|
+
'Do not penalize originality when the implementation still aligns with the contract.',
|
|
238
|
+
'Only flag drift when there is a clear mismatch with the contract, accessibility non-negotiables, or cross-viewport adaptation rules.',
|
|
239
|
+
'This audit always runs in advisory mode for this repository workflow.',
|
|
240
|
+
'Focus on color intent, typographic hierarchy, responsive re-layout, interaction behavior, and genericity drift.',
|
|
241
|
+
'Return ONLY one JSON object on a single line prefixed with JSON_VERDICT:.',
|
|
242
|
+
'Schema:',
|
|
243
|
+
'{"alignmentScore": number|null, "notes": string[], "findings": [{"area": string, "severity": "high|medium|low", "problem": string, "evidence": string, "recommendation": string, "blockingRecommended": boolean}]}',
|
|
244
|
+
].join('\n');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function buildUserMessage(designIntentContent, designGuideContent, diffContent, changedUiFiles) {
|
|
248
|
+
const truncatedDiff = diffContent.length > MAX_DIFF_CHARS
|
|
249
|
+
? `${diffContent.slice(0, MAX_DIFF_CHARS)}\n\n[DIFF TRUNCATED - ${diffContent.length - MAX_DIFF_CHARS} additional characters omitted]`
|
|
250
|
+
: diffContent;
|
|
251
|
+
|
|
252
|
+
return [
|
|
253
|
+
'## Changed UI Files',
|
|
254
|
+
changedUiFiles.length > 0 ? changedUiFiles.map((filePath) => `- ${filePath}`).join('\n') : '- none',
|
|
255
|
+
'',
|
|
256
|
+
'## design-intent.json',
|
|
257
|
+
'```json',
|
|
258
|
+
JSON.stringify(designIntentContent, null, 2),
|
|
259
|
+
'```',
|
|
260
|
+
'',
|
|
261
|
+
'## DESIGN.md',
|
|
262
|
+
'```md',
|
|
263
|
+
designGuideContent.trim() || '(missing DESIGN.md)',
|
|
264
|
+
'```',
|
|
265
|
+
'',
|
|
266
|
+
'## UI Diff',
|
|
267
|
+
'```diff',
|
|
268
|
+
truncatedDiff.trim() || '(no UI diff)',
|
|
269
|
+
'```',
|
|
270
|
+
'',
|
|
271
|
+
'Judge alignment to the contract. Avoid aesthetic bias toward generic web trends.',
|
|
272
|
+
].join('\n');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function callOpenAiProvider(systemPrompt, userMessage) {
|
|
276
|
+
const selectedModel = process.env.LLM_JUDGE_MODEL ?? 'gpt-4o-mini';
|
|
277
|
+
const apiResponse = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
278
|
+
method: 'POST',
|
|
279
|
+
headers: {
|
|
280
|
+
'Content-Type': 'application/json',
|
|
281
|
+
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
|
|
282
|
+
},
|
|
283
|
+
body: JSON.stringify({
|
|
284
|
+
model: selectedModel,
|
|
285
|
+
max_tokens: 2048,
|
|
286
|
+
temperature: 0,
|
|
287
|
+
messages: [
|
|
288
|
+
{ role: 'system', content: systemPrompt },
|
|
289
|
+
{ role: 'user', content: userMessage },
|
|
290
|
+
],
|
|
291
|
+
}),
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (!apiResponse.ok) {
|
|
295
|
+
const errorBody = await apiResponse.text();
|
|
296
|
+
throw new Error(`OpenAI API returned ${apiResponse.status}: ${errorBody}`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const responsePayload = await apiResponse.json();
|
|
300
|
+
return responsePayload.choices[0].message.content;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function callAnthropicProvider(systemPrompt, userMessage) {
|
|
304
|
+
const selectedModel = process.env.LLM_JUDGE_MODEL ?? 'claude-3-5-haiku-latest';
|
|
305
|
+
const apiResponse = await fetch('https://api.anthropic.com/v1/messages', {
|
|
306
|
+
method: 'POST',
|
|
307
|
+
headers: {
|
|
308
|
+
'Content-Type': 'application/json',
|
|
309
|
+
'x-api-key': process.env.ANTHROPIC_API_KEY ?? '',
|
|
310
|
+
'anthropic-version': '2023-06-01',
|
|
311
|
+
},
|
|
312
|
+
body: JSON.stringify({
|
|
313
|
+
model: selectedModel,
|
|
314
|
+
max_tokens: 2048,
|
|
315
|
+
system: systemPrompt,
|
|
316
|
+
messages: [{ role: 'user', content: userMessage }],
|
|
317
|
+
}),
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
if (!apiResponse.ok) {
|
|
321
|
+
const errorBody = await apiResponse.text();
|
|
322
|
+
throw new Error(`Anthropic API returned ${apiResponse.status}: ${errorBody}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const responsePayload = await apiResponse.json();
|
|
326
|
+
return responsePayload.content[0].text;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function callGeminiProvider(systemPrompt, userMessage) {
|
|
330
|
+
const selectedModel = process.env.LLM_JUDGE_MODEL ?? 'gemini-2.0-flash';
|
|
331
|
+
const apiKey = process.env.GEMINI_API_KEY ?? '';
|
|
332
|
+
const endpointUrl = `https://generativelanguage.googleapis.com/v1beta/models/${selectedModel}:generateContent?key=${apiKey}`;
|
|
333
|
+
|
|
334
|
+
const apiResponse = await fetch(endpointUrl, {
|
|
335
|
+
method: 'POST',
|
|
336
|
+
headers: { 'Content-Type': 'application/json' },
|
|
337
|
+
body: JSON.stringify({
|
|
338
|
+
system_instruction: { parts: [{ text: systemPrompt }] },
|
|
339
|
+
contents: [{ role: 'user', parts: [{ text: userMessage }] }],
|
|
340
|
+
generationConfig: { temperature: 0, maxOutputTokens: 2048 },
|
|
341
|
+
}),
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (!apiResponse.ok) {
|
|
345
|
+
const errorBody = await apiResponse.text();
|
|
346
|
+
throw new Error(`Gemini API returned ${apiResponse.status}: ${errorBody}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const responsePayload = await apiResponse.json();
|
|
350
|
+
return responsePayload.candidates[0].content.parts[0].text;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function selectAvailableProvider() {
|
|
354
|
+
if (process.env.UI_DESIGN_JUDGE_MOCK_RESPONSE) {
|
|
355
|
+
return {
|
|
356
|
+
providerName: 'mock',
|
|
357
|
+
invokeProvider: async () => process.env.UI_DESIGN_JUDGE_MOCK_RESPONSE,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (process.env.OPENAI_API_KEY) {
|
|
362
|
+
return { providerName: 'openai', invokeProvider: callOpenAiProvider };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
366
|
+
return { providerName: 'anthropic', invokeProvider: callAnthropicProvider };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (process.env.GEMINI_API_KEY) {
|
|
370
|
+
return { providerName: 'gemini', invokeProvider: callGeminiProvider };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function extractVerdictObject(rawResponseText) {
|
|
377
|
+
const verdictMatch = rawResponseText.match(/JSON_VERDICT:\s*(\{[\s\S]*\})/i);
|
|
378
|
+
if (!verdictMatch) {
|
|
379
|
+
return { verdict: null, malformed: true };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
return {
|
|
384
|
+
verdict: JSON.parse(verdictMatch[1]),
|
|
385
|
+
malformed: false,
|
|
386
|
+
};
|
|
387
|
+
} catch {
|
|
388
|
+
return {
|
|
389
|
+
verdict: null,
|
|
390
|
+
malformed: true,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function normalizeFindings(rawFindings) {
|
|
396
|
+
if (!Array.isArray(rawFindings)) {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return rawFindings.map((rawFinding) => ({
|
|
401
|
+
area: String(rawFinding?.area || 'general'),
|
|
402
|
+
severity: normalizeSeverity(rawFinding?.severity),
|
|
403
|
+
problem: String(rawFinding?.problem || 'No problem description provided.'),
|
|
404
|
+
evidence: String(rawFinding?.evidence || 'No evidence provided.'),
|
|
405
|
+
recommendation: String(rawFinding?.recommendation || 'No recommendation provided.'),
|
|
406
|
+
blockingRecommended: rawFinding?.blockingRecommended === true,
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* @param {Partial<UiDesignJudgeReport>} partialReport
|
|
412
|
+
* @returns {UiDesignJudgeReport}
|
|
413
|
+
*/
|
|
414
|
+
function buildReport(partialReport) {
|
|
415
|
+
return {
|
|
416
|
+
generatedAt: new Date().toISOString(),
|
|
417
|
+
auditName: 'ui-design-judge',
|
|
418
|
+
schemaVersion: '1.0',
|
|
419
|
+
mode: 'advisory',
|
|
420
|
+
advisoryOnly: true,
|
|
421
|
+
passed: true,
|
|
422
|
+
skipped: false,
|
|
423
|
+
skipReason: null,
|
|
424
|
+
provider: 'none',
|
|
425
|
+
ciProvider: detectCiProvider(),
|
|
426
|
+
contractPresent: false,
|
|
427
|
+
summary: {
|
|
428
|
+
changedUiFileCount: 0,
|
|
429
|
+
alignmentScore: null,
|
|
430
|
+
driftCount: 0,
|
|
431
|
+
blockingCandidateCount: 0,
|
|
432
|
+
},
|
|
433
|
+
malformedVerdict: false,
|
|
434
|
+
providerError: false,
|
|
435
|
+
findings: [],
|
|
436
|
+
notes: [],
|
|
437
|
+
...partialReport,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function emitMachineReadableReport(machineReportPayload) {
|
|
442
|
+
console.log(JSON.stringify(machineReportPayload, null, 2));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function main() {
|
|
446
|
+
const changedFiles = collectChangedFiles();
|
|
447
|
+
const changedUiFiles = changedFiles.filter(isUiRelevantFilePath);
|
|
448
|
+
const rawDiff = collectPullRequestDiff();
|
|
449
|
+
const designIntentContent = loadDesignIntent();
|
|
450
|
+
const designGuideContent = loadDesignGuide();
|
|
451
|
+
|
|
452
|
+
if (!designIntentContent) {
|
|
453
|
+
emitMachineReadableReport(buildReport({
|
|
454
|
+
skipped: true,
|
|
455
|
+
skipReason: 'Design contract is missing or unreadable. Skipping UI design judge.',
|
|
456
|
+
contractPresent: false,
|
|
457
|
+
notes: ['docs/design-intent.json is required for contract-aware UI judging.'],
|
|
458
|
+
}));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (changedUiFiles.length === 0) {
|
|
463
|
+
emitMachineReadableReport(buildReport({
|
|
464
|
+
skipped: true,
|
|
465
|
+
skipReason: 'No UI-relevant changed files detected.',
|
|
466
|
+
contractPresent: true,
|
|
467
|
+
summary: {
|
|
468
|
+
changedUiFileCount: 0,
|
|
469
|
+
alignmentScore: null,
|
|
470
|
+
driftCount: 0,
|
|
471
|
+
blockingCandidateCount: 0,
|
|
472
|
+
},
|
|
473
|
+
notes: ['UI design judge only evaluates changed UI surfaces.'],
|
|
474
|
+
}));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const systemPrompt = buildSystemPrompt();
|
|
479
|
+
const userMessage = buildUserMessage(designIntentContent, designGuideContent, rawDiff, changedUiFiles);
|
|
480
|
+
|
|
481
|
+
const selectedProvider = selectAvailableProvider();
|
|
482
|
+
if (!selectedProvider) {
|
|
483
|
+
emitMachineReadableReport(buildReport({
|
|
484
|
+
provider: 'none',
|
|
485
|
+
contractPresent: true,
|
|
486
|
+
summary: {
|
|
487
|
+
changedUiFileCount: changedUiFiles.length,
|
|
488
|
+
alignmentScore: null,
|
|
489
|
+
driftCount: 0,
|
|
490
|
+
blockingCandidateCount: 0,
|
|
491
|
+
},
|
|
492
|
+
notes: ['No LLM provider configured. UI design judge skipped provider review and stayed advisory.'],
|
|
493
|
+
}));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
let rawJudgeResponse;
|
|
498
|
+
try {
|
|
499
|
+
rawJudgeResponse = await selectedProvider.invokeProvider(systemPrompt, userMessage);
|
|
500
|
+
} catch (providerError) {
|
|
501
|
+
const providerErrorMessage = providerError instanceof Error
|
|
502
|
+
? providerError.message
|
|
503
|
+
: 'Unknown provider error';
|
|
504
|
+
|
|
505
|
+
emitMachineReadableReport(buildReport({
|
|
506
|
+
provider: selectedProvider.providerName,
|
|
507
|
+
contractPresent: true,
|
|
508
|
+
providerError: true,
|
|
509
|
+
summary: {
|
|
510
|
+
changedUiFileCount: changedUiFiles.length,
|
|
511
|
+
alignmentScore: null,
|
|
512
|
+
driftCount: 0,
|
|
513
|
+
blockingCandidateCount: 0,
|
|
514
|
+
},
|
|
515
|
+
notes: [`Provider call failed: ${providerErrorMessage}`],
|
|
516
|
+
passed: true,
|
|
517
|
+
}));
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const { verdict, malformed } = extractVerdictObject(rawJudgeResponse);
|
|
522
|
+
const findings = normalizeFindings(verdict?.findings);
|
|
523
|
+
const blockingCandidateCount = findings.filter((finding) => finding.blockingRecommended || finding.severity === 'high').length;
|
|
524
|
+
const alignmentScore = typeof verdict?.alignmentScore === 'number' ? verdict.alignmentScore : null;
|
|
525
|
+
const notes = Array.isArray(verdict?.notes)
|
|
526
|
+
? verdict.notes.map((note) => String(note))
|
|
527
|
+
: [];
|
|
528
|
+
|
|
529
|
+
const reportPayload = buildReport({
|
|
530
|
+
provider: selectedProvider.providerName,
|
|
531
|
+
contractPresent: true,
|
|
532
|
+
passed: true,
|
|
533
|
+
malformedVerdict: malformed,
|
|
534
|
+
summary: {
|
|
535
|
+
changedUiFileCount: changedUiFiles.length,
|
|
536
|
+
alignmentScore,
|
|
537
|
+
driftCount: findings.length,
|
|
538
|
+
blockingCandidateCount,
|
|
539
|
+
},
|
|
540
|
+
findings,
|
|
541
|
+
notes: malformed
|
|
542
|
+
? ['LLM response was malformed. Advisory mode kept the audit non-blocking.']
|
|
543
|
+
: notes,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
emitMachineReadableReport(reportPayload);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
main().catch((unexpectedError) => {
|
|
550
|
+
const errorMessage = unexpectedError instanceof Error
|
|
551
|
+
? unexpectedError.message
|
|
552
|
+
: 'Unknown unexpected error';
|
|
553
|
+
|
|
554
|
+
emitMachineReadableReport(buildReport({
|
|
555
|
+
provider: 'none',
|
|
556
|
+
providerError: true,
|
|
557
|
+
passed: true,
|
|
558
|
+
notes: [`Unexpected ui-design-judge failure: ${errorMessage}`],
|
|
559
|
+
}));
|
|
560
|
+
});
|
package/scripts/validate.mjs
CHANGED
|
@@ -268,6 +268,62 @@ const REQUIRED_UPGRADE_UI_CONTRACT_WARNING_SNIPPETS = [
|
|
|
268
268
|
],
|
|
269
269
|
},
|
|
270
270
|
];
|
|
271
|
+
const REQUIRED_UI_DESIGN_AUTOMATION_SNIPPETS = [
|
|
272
|
+
{
|
|
273
|
+
path: '.instructions.md',
|
|
274
|
+
snippets: [
|
|
275
|
+
'UI Design Mode',
|
|
276
|
+
'bootstrap-design.md',
|
|
277
|
+
'frontend-architecture.md',
|
|
278
|
+
'do not eagerly load unrelated backend-only rules',
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
path: '.agent-context/prompts/bootstrap-design.md',
|
|
283
|
+
snippets: [
|
|
284
|
+
'UI Design Mode is context-isolated by default:',
|
|
285
|
+
'Responsive Strategy and Cross-Viewport Adaptation Matrix',
|
|
286
|
+
'`colorTruth.format`',
|
|
287
|
+
'`crossViewportAdaptation.mutationRules.mobile/tablet/desktop`',
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
path: 'scripts/ui-design-judge.mjs',
|
|
292
|
+
snippets: [
|
|
293
|
+
'Advisory-first UI design contract judge.',
|
|
294
|
+
'Repo-internal workflow audit; no user-facing runtime modes.',
|
|
295
|
+
'Runs only in advisory mode for this repository workflow.',
|
|
296
|
+
'Do not reward generic SaaS defaults or popular template patterns.',
|
|
297
|
+
'UI design judge only evaluates changed UI surfaces.',
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
path: 'lib/cli/project-scaffolder.mjs',
|
|
302
|
+
snippets: [
|
|
303
|
+
'colorTruth',
|
|
304
|
+
'crossViewportAdaptation',
|
|
305
|
+
'requireViewportMutationRules',
|
|
306
|
+
'allowHexDerivatives',
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
path: 'lib/cli/detector.mjs',
|
|
311
|
+
snippets: [
|
|
312
|
+
'hardcodedColorCount',
|
|
313
|
+
'propDrillingCandidateCount',
|
|
314
|
+
'arbitraryBreakpointCount',
|
|
315
|
+
'frontendEvidenceMetrics',
|
|
316
|
+
],
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
path: 'lib/cli/compiler.mjs',
|
|
320
|
+
snippets: [
|
|
321
|
+
'LAYER 5: EXECUTION PROMPTS AND UI TRIGGERS',
|
|
322
|
+
'bootstrap-design.md -> ui, ux, layout, screen, tailwind, frontend, redesign',
|
|
323
|
+
'Keep UI-only requests context-isolated',
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
];
|
|
271
327
|
const FORBIDDEN_TEMPLATE_BOOTSTRAP_SNIPPETS = [
|
|
272
328
|
{
|
|
273
329
|
path: 'lib/cli/project-scaffolder.mjs',
|
|
@@ -392,6 +448,7 @@ async function validateRequiredFiles() {
|
|
|
392
448
|
'scripts/governance-weekly-report.mjs',
|
|
393
449
|
'scripts/mcp-server.mjs',
|
|
394
450
|
'scripts/frontend-usability-audit.mjs',
|
|
451
|
+
'scripts/ui-design-judge.mjs',
|
|
395
452
|
'scripts/documentation-boundary-audit.mjs',
|
|
396
453
|
'scripts/context-triggered-audit.mjs',
|
|
397
454
|
'scripts/rules-guardian-audit.mjs',
|
|
@@ -490,6 +547,7 @@ async function validateRuleFiles() {
|
|
|
490
547
|
'review-checklists/pr-checklist.md',
|
|
491
548
|
'review-checklists/architecture-review.md',
|
|
492
549
|
'prompts/init-project.md',
|
|
550
|
+
'prompts/bootstrap-design.md',
|
|
493
551
|
'prompts/refactor.md',
|
|
494
552
|
'prompts/review-code.md',
|
|
495
553
|
'state/architecture-map.md',
|
|
@@ -1057,6 +1115,28 @@ async function validateUpgradeUiContractWarningCoverage() {
|
|
|
1057
1115
|
}
|
|
1058
1116
|
}
|
|
1059
1117
|
|
|
1118
|
+
async function validateUiDesignAutomationCoverage() {
|
|
1119
|
+
console.log('\nChecking UI design automation coverage...');
|
|
1120
|
+
|
|
1121
|
+
for (const coverageRule of REQUIRED_UI_DESIGN_AUTOMATION_SNIPPETS) {
|
|
1122
|
+
const absoluteCoveragePath = join(ROOT_DIR, coverageRule.path);
|
|
1123
|
+
|
|
1124
|
+
if (!(await fileExists(absoluteCoveragePath))) {
|
|
1125
|
+
fail(`Missing UI design automation source: ${coverageRule.path}`);
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const coverageContent = await readTextFile(absoluteCoveragePath);
|
|
1130
|
+
for (const requiredSnippet of coverageRule.snippets) {
|
|
1131
|
+
if (coverageContent.includes(requiredSnippet)) {
|
|
1132
|
+
pass(`${coverageRule.path} includes UI design automation snippet: ${requiredSnippet}`);
|
|
1133
|
+
} else {
|
|
1134
|
+
fail(`${coverageRule.path} is missing UI design automation snippet: ${requiredSnippet}`);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1060
1140
|
async function validateDeterministicBoundaryEnforcementCoverage() {
|
|
1061
1141
|
console.log('\nChecking deterministic boundary enforcement coverage...');
|
|
1062
1142
|
|
|
@@ -1326,6 +1406,7 @@ async function main() {
|
|
|
1326
1406
|
await validateUniversalSopConsolidationCoverage();
|
|
1327
1407
|
await validateTemplateFreeBootstrapCoverage();
|
|
1328
1408
|
await validateUpgradeUiContractWarningCoverage();
|
|
1409
|
+
await validateUiDesignAutomationCoverage();
|
|
1329
1410
|
await validateDeterministicBoundaryEnforcementCoverage();
|
|
1330
1411
|
await validateStackResearchSnapshotState();
|
|
1331
1412
|
await validateMcpConfiguration();
|