@ryuenn3123/agentic-senior-core 4.0.1 → 4.0.3
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 +2 -1
- package/.agent-context/prompts/research-design.md +17 -0
- package/README.md +2 -0
- package/bin/agentic-senior-core.js +6 -0
- package/lib/cli/audits/typography-palette-anti-repeat/color-utils.mjs +156 -0
- package/lib/cli/audits/typography-palette-anti-repeat/file-scanner.mjs +103 -0
- package/lib/cli/audits/typography-palette-anti-repeat/typography-utils.mjs +70 -0
- package/lib/cli/audits/typography-palette-anti-repeat-audit.mjs +255 -0
- package/lib/cli/commands/audit-design-anti-repeat.mjs +198 -0
- package/lib/cli/commands/upgrade.mjs +1 -0
- package/lib/cli/project-scaffolder/design-contract/research-dossier-migration.mjs +24 -0
- package/lib/cli/project-scaffolder/design-contract/validation/completeness.mjs +38 -0
- package/lib/cli/project-scaffolder/design-contract/validation/research-dossier-validators.mjs +1 -1
- package/lib/cli/project-scaffolder/design-contract.mjs +8 -0
- package/lib/cli/utils.mjs +1 -0
- package/package.json +3 -2
- package/scripts/audit-typography-palette-anti-repeat.mjs +120 -0
- package/scripts/documentation-boundary-audit.mjs +13 -0
- package/scripts/release-gate/static-checks.mjs +6 -1
- package/scripts/validate/config.mjs +7 -0
- package/scripts/validate.mjs +33 -0
|
@@ -20,7 +20,8 @@ This contract is a decision scaffold, not a style preset. We guide the agent; we
|
|
|
20
20
|
5. Encode `repoEvidence.designEvidenceSummary` when onboarding or detector evidence exists.
|
|
21
21
|
6. Keep both design docs synchronized after implementation.
|
|
22
22
|
7. Complete the Section 3-5 gates from `research-design.md` before UI implementation: `conceptualAnchor.categoryCodes.candidateEntries`, `conceptualAnchor.morphologicalExploration` (selected and uncomfortable combinations), and `conceptualAnchor.anchorCandidates.candidates` (exactly five, each with the strengthened rename test recorded).
|
|
23
|
-
8.
|
|
23
|
+
8. Set `derivedTokenLogic.tokenContinuityClassification` for each of typography, palette, motion, and spacing. Use `anchor-derived` only when the token choice is causally tied to the anchor's real-world reality. Use `continuity-retained` when the token is kept from a previous design iteration without re-derivation. Use `newly-introduced` when the token is fresh but not anchor-derived. If any token category is `continuity-retained`, the typography, palette, or motion entry in `researchDossier.metadata.antiRepeatLedger` stays as historical record, and the classification declares the retention is intentional with explicit rationale recorded in the matching `derivationSource` field.
|
|
24
|
+
9. After agent and user select an anchor, set `researchDossier.metadata.researchVerifiedAt` to today's ISO date and flip `status` from any seed value to `active`. This closes the freshness window for additive UI tasks within `freshnessWindowDays`.
|
|
24
25
|
## Creative Commitment Gate
|
|
25
26
|
Before broad compliance review or UI implementation, record an agent-chosen visual direction in both design docs:
|
|
26
27
|
- one concrete real-world anchor reference
|
|
@@ -90,6 +90,23 @@ Self-test: read each category code aloud to someone unfamiliar with the project.
|
|
|
90
90
|
|
|
91
91
|
Output: at least three category codes per product surface in `categoryCodes`. Each entry must pass the specificity self-test, must include the one-sentence reason that pattern is the default for the category, and must include an explicit one-sentence rejection note ("I will not ship this; here is the trap it sets") so the cliche cannot quietly become the target.
|
|
92
92
|
|
|
93
|
+
### Dimensional split (mandatory)
|
|
94
|
+
|
|
95
|
+
Category codes must be broken down by dimension. Do not collapse multiple dimensions into a single category-level cliche. Each cluster lists the patterns that the product category will default to without intervention.
|
|
96
|
+
|
|
97
|
+
- `typographyClusters`: font family combinations that are the category default. Be explicit about font families. Name the actual trio or pair that this product's category currently defaults to, derived from live portfolio observation for THIS task. Do not anchor on examples from other categories or other timeframes.
|
|
98
|
+
- `paletteClusters`: palette signatures that are the category default.
|
|
99
|
+
- `layoutClusters`: layout patterns that are the category default.
|
|
100
|
+
- `motionClusters`: motion signatures that are the category default.
|
|
101
|
+
- `imageryClusters`: image style or visual treatment that is the category default.
|
|
102
|
+
|
|
103
|
+
Self-check before proceeding to Section 4: do the typography choices the agent is about to commit to in `derivedTokenLogic` (or downstream token sections) overlap with any item in `typographyClusters`? If yes, the agent must either:
|
|
104
|
+
|
|
105
|
+
1. Flag the typography as a continuity choice with an explicit rationale, set `derivedTokenLogic.tokenContinuityClassification.typography` to `continuity-retained`, and record the reason that font family swap is deferred. The previous typography ledger entry stays as historical record; the classification declares the retention is intentional. OR
|
|
106
|
+
2. Revise the typography pick to escape the autopilot cluster and set `tokenContinuityClassification.typography` to `anchor-derived` only when the new choice is causally tied to the anchor's real-world reality.
|
|
107
|
+
|
|
108
|
+
This self-check applies to every dimension, not only typography. Do not let an output token match a category-code item from the agent's own list without explicit (1) or (2) treatment per dimension. Pretending continuity is derivation is the failure mode this gate exists to prevent.
|
|
109
|
+
|
|
93
110
|
## Section 4 — Morphological Exploration
|
|
94
111
|
|
|
95
112
|
A morphological matrix forces the design space to be explored beyond the first idea.
|
package/README.md
CHANGED
|
@@ -63,6 +63,8 @@ The intended behavior is agent-led, not offline-template-led:
|
|
|
63
63
|
- Modern UI claims: research current-year libraries and patterns when relevant; 2026 work should use 2026 evidence, and future years should update automatically through agent research.
|
|
64
64
|
- Anti-generic rule: avoid safe dashboard shells, admin panels, card grids, scale-only mobile layouts, and static no-motion interfaces unless the product context explicitly justifies them.
|
|
65
65
|
|
|
66
|
+
UI design work runs a research dossier prompt (`.agent-context/prompts/research-design.md`) before the bootstrap prompt. The dossier captures product reading, reference intake, category cliches, a morphological matrix, and five anchor candidates with a strengthened rename test. The contract carries a 90-day `researchVerifiedAt` freshness gate and an anti-repeat ledger seeded from prior anchor, palette, motion, and typography choices on existing projects, so additive UI work within the freshness window skips the research stage while redesigns and stale dossiers re-run it.
|
|
67
|
+
|
|
66
68
|
---
|
|
67
69
|
|
|
68
70
|
## MCP Quick Setup (VS Code)
|
|
@@ -15,6 +15,7 @@ import { runMcpServerCommand } from '../lib/cli/commands/mcp.mjs';
|
|
|
15
15
|
import { runOptimizeCommand, parseOptimizeArguments } from '../lib/cli/commands/optimize.mjs';
|
|
16
16
|
import { runInitCommand, parseInitArguments } from '../lib/cli/commands/init.mjs';
|
|
17
17
|
import { runUpgradeCommand, parseUpgradeArguments } from '../lib/cli/commands/upgrade.mjs';
|
|
18
|
+
import { runDesignAntiRepeatAuditCommand } from '../lib/cli/commands/audit-design-anti-repeat.mjs';
|
|
18
19
|
|
|
19
20
|
async function main() {
|
|
20
21
|
const commandArgument = process.argv[2];
|
|
@@ -63,6 +64,11 @@ async function main() {
|
|
|
63
64
|
return;
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
if (commandArgument === 'audit:design-anti-repeat') {
|
|
68
|
+
const auditExitCode = await runDesignAntiRepeatAuditCommand(commandArguments);
|
|
69
|
+
exit(auditExitCode);
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
console.error(`Unknown command: ${commandArgument}`);
|
|
67
73
|
printUsage();
|
|
68
74
|
exit(1);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Color parsing and distance utilities for the typography/palette
|
|
5
|
+
* anti-repeat audit. OKLCH math is contained here so the main audit module
|
|
6
|
+
* stays focused on file scanning and ledger cross-check.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const HEX_COLOR_PATTERN = /#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/g;
|
|
10
|
+
export const OKLCH_PATTERN = /oklch\(\s*([^)]+)\s*\)/gi;
|
|
11
|
+
|
|
12
|
+
export function expandShortHexColor(rawHexColor) {
|
|
13
|
+
if (rawHexColor.length !== 4) {
|
|
14
|
+
return rawHexColor;
|
|
15
|
+
}
|
|
16
|
+
const expandedChars = [];
|
|
17
|
+
for (let charIndex = 1; charIndex < rawHexColor.length; charIndex += 1) {
|
|
18
|
+
expandedChars.push(rawHexColor[charIndex], rawHexColor[charIndex]);
|
|
19
|
+
}
|
|
20
|
+
return `#${expandedChars.join('')}`.toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function normalizeHexColor(rawHexColor) {
|
|
24
|
+
if (typeof rawHexColor !== 'string' || rawHexColor.length === 0) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const trimmedHex = rawHexColor.trim().toLowerCase();
|
|
28
|
+
if (!trimmedHex.startsWith('#')) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (trimmedHex.length === 4) {
|
|
32
|
+
return expandShortHexColor(trimmedHex);
|
|
33
|
+
}
|
|
34
|
+
if (trimmedHex.length === 7 || trimmedHex.length === 9) {
|
|
35
|
+
return trimmedHex.slice(0, 7);
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseOklchNumber(rawNumber, percentScale) {
|
|
41
|
+
const numberString = String(rawNumber || '').trim();
|
|
42
|
+
if (numberString.endsWith('%')) {
|
|
43
|
+
const percentValue = parseFloat(numberString.slice(0, -1));
|
|
44
|
+
if (!Number.isFinite(percentValue)) {
|
|
45
|
+
return Number.NaN;
|
|
46
|
+
}
|
|
47
|
+
return (percentValue / 100) * percentScale;
|
|
48
|
+
}
|
|
49
|
+
return parseFloat(numberString);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function parseOklchTriple(oklchExpression) {
|
|
53
|
+
const componentTokens = String(oklchExpression || '')
|
|
54
|
+
.replace(/\//g, ' ')
|
|
55
|
+
.split(/[\s,]+/)
|
|
56
|
+
.filter((componentToken) => componentToken.length > 0);
|
|
57
|
+
if (componentTokens.length < 3) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const lightness = parseOklchNumber(componentTokens[0], 1);
|
|
61
|
+
const chroma = parseFloat(componentTokens[1]);
|
|
62
|
+
const hueDegrees = parseOklchNumber(componentTokens[2], 360);
|
|
63
|
+
if (!Number.isFinite(lightness) || !Number.isFinite(chroma) || !Number.isFinite(hueDegrees)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return { lightness, chroma, hueDegrees };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* L*C*H-style perceptual distance proxy. OKLCH is perceptually uniform,
|
|
71
|
+
* so a Cartesian-like delta over (L, C, C * Δhue_radians) tracks visible
|
|
72
|
+
* difference well enough for an anti-repeat heuristic.
|
|
73
|
+
*/
|
|
74
|
+
export function oklchPerceptualDistance(leftColor, rightColor) {
|
|
75
|
+
const lightnessDelta = leftColor.lightness - rightColor.lightness;
|
|
76
|
+
const chromaDelta = leftColor.chroma - rightColor.chroma;
|
|
77
|
+
const hueDelta = ((leftColor.hueDegrees - rightColor.hueDegrees + 540) % 360) - 180;
|
|
78
|
+
const hueArcRadians = (hueDelta * Math.PI) / 180;
|
|
79
|
+
const hueChromaDelta = leftColor.chroma * hueArcRadians;
|
|
80
|
+
return Math.sqrt(
|
|
81
|
+
lightnessDelta * lightnessDelta
|
|
82
|
+
+ chromaDelta * chromaDelta
|
|
83
|
+
+ hueChromaDelta * hueChromaDelta,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function extractColorOccurrencesFromText(sourceText) {
|
|
88
|
+
/** @type {{ kind: 'hex' | 'oklch', raw: string, normalized: string | null, oklch: { lightness: number, chroma: number, hueDegrees: number } | null, index: number }[]} */
|
|
89
|
+
const colorOccurrences = [];
|
|
90
|
+
|
|
91
|
+
HEX_COLOR_PATTERN.lastIndex = 0;
|
|
92
|
+
let hexMatch;
|
|
93
|
+
// eslint-disable-next-line no-cond-assign
|
|
94
|
+
while ((hexMatch = HEX_COLOR_PATTERN.exec(sourceText)) !== null) {
|
|
95
|
+
colorOccurrences.push({
|
|
96
|
+
kind: 'hex',
|
|
97
|
+
raw: hexMatch[0],
|
|
98
|
+
normalized: normalizeHexColor(hexMatch[0]),
|
|
99
|
+
oklch: null,
|
|
100
|
+
index: hexMatch.index,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
OKLCH_PATTERN.lastIndex = 0;
|
|
105
|
+
let oklchMatch;
|
|
106
|
+
// eslint-disable-next-line no-cond-assign
|
|
107
|
+
while ((oklchMatch = OKLCH_PATTERN.exec(sourceText)) !== null) {
|
|
108
|
+
colorOccurrences.push({
|
|
109
|
+
kind: 'oklch',
|
|
110
|
+
raw: oklchMatch[0],
|
|
111
|
+
normalized: oklchMatch[0].toLowerCase(),
|
|
112
|
+
oklch: parseOklchTriple(oklchMatch[1]),
|
|
113
|
+
index: oklchMatch.index,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return colorOccurrences;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function extractLedgerColors(antiRepeatLedger) {
|
|
121
|
+
const previousPalettes = Array.isArray(antiRepeatLedger?.previousPalettes)
|
|
122
|
+
? antiRepeatLedger.previousPalettes
|
|
123
|
+
: [];
|
|
124
|
+
|
|
125
|
+
/** @type {{ kind: 'hex' | 'oklch', normalized: string | null, oklch: { lightness: number, chroma: number, hueDegrees: number } | null, sourceEntry: any }[]} */
|
|
126
|
+
const ledgerColors = [];
|
|
127
|
+
for (const ledgerEntry of previousPalettes) {
|
|
128
|
+
const summaryString = String(ledgerEntry?.summary || '');
|
|
129
|
+
if (!summaryString) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
HEX_COLOR_PATTERN.lastIndex = 0;
|
|
133
|
+
let hexMatch;
|
|
134
|
+
// eslint-disable-next-line no-cond-assign
|
|
135
|
+
while ((hexMatch = HEX_COLOR_PATTERN.exec(summaryString)) !== null) {
|
|
136
|
+
ledgerColors.push({
|
|
137
|
+
kind: 'hex',
|
|
138
|
+
normalized: normalizeHexColor(hexMatch[0]),
|
|
139
|
+
oklch: null,
|
|
140
|
+
sourceEntry: ledgerEntry,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
OKLCH_PATTERN.lastIndex = 0;
|
|
144
|
+
let oklchMatch;
|
|
145
|
+
// eslint-disable-next-line no-cond-assign
|
|
146
|
+
while ((oklchMatch = OKLCH_PATTERN.exec(summaryString)) !== null) {
|
|
147
|
+
ledgerColors.push({
|
|
148
|
+
kind: 'oklch',
|
|
149
|
+
normalized: oklchMatch[0].toLowerCase(),
|
|
150
|
+
oklch: parseOklchTriple(oklchMatch[1]),
|
|
151
|
+
sourceEntry: ledgerEntry,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return ledgerColors;
|
|
156
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Filesystem scanning for the typography/palette anti-repeat audit. Walks
|
|
5
|
+
* the repository tree, skips noisy directories (node_modules, build outputs,
|
|
6
|
+
* etc.), and yields scannable CSS-like and token-config file paths.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readdirSync, statSync, existsSync } from 'node:fs';
|
|
10
|
+
import { extname, join, relative, sep } from 'node:path';
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_CSS_FILE_EXTENSIONS = new Set(['.css', '.scss', '.sass', '.less']);
|
|
13
|
+
export const TOKEN_CONFIG_FILE_NAMES = new Set([
|
|
14
|
+
'tailwind.config.js',
|
|
15
|
+
'tailwind.config.cjs',
|
|
16
|
+
'tailwind.config.mjs',
|
|
17
|
+
'tailwind.config.ts',
|
|
18
|
+
'theme.config.js',
|
|
19
|
+
'theme.config.ts',
|
|
20
|
+
'design-tokens.js',
|
|
21
|
+
'design-tokens.ts',
|
|
22
|
+
]);
|
|
23
|
+
export const SCAN_SKIP_DIRECTORY_NAMES = new Set([
|
|
24
|
+
'node_modules',
|
|
25
|
+
'.git',
|
|
26
|
+
'.agentic-backup',
|
|
27
|
+
'.benchmarks',
|
|
28
|
+
'dist',
|
|
29
|
+
'build',
|
|
30
|
+
'.next',
|
|
31
|
+
'out',
|
|
32
|
+
'coverage',
|
|
33
|
+
'.cache',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function isInsideSkippedDirectory(absolutePath, repositoryRootPath) {
|
|
37
|
+
const relativePath = relative(repositoryRootPath, absolutePath);
|
|
38
|
+
if (!relativePath || relativePath.startsWith('..')) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const pathSegments = relativePath.split(sep);
|
|
42
|
+
return pathSegments.some((segmentName) => SCAN_SKIP_DIRECTORY_NAMES.has(segmentName));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function* walkDirectoryEntries(currentDirectoryPath, repositoryRootPath) {
|
|
46
|
+
const directoryEntries = readdirSync(currentDirectoryPath, { withFileTypes: true });
|
|
47
|
+
for (const directoryEntry of directoryEntries) {
|
|
48
|
+
const childPath = join(currentDirectoryPath, directoryEntry.name);
|
|
49
|
+
if (directoryEntry.isDirectory()) {
|
|
50
|
+
if (SCAN_SKIP_DIRECTORY_NAMES.has(directoryEntry.name)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (isInsideSkippedDirectory(childPath, repositoryRootPath)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
yield* walkDirectoryEntries(childPath, repositoryRootPath);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (directoryEntry.isFile()) {
|
|
60
|
+
yield childPath;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isScannableFile(absoluteFilePath) {
|
|
66
|
+
const fileExtension = extname(absoluteFilePath).toLowerCase();
|
|
67
|
+
if (DEFAULT_CSS_FILE_EXTENSIONS.has(fileExtension)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
const baseName = absoluteFilePath.split(/[\\/]/).pop();
|
|
71
|
+
return TOKEN_CONFIG_FILE_NAMES.has(baseName);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function collectScannableFilePaths(repositoryRootPath, scanRoots) {
|
|
75
|
+
const collectedFilePaths = [];
|
|
76
|
+
for (const scanRoot of scanRoots) {
|
|
77
|
+
const absoluteScanRoot = join(repositoryRootPath, scanRoot);
|
|
78
|
+
if (!existsSync(absoluteScanRoot)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const scanRootStat = statSync(absoluteScanRoot);
|
|
82
|
+
if (!scanRootStat.isDirectory()) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
for (const candidateFilePath of walkDirectoryEntries(absoluteScanRoot, repositoryRootPath)) {
|
|
86
|
+
if (!isScannableFile(candidateFilePath)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
collectedFilePaths.push(candidateFilePath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return collectedFilePaths.sort();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function lineNumberFromIndex(sourceText, charIndex) {
|
|
96
|
+
let lineNumber = 1;
|
|
97
|
+
for (let scanIndex = 0; scanIndex < charIndex && scanIndex < sourceText.length; scanIndex += 1) {
|
|
98
|
+
if (sourceText[scanIndex] === '\n') {
|
|
99
|
+
lineNumber += 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return lineNumber;
|
|
103
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Font-family parsing for the typography/palette anti-repeat audit. CSS
|
|
5
|
+
* `font-family:` declarations and `@font-face` blocks are the inputs;
|
|
6
|
+
* normalized lowercase family tokens are the outputs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const FONT_FAMILY_DECLARATION_PATTERN = /font-family\s*:\s*([^;}]+)[;}]/gi;
|
|
10
|
+
export const FONT_FACE_FAMILY_PATTERN = /@font-face\s*\{[^}]*font-family\s*:\s*([^;]+);/gi;
|
|
11
|
+
|
|
12
|
+
export function normalizeFontFamilyToken(rawFamilyToken) {
|
|
13
|
+
return String(rawFamilyToken || '')
|
|
14
|
+
.replace(/^["']|["']$/g, '')
|
|
15
|
+
.trim()
|
|
16
|
+
.toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function splitFontFamilyDeclaration(declarationValue) {
|
|
20
|
+
return String(declarationValue || '')
|
|
21
|
+
.split(',')
|
|
22
|
+
.map((familyEntry) => normalizeFontFamilyToken(familyEntry))
|
|
23
|
+
.filter((familyEntry) => familyEntry.length > 0 && !familyEntry.startsWith('var('));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function extractFontFamiliesFromText(sourceText) {
|
|
27
|
+
/** @type {{ family: string, index: number }[]} */
|
|
28
|
+
const familyOccurrences = [];
|
|
29
|
+
|
|
30
|
+
for (const matchPattern of [FONT_FAMILY_DECLARATION_PATTERN, FONT_FACE_FAMILY_PATTERN]) {
|
|
31
|
+
matchPattern.lastIndex = 0;
|
|
32
|
+
let regexMatch;
|
|
33
|
+
// eslint-disable-next-line no-cond-assign
|
|
34
|
+
while ((regexMatch = matchPattern.exec(sourceText)) !== null) {
|
|
35
|
+
const declarationValue = regexMatch[1];
|
|
36
|
+
const declarationStartIndex = regexMatch.index;
|
|
37
|
+
for (const familyEntry of splitFontFamilyDeclaration(declarationValue)) {
|
|
38
|
+
familyOccurrences.push({ family: familyEntry, index: declarationStartIndex });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return familyOccurrences;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function extractLedgerTypographyFamilies(antiRepeatLedger) {
|
|
47
|
+
const previousTypographyChoices = Array.isArray(antiRepeatLedger?.previousTypographyChoices)
|
|
48
|
+
? antiRepeatLedger.previousTypographyChoices
|
|
49
|
+
: [];
|
|
50
|
+
|
|
51
|
+
const ledgerFamilies = new Map();
|
|
52
|
+
for (const ledgerEntry of previousTypographyChoices) {
|
|
53
|
+
const summaryString = String(ledgerEntry?.summary || '');
|
|
54
|
+
if (!summaryString) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
// Summaries are emitted as "role: value; role: value". Each value is a
|
|
58
|
+
// font family that the previous design shipped.
|
|
59
|
+
for (const summaryPart of summaryString.split(';')) {
|
|
60
|
+
const colonSplitIndex = summaryPart.indexOf(':');
|
|
61
|
+
const familyValue = colonSplitIndex >= 0 ? summaryPart.slice(colonSplitIndex + 1) : summaryPart;
|
|
62
|
+
const normalizedFamilyValue = normalizeFontFamilyToken(familyValue);
|
|
63
|
+
if (normalizedFamilyValue.length === 0) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
ledgerFamilies.set(normalizedFamilyValue, ledgerEntry);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return ledgerFamilies;
|
|
70
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Typography and palette anti-repeat audit.
|
|
5
|
+
*
|
|
6
|
+
* Scans CSS-like files for font-family declarations and color values, then
|
|
7
|
+
* cross-checks them against
|
|
8
|
+
* `docs/design-intent.json`'s
|
|
9
|
+
* `researchDossier.metadata.antiRepeatLedger.previousTypographyChoices` and
|
|
10
|
+
* `previousPalettes` so a redesign cannot leak the previous direction's
|
|
11
|
+
* exact font trio or perceptually-identical palette colors through CSS
|
|
12
|
+
* implementation files alone.
|
|
13
|
+
*
|
|
14
|
+
* Severity model:
|
|
15
|
+
* - Typography matches are EXACT family-name matches and are always
|
|
16
|
+
* blocking (kind: typography.previously-shipped-family,
|
|
17
|
+
* diagnosticCode: BOUNDARY_TYPOGRAPHY_LEDGER_VIOLATION).
|
|
18
|
+
* - Palette matches are HEURISTIC. OKLCH-to-OKLCH distance is perceptually
|
|
19
|
+
* uniform (deltaE < threshold => match). Hex-to-hex matches are exact
|
|
20
|
+
* normalized equality. Cross-type matches (hex declared in CSS vs OKLCH
|
|
21
|
+
* in the ledger, or vice versa) are NOT performed; reporting cross-type
|
|
22
|
+
* would require a color-space conversion that is its own correctness
|
|
23
|
+
* surface (tracked in docs/deep-analysis-and-roadmap-backlog.md).
|
|
24
|
+
* - Palette findings BLOCK by default with kind:
|
|
25
|
+
* palette.previously-shipped-color and diagnosticCode:
|
|
26
|
+
* BOUNDARY_PALETTE_LEDGER_VIOLATION. Pass
|
|
27
|
+
* `treatPaletteAsAdvisory: true` (or `--palette-advisory` on the CLI)
|
|
28
|
+
* to opt a project into advisory-only palette reporting; severity drops
|
|
29
|
+
* to "advisory" and the diagnostic code becomes
|
|
30
|
+
* BOUNDARY_PALETTE_LEDGER_ADVISORY. Use the advisory mode only when a
|
|
31
|
+
* project knowingly accepts the false-positive risk of OKLCH distance
|
|
32
|
+
* matching at its chosen threshold.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
36
|
+
import { join, relative } from 'node:path';
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
collectScannableFilePaths,
|
|
40
|
+
lineNumberFromIndex,
|
|
41
|
+
} from './typography-palette-anti-repeat/file-scanner.mjs';
|
|
42
|
+
import {
|
|
43
|
+
extractFontFamiliesFromText,
|
|
44
|
+
extractLedgerTypographyFamilies,
|
|
45
|
+
} from './typography-palette-anti-repeat/typography-utils.mjs';
|
|
46
|
+
import {
|
|
47
|
+
extractColorOccurrencesFromText,
|
|
48
|
+
extractLedgerColors,
|
|
49
|
+
oklchPerceptualDistance,
|
|
50
|
+
} from './typography-palette-anti-repeat/color-utils.mjs';
|
|
51
|
+
|
|
52
|
+
const DEFAULT_OKLCH_DISTANCE_THRESHOLD = 0.04;
|
|
53
|
+
|
|
54
|
+
function findTypographyViolationsForFile({ relativeFilePath, sourceText, ledgerFamilies }) {
|
|
55
|
+
const violations = [];
|
|
56
|
+
const fontFamilyOccurrences = extractFontFamiliesFromText(sourceText);
|
|
57
|
+
for (const familyOccurrence of fontFamilyOccurrences) {
|
|
58
|
+
const ledgerEntry = ledgerFamilies.get(familyOccurrence.family);
|
|
59
|
+
if (!ledgerEntry) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
violations.push({
|
|
63
|
+
file: relativeFilePath,
|
|
64
|
+
line: lineNumberFromIndex(sourceText, familyOccurrence.index),
|
|
65
|
+
kind: 'typography.previously-shipped-family',
|
|
66
|
+
severity: 'blocking',
|
|
67
|
+
diagnosticCode: 'BOUNDARY_TYPOGRAPHY_LEDGER_VIOLATION',
|
|
68
|
+
detail: `CSS declares font-family "${familyOccurrence.family}" which appears in researchDossier.metadata.antiRepeatLedger.previousTypographyChoices ("${ledgerEntry.summary}"). Either escape the previously-shipped trio, or set derivedTokenLogic.tokenContinuityClassification.typography to continuity-retained with explicit rationale.`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return violations;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildHexAdvisory({ relativeFilePath, sourceText, colorOccurrence, ledgerColor, paletteSeverity, paletteDiagnosticCode }) {
|
|
75
|
+
return {
|
|
76
|
+
file: relativeFilePath,
|
|
77
|
+
line: lineNumberFromIndex(sourceText, colorOccurrence.index),
|
|
78
|
+
kind: 'palette.previously-shipped-color',
|
|
79
|
+
severity: paletteSeverity,
|
|
80
|
+
diagnosticCode: paletteDiagnosticCode,
|
|
81
|
+
detail: `CSS hex "${colorOccurrence.raw}" matches a hex color in researchDossier.metadata.antiRepeatLedger.previousPalettes ("${ledgerColor.sourceEntry.summary}"). Either escape the previously-shipped palette or set derivedTokenLogic.tokenContinuityClassification.palette to continuity-retained with explicit rationale.`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildOklchAdvisory({ relativeFilePath, sourceText, colorOccurrence, ledgerColor, perceptualDistance, oklchDistanceThreshold, paletteSeverity, paletteDiagnosticCode }) {
|
|
86
|
+
return {
|
|
87
|
+
file: relativeFilePath,
|
|
88
|
+
line: lineNumberFromIndex(sourceText, colorOccurrence.index),
|
|
89
|
+
kind: 'palette.previously-shipped-color',
|
|
90
|
+
severity: paletteSeverity,
|
|
91
|
+
diagnosticCode: paletteDiagnosticCode,
|
|
92
|
+
detail: `CSS OKLCH "${colorOccurrence.raw}" is within ${perceptualDistance.toFixed(4)} of an OKLCH color in researchDossier.metadata.antiRepeatLedger.previousPalettes ("${ledgerColor.sourceEntry.summary}") (threshold ${oklchDistanceThreshold}). Heuristic match on perceptually-uniform OKLCH distance; either escape the previously-shipped palette, raise the threshold via --threshold, or set derivedTokenLogic.tokenContinuityClassification.palette to continuity-retained with explicit rationale.`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function findPaletteFindingsForFile({ relativeFilePath, sourceText, ledgerColors, oklchDistanceThreshold, paletteSeverity, paletteDiagnosticCode }) {
|
|
97
|
+
const findings = [];
|
|
98
|
+
const colorOccurrences = extractColorOccurrencesFromText(sourceText);
|
|
99
|
+
for (const colorOccurrence of colorOccurrences) {
|
|
100
|
+
for (const ledgerColor of ledgerColors) {
|
|
101
|
+
if (colorOccurrence.kind === 'hex' && ledgerColor.kind === 'hex') {
|
|
102
|
+
if (
|
|
103
|
+
colorOccurrence.normalized
|
|
104
|
+
&& ledgerColor.normalized
|
|
105
|
+
&& colorOccurrence.normalized === ledgerColor.normalized
|
|
106
|
+
) {
|
|
107
|
+
findings.push(buildHexAdvisory({
|
|
108
|
+
relativeFilePath, sourceText, colorOccurrence, ledgerColor,
|
|
109
|
+
paletteSeverity, paletteDiagnosticCode,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (colorOccurrence.kind === 'oklch' && ledgerColor.kind === 'oklch' && colorOccurrence.oklch && ledgerColor.oklch) {
|
|
115
|
+
const perceptualDistance = oklchPerceptualDistance(colorOccurrence.oklch, ledgerColor.oklch);
|
|
116
|
+
if (perceptualDistance <= oklchDistanceThreshold) {
|
|
117
|
+
findings.push(buildOklchAdvisory({
|
|
118
|
+
relativeFilePath, sourceText, colorOccurrence, ledgerColor, perceptualDistance, oklchDistanceThreshold,
|
|
119
|
+
paletteSeverity, paletteDiagnosticCode,
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Cross-type (hex declared vs OKLCH ledger or vice versa) is intentionally
|
|
124
|
+
// skipped here; reporting cross-type would require a color-space conversion
|
|
125
|
+
// pass that is its own correctness surface. Tracked in
|
|
126
|
+
// docs/deep-analysis-and-roadmap-backlog.md.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return findings;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function loadDesignIntentContract(repositoryRootPath, designIntentRelativePath) {
|
|
133
|
+
const designIntentAbsolutePath = join(repositoryRootPath, designIntentRelativePath);
|
|
134
|
+
if (!existsSync(designIntentAbsolutePath)) {
|
|
135
|
+
return { contract: null, reason: 'design-intent-file-absent' };
|
|
136
|
+
}
|
|
137
|
+
let contract;
|
|
138
|
+
try {
|
|
139
|
+
contract = JSON.parse(readFileSync(designIntentAbsolutePath, 'utf8'));
|
|
140
|
+
} catch (parseError) {
|
|
141
|
+
const errorMessage = parseError instanceof Error ? parseError.message : String(parseError);
|
|
142
|
+
return { contract: null, reason: `design-intent-file-not-valid-json: ${errorMessage}` };
|
|
143
|
+
}
|
|
144
|
+
return { contract, reason: 'ok' };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildSkippedReport(reportShell, reason) {
|
|
148
|
+
return {
|
|
149
|
+
...reportShell,
|
|
150
|
+
passed: true,
|
|
151
|
+
skipped: true,
|
|
152
|
+
reason,
|
|
153
|
+
filesScanned: 0,
|
|
154
|
+
typographyViolationCount: 0,
|
|
155
|
+
paletteFindingCount: 0,
|
|
156
|
+
typographyViolations: [],
|
|
157
|
+
paletteFindings: [],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Runs the typography and palette anti-repeat audit.
|
|
163
|
+
*
|
|
164
|
+
* @param {object} [auditOptions]
|
|
165
|
+
* @param {string} [auditOptions.repositoryRootPath]
|
|
166
|
+
* @param {string[]} [auditOptions.scanRoots]
|
|
167
|
+
* @param {string} [auditOptions.designIntentRelativePath]
|
|
168
|
+
* @param {number} [auditOptions.oklchDistanceThreshold]
|
|
169
|
+
* @param {boolean} [auditOptions.treatPaletteAsAdvisory]
|
|
170
|
+
*/
|
|
171
|
+
export function runTypographyPaletteAntiRepeatAudit(auditOptions = {}) {
|
|
172
|
+
const repositoryRootPath = auditOptions.repositoryRootPath || process.cwd();
|
|
173
|
+
const scanRoots = Array.isArray(auditOptions.scanRoots) && auditOptions.scanRoots.length > 0
|
|
174
|
+
? auditOptions.scanRoots
|
|
175
|
+
: ['.'];
|
|
176
|
+
const designIntentRelativePath = auditOptions.designIntentRelativePath || 'docs/design-intent.json';
|
|
177
|
+
const oklchDistanceThreshold = typeof auditOptions.oklchDistanceThreshold === 'number'
|
|
178
|
+
? auditOptions.oklchDistanceThreshold
|
|
179
|
+
: DEFAULT_OKLCH_DISTANCE_THRESHOLD;
|
|
180
|
+
const treatPaletteAsAdvisory = auditOptions.treatPaletteAsAdvisory === true;
|
|
181
|
+
const paletteSeverity = treatPaletteAsAdvisory ? 'advisory' : 'blocking';
|
|
182
|
+
const paletteDiagnosticCode = treatPaletteAsAdvisory
|
|
183
|
+
? 'BOUNDARY_PALETTE_LEDGER_ADVISORY'
|
|
184
|
+
: 'BOUNDARY_PALETTE_LEDGER_VIOLATION';
|
|
185
|
+
|
|
186
|
+
const reportShell = {
|
|
187
|
+
auditName: 'audit-typography-palette-anti-repeat',
|
|
188
|
+
reportVersion: '2.0.0',
|
|
189
|
+
generatedAt: new Date().toISOString(),
|
|
190
|
+
designIntentRelativePath,
|
|
191
|
+
paletteSeverity,
|
|
192
|
+
paletteDiagnosticCode,
|
|
193
|
+
oklchDistanceThreshold,
|
|
194
|
+
scope: {
|
|
195
|
+
typographyMatching: 'exact-family-name-after-normalization',
|
|
196
|
+
paletteHexMatching: 'exact-hex-after-7-digit-normalization',
|
|
197
|
+
paletteOklchMatching: 'l*c*h-distance-under-threshold',
|
|
198
|
+
paletteCrossTypeMatching: 'not-supported-color-conversion-out-of-scope',
|
|
199
|
+
paletteCrossTypeMatchingTrackedIn: 'docs/deep-analysis-and-roadmap-backlog.md',
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const designIntentLoad = loadDesignIntentContract(repositoryRootPath, designIntentRelativePath);
|
|
204
|
+
if (!designIntentLoad.contract) {
|
|
205
|
+
return buildSkippedReport(reportShell, designIntentLoad.reason);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const antiRepeatLedger = designIntentLoad.contract?.researchDossier?.metadata?.antiRepeatLedger;
|
|
209
|
+
if (!antiRepeatLedger || typeof antiRepeatLedger !== 'object') {
|
|
210
|
+
return buildSkippedReport(reportShell, 'anti-repeat-ledger-absent');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const ledgerFamilies = extractLedgerTypographyFamilies(antiRepeatLedger);
|
|
214
|
+
const ledgerColors = extractLedgerColors(antiRepeatLedger);
|
|
215
|
+
const scannableFilePaths = collectScannableFilePaths(repositoryRootPath, scanRoots);
|
|
216
|
+
|
|
217
|
+
/** @type {any[]} */
|
|
218
|
+
const typographyViolations = [];
|
|
219
|
+
/** @type {any[]} */
|
|
220
|
+
const paletteFindings = [];
|
|
221
|
+
|
|
222
|
+
for (const absoluteFilePath of scannableFilePaths) {
|
|
223
|
+
const relativeFilePath = relative(repositoryRootPath, absoluteFilePath).replace(/\\/g, '/');
|
|
224
|
+
let sourceText;
|
|
225
|
+
try {
|
|
226
|
+
sourceText = readFileSync(absoluteFilePath, 'utf8');
|
|
227
|
+
} catch {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (ledgerFamilies.size > 0) {
|
|
231
|
+
typographyViolations.push(...findTypographyViolationsForFile({
|
|
232
|
+
relativeFilePath, sourceText, ledgerFamilies,
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
if (ledgerColors.length > 0) {
|
|
236
|
+
paletteFindings.push(...findPaletteFindingsForFile({
|
|
237
|
+
relativeFilePath, sourceText, ledgerColors, oklchDistanceThreshold,
|
|
238
|
+
paletteSeverity, paletteDiagnosticCode,
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const blockingViolationCount = typographyViolations.length + (treatPaletteAsAdvisory ? 0 : paletteFindings.length);
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
...reportShell,
|
|
247
|
+
passed: blockingViolationCount === 0,
|
|
248
|
+
skipped: false,
|
|
249
|
+
filesScanned: scannableFilePaths.length,
|
|
250
|
+
typographyViolationCount: typographyViolations.length,
|
|
251
|
+
paletteFindingCount: paletteFindings.length,
|
|
252
|
+
typographyViolations,
|
|
253
|
+
paletteFindings,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `agentic-senior-core audit:design-anti-repeat`
|
|
5
|
+
*
|
|
6
|
+
* User-facing CLI subcommand that runs the typography/palette anti-repeat
|
|
7
|
+
* audit against a user project's working directory. The default scan root is
|
|
8
|
+
* `process.cwd()` so `npx @ryuenn3123/agentic-senior-core audit:design-anti-repeat`
|
|
9
|
+
* inside a user project scans that project (not the package install).
|
|
10
|
+
*
|
|
11
|
+
* Skip behavior is friendly: missing design-intent.json or missing ledger
|
|
12
|
+
* print actionable next steps and exit 0. Typography violations and palette
|
|
13
|
+
* findings (when palette is blocking, which is the default) exit 1.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { resolve } from 'node:path';
|
|
17
|
+
|
|
18
|
+
import { runTypographyPaletteAntiRepeatAudit } from '../audits/typography-palette-anti-repeat-audit.mjs';
|
|
19
|
+
|
|
20
|
+
const HELP_TEXT_LINES = [
|
|
21
|
+
'agentic-senior-core audit:design-anti-repeat',
|
|
22
|
+
'',
|
|
23
|
+
'Scan CSS, SCSS, SASS, LESS, and Tailwind/theme/design-token config files in',
|
|
24
|
+
'the current project for typography or palette values that match the',
|
|
25
|
+
'anti-repeat ledger in docs/design-intent.json. Catches redesigns that leak',
|
|
26
|
+
"the previous direction's font trio or palette through CSS implementation",
|
|
27
|
+
'even when the JSON design contract claims a fresh anchor.',
|
|
28
|
+
'',
|
|
29
|
+
'Usage:',
|
|
30
|
+
' agentic-senior-core audit:design-anti-repeat',
|
|
31
|
+
' agentic-senior-core audit:design-anti-repeat [target-directory]',
|
|
32
|
+
' agentic-senior-core audit:design-anti-repeat --json',
|
|
33
|
+
' agentic-senior-core audit:design-anti-repeat --palette-advisory',
|
|
34
|
+
' agentic-senior-core audit:design-anti-repeat --threshold 0.05',
|
|
35
|
+
'',
|
|
36
|
+
'Defaults:',
|
|
37
|
+
' Target directory: current working directory',
|
|
38
|
+
' Typography matches: blocking (BOUNDARY_TYPOGRAPHY_LEDGER_VIOLATION)',
|
|
39
|
+
' Palette matches: blocking (BOUNDARY_PALETTE_LEDGER_VIOLATION)',
|
|
40
|
+
' OKLCH distance: 0.04 in L*C*H space',
|
|
41
|
+
'',
|
|
42
|
+
'Options:',
|
|
43
|
+
' --json Print the full report as JSON only (no human summary).',
|
|
44
|
+
' --palette-advisory Opt out of blocking palette severity. Findings still',
|
|
45
|
+
' print but do not fail the run. Typography stays blocking.',
|
|
46
|
+
' --threshold <num> Override the OKLCH perceptual distance threshold.',
|
|
47
|
+
' Higher values are more permissive.',
|
|
48
|
+
' --help Show this help.',
|
|
49
|
+
'',
|
|
50
|
+
'Skip behavior (exit 0):',
|
|
51
|
+
' - docs/design-intent.json absent. Run `npx @ryuenn3123/agentic-senior-core init`',
|
|
52
|
+
' in a fresh project, or `... upgrade` in an existing project to seed the',
|
|
53
|
+
' design contract.',
|
|
54
|
+
' - docs/design-intent.json present but researchDossier metadata absent.',
|
|
55
|
+
" Run `... upgrade` to migrate the contract, then run `.agent-context/prompts/research-design.md`",
|
|
56
|
+
' so the dossier and anti-repeat ledger get populated before the next UI work.',
|
|
57
|
+
'',
|
|
58
|
+
'Exit codes:',
|
|
59
|
+
' 0 = no blocking violations (or audit skipped with a friendly note).',
|
|
60
|
+
' 1 = at least one BOUNDARY_TYPOGRAPHY_LEDGER_VIOLATION or, in default mode,',
|
|
61
|
+
' BOUNDARY_PALETTE_LEDGER_VIOLATION.',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function buildHelpText() {
|
|
65
|
+
return HELP_TEXT_LINES.join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readNumericFlag(commandLineArgs, flagName) {
|
|
69
|
+
const flagIndex = commandLineArgs.indexOf(flagName);
|
|
70
|
+
if (flagIndex === -1) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const flagValue = commandLineArgs[flagIndex + 1];
|
|
74
|
+
if (typeof flagValue !== 'string' || flagValue.length === 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const numericValue = Number(flagValue);
|
|
78
|
+
return Number.isFinite(numericValue) ? numericValue : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const VALUE_FLAGS = new Set(['--threshold']);
|
|
82
|
+
|
|
83
|
+
function extractTargetDirectoryArgument(commandLineArgs) {
|
|
84
|
+
for (let argumentIndex = 0; argumentIndex < commandLineArgs.length; argumentIndex += 1) {
|
|
85
|
+
const candidateArgument = commandLineArgs[argumentIndex];
|
|
86
|
+
if (VALUE_FLAGS.has(candidateArgument)) {
|
|
87
|
+
argumentIndex += 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!candidateArgument.startsWith('--')) {
|
|
91
|
+
return candidateArgument;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildSkipMessage(reason) {
|
|
98
|
+
if (reason === 'design-intent-file-absent') {
|
|
99
|
+
return [
|
|
100
|
+
'Audit skipped: docs/design-intent.json is missing.',
|
|
101
|
+
'Next step:',
|
|
102
|
+
' - Fresh project: run `npx @ryuenn3123/agentic-senior-core init` to seed the design contract.',
|
|
103
|
+
' - Existing project: run `npx @ryuenn3123/agentic-senior-core upgrade` to seed it without touching app code.',
|
|
104
|
+
'Then re-run this audit.',
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
if (reason === 'anti-repeat-ledger-absent') {
|
|
108
|
+
return [
|
|
109
|
+
'Audit skipped: docs/design-intent.json is present but researchDossier metadata is absent.',
|
|
110
|
+
'Next step:',
|
|
111
|
+
' - Run `npx @ryuenn3123/agentic-senior-core upgrade` to migrate the contract; the upgrade injects',
|
|
112
|
+
' `researchDossier.metadata` and seeds the anti-repeat ledger from existing anchor, palette,',
|
|
113
|
+
' motion, and typography fields without overwriting them.',
|
|
114
|
+
" - Then run the design-research dossier prompt at `.agent-context/prompts/research-design.md`",
|
|
115
|
+
' so the ledger and `researchVerifiedAt` get populated before the next UI work.',
|
|
116
|
+
'After that, re-run this audit to enforce the ledger against your CSS.',
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
if (typeof reason === 'string' && reason.startsWith('design-intent-file-not-valid-json')) {
|
|
120
|
+
return [
|
|
121
|
+
'Audit skipped: docs/design-intent.json is present but cannot be parsed as JSON.',
|
|
122
|
+
` ${reason}`,
|
|
123
|
+
'Next step: fix the JSON syntax, then re-run this audit.',
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
return [`Audit skipped: ${reason || 'unknown reason'}`];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function printHumanReport(auditReport) {
|
|
130
|
+
console.log('===============================================');
|
|
131
|
+
console.log(' audit:design-anti-repeat');
|
|
132
|
+
console.log('===============================================');
|
|
133
|
+
console.log(` Target directory: ${auditReport.targetDirectoryPath}`);
|
|
134
|
+
console.log(` Files scanned: ${auditReport.filesScanned}`);
|
|
135
|
+
console.log(` Typography violations: ${auditReport.typographyViolationCount} (blocking)`);
|
|
136
|
+
console.log(` Palette findings: ${auditReport.paletteFindingCount} (${auditReport.paletteSeverity})`);
|
|
137
|
+
console.log(` OKLCH distance threshold: ${auditReport.oklchDistanceThreshold}`);
|
|
138
|
+
console.log('');
|
|
139
|
+
|
|
140
|
+
if (auditReport.skipped) {
|
|
141
|
+
for (const skipLine of buildSkipMessage(auditReport.reason)) {
|
|
142
|
+
console.log(` ${skipLine}`);
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (auditReport.typographyViolations.length > 0) {
|
|
148
|
+
console.log(' Typography violations (blocking):');
|
|
149
|
+
for (const violation of auditReport.typographyViolations) {
|
|
150
|
+
console.log(` [${violation.kind}] ${violation.file}:${violation.line} ${violation.detail}`);
|
|
151
|
+
}
|
|
152
|
+
console.log('');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (auditReport.paletteFindings.length > 0) {
|
|
156
|
+
console.log(` Palette findings (${auditReport.paletteSeverity}):`);
|
|
157
|
+
for (const finding of auditReport.paletteFindings) {
|
|
158
|
+
console.log(` [${finding.kind}] ${finding.file}:${finding.line} ${finding.detail}`);
|
|
159
|
+
}
|
|
160
|
+
console.log('');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (auditReport.passed) {
|
|
164
|
+
console.log(' No blocking violations against the anti-repeat ledger.');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const blockingPaletteCount = auditReport.paletteSeverity === 'blocking' ? auditReport.paletteFindingCount : 0;
|
|
169
|
+
console.log(` ${auditReport.typographyViolationCount} typography violation(s) and ${blockingPaletteCount} palette violation(s) found; release blocked.`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function runDesignAntiRepeatAuditCommand(commandLineArgs = []) {
|
|
173
|
+
if (commandLineArgs.includes('--help') || commandLineArgs.includes('-h')) {
|
|
174
|
+
console.log(buildHelpText());
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const shouldOutputJsonOnly = commandLineArgs.includes('--json');
|
|
179
|
+
const shouldTreatPaletteAsAdvisory = commandLineArgs.includes('--palette-advisory');
|
|
180
|
+
const oklchDistanceThreshold = readNumericFlag(commandLineArgs, '--threshold');
|
|
181
|
+
const targetDirectoryArgument = extractTargetDirectoryArgument(commandLineArgs);
|
|
182
|
+
const targetDirectoryPath = resolve(targetDirectoryArgument || process.cwd());
|
|
183
|
+
|
|
184
|
+
const auditReport = runTypographyPaletteAntiRepeatAudit({
|
|
185
|
+
repositoryRootPath: targetDirectoryPath,
|
|
186
|
+
treatPaletteAsAdvisory: shouldTreatPaletteAsAdvisory,
|
|
187
|
+
...(typeof oklchDistanceThreshold === 'number' ? { oklchDistanceThreshold } : {}),
|
|
188
|
+
});
|
|
189
|
+
const auditReportWithRoot = { ...auditReport, targetDirectoryPath };
|
|
190
|
+
|
|
191
|
+
if (shouldOutputJsonOnly) {
|
|
192
|
+
process.stdout.write(`${JSON.stringify(auditReportWithRoot, null, 2)}\n`);
|
|
193
|
+
return auditReportWithRoot.passed ? 0 : 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
printHumanReport(auditReportWithRoot);
|
|
197
|
+
return auditReportWithRoot.passed ? 0 : 1;
|
|
198
|
+
}
|
|
@@ -464,6 +464,7 @@ export async function runUpgradeCommand(targetDirectoryArgument, upgradeOptions
|
|
|
464
464
|
}
|
|
465
465
|
|
|
466
466
|
console.log('\nRefreshed files: AGENTS.md, CLAUDE.md, GEMINI.md, .agent-context/, and .agent-context/state/onboarding-report.json');
|
|
467
|
+
console.log('\nNext-step suggestion (UI scope): run `npx @ryuenn3123/agentic-senior-core audit:design-anti-repeat` to scan CSS, SCSS, SASS, LESS, Tailwind config, and design-token files in this project for typography or palette values that match the anti-repeat ledger in docs/design-intent.json. Add it to your CI alongside `npm test` once the design dossier is populated.');
|
|
467
468
|
} catch (error) {
|
|
468
469
|
console.error('\n[FATAL] An error occurred during upgrade. Attempting automatic rollback...');
|
|
469
470
|
try {
|
|
@@ -84,6 +84,28 @@ function buildPreviousMotionEntry(designIntentContract) {
|
|
|
84
84
|
}];
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function buildPreviousTypographyEntry(designIntentContract) {
|
|
88
|
+
if (!designIntentContract || typeof designIntentContract !== 'object') {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const tokenSystem = designIntentContract?.tokenSystem;
|
|
92
|
+
const typographyTokens = tokenSystem && typeof tokenSystem === 'object' ? tokenSystem.typographyTokens : null;
|
|
93
|
+
if (!typographyTokens || typeof typographyTokens !== 'object') {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const tokenEntries = Object.entries(typographyTokens)
|
|
97
|
+
.filter(([, value]) => typeof value === 'string' && value.trim().length > 0)
|
|
98
|
+
.map(([role, value]) => `${role}: ${value.trim()}`);
|
|
99
|
+
if (tokenEntries.length === 0) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
return [{
|
|
103
|
+
summary: tokenEntries.join('; '),
|
|
104
|
+
source: 'migrated-from-existing-design-intent',
|
|
105
|
+
blockedBecause: 'previously-shipped-typography-trio',
|
|
106
|
+
}];
|
|
107
|
+
}
|
|
108
|
+
|
|
87
109
|
export function buildResearchDossierMetadata({
|
|
88
110
|
designIntentContract = null,
|
|
89
111
|
populateLedgerFromExistingContract = false,
|
|
@@ -99,6 +121,7 @@ export function buildResearchDossierMetadata({
|
|
|
99
121
|
previousAnchors: [],
|
|
100
122
|
previousPalettes: [],
|
|
101
123
|
previousMotionSignatures: [],
|
|
124
|
+
previousTypographyChoices: [],
|
|
102
125
|
},
|
|
103
126
|
userExplicitRedesignBypassesFreshness: true,
|
|
104
127
|
statusAwareValidation: {
|
|
@@ -116,6 +139,7 @@ export function buildResearchDossierMetadata({
|
|
|
116
139
|
metadata.antiRepeatLedger.previousAnchors = buildPreviousAnchorEntry(designIntentContract).slice(0, metadata.antiRepeatLedger.ledgerMaxEntriesPerCategory);
|
|
117
140
|
metadata.antiRepeatLedger.previousPalettes = buildPreviousPaletteEntry(designIntentContract).slice(0, metadata.antiRepeatLedger.ledgerMaxEntriesPerCategory);
|
|
118
141
|
metadata.antiRepeatLedger.previousMotionSignatures = buildPreviousMotionEntry(designIntentContract).slice(0, metadata.antiRepeatLedger.ledgerMaxEntriesPerCategory);
|
|
142
|
+
metadata.antiRepeatLedger.previousTypographyChoices = buildPreviousTypographyEntry(designIntentContract).slice(0, metadata.antiRepeatLedger.ledgerMaxEntriesPerCategory);
|
|
119
143
|
}
|
|
120
144
|
|
|
121
145
|
return metadata;
|
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
|
|
8
8
|
import { hasNonEmptyString } from './helpers.mjs';
|
|
9
9
|
|
|
10
|
+
const SEED_STATUSES = new Set([
|
|
11
|
+
'seed-needs-design-synthesis',
|
|
12
|
+
'seed-generated-during-init',
|
|
13
|
+
'seed-generated-during-upgrade',
|
|
14
|
+
]);
|
|
15
|
+
const REQUIRED_TOKEN_CATEGORIES = ['typography', 'palette', 'motion', 'spacing'];
|
|
16
|
+
const ACTIVE_CLASSIFICATION_VALUES = new Set(['anchor-derived', 'continuity-retained', 'newly-introduced']);
|
|
17
|
+
const SEED_CLASSIFICATION_VALUES = new Set(['anchor-derived', 'continuity-retained', 'newly-introduced', 'pending-research']);
|
|
18
|
+
|
|
10
19
|
export function validateDesignContractCompleteness(designIntentContract) {
|
|
11
20
|
const validationIssues = [];
|
|
12
21
|
const conceptualAnchor = designIntentContract?.conceptualAnchor;
|
|
@@ -47,6 +56,35 @@ export function validateDesignContractCompleteness(designIntentContract) {
|
|
|
47
56
|
) {
|
|
48
57
|
validationIssues.push('designIntent.derivedTokenLogic.validationRule must require traceability to anchorReference.');
|
|
49
58
|
}
|
|
59
|
+
|
|
60
|
+
const tokenContinuityClassification = derivedTokenLogic.tokenContinuityClassification;
|
|
61
|
+
const isSeedStatus = SEED_STATUSES.has(designIntentContract?.status);
|
|
62
|
+
const allowedClassificationValues = isSeedStatus ? SEED_CLASSIFICATION_VALUES : ACTIVE_CLASSIFICATION_VALUES;
|
|
63
|
+
if (!tokenContinuityClassification || typeof tokenContinuityClassification !== 'object') {
|
|
64
|
+
validationIssues.push('designIntent.derivedTokenLogic.tokenContinuityClassification must exist. Each token category (typography, palette, motion, spacing) must declare anchor-derived, continuity-retained, or newly-introduced (pending-research is allowed only on seed contracts).');
|
|
65
|
+
} else {
|
|
66
|
+
if (
|
|
67
|
+
!Array.isArray(tokenContinuityClassification.validValues)
|
|
68
|
+
|| !tokenContinuityClassification.validValues.includes('anchor-derived')
|
|
69
|
+
|| !tokenContinuityClassification.validValues.includes('continuity-retained')
|
|
70
|
+
|| !tokenContinuityClassification.validValues.includes('newly-introduced')
|
|
71
|
+
|| !tokenContinuityClassification.validValues.includes('pending-research')
|
|
72
|
+
) {
|
|
73
|
+
validationIssues.push('designIntent.derivedTokenLogic.tokenContinuityClassification.validValues must include anchor-derived, continuity-retained, newly-introduced, and pending-research.');
|
|
74
|
+
}
|
|
75
|
+
if (!hasNonEmptyString(tokenContinuityClassification.rule)) {
|
|
76
|
+
validationIssues.push('designIntent.derivedTokenLogic.tokenContinuityClassification.rule must explain when each value applies.');
|
|
77
|
+
}
|
|
78
|
+
for (const tokenCategoryName of REQUIRED_TOKEN_CATEGORIES) {
|
|
79
|
+
const classifiedValue = tokenContinuityClassification[tokenCategoryName];
|
|
80
|
+
if (!allowedClassificationValues.has(classifiedValue)) {
|
|
81
|
+
const allowedLabel = isSeedStatus
|
|
82
|
+
? 'anchor-derived, continuity-retained, newly-introduced, or pending-research'
|
|
83
|
+
: 'anchor-derived, continuity-retained, or newly-introduced';
|
|
84
|
+
validationIssues.push(`designIntent.derivedTokenLogic.tokenContinuityClassification.${tokenCategoryName} must be one of: ${allowedLabel}. Got: "${classifiedValue}".`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
50
88
|
}
|
|
51
89
|
|
|
52
90
|
if (!['verified', 'pending-verification', 'no-external-library-needed'].includes(designIntentContract?.libraryResearchStatus)) {
|
package/lib/cli/project-scaffolder/design-contract/validation/research-dossier-validators.mjs
CHANGED
|
@@ -12,7 +12,7 @@ const REQUIRED_SEED_STATUSES = [
|
|
|
12
12
|
'seed-generated-during-init',
|
|
13
13
|
'seed-generated-during-upgrade',
|
|
14
14
|
];
|
|
15
|
-
const ANTI_REPEAT_LEDGER_CATEGORIES = ['previousAnchors', 'previousPalettes', 'previousMotionSignatures'];
|
|
15
|
+
const ANTI_REPEAT_LEDGER_CATEGORIES = ['previousAnchors', 'previousPalettes', 'previousMotionSignatures', 'previousTypographyChoices'];
|
|
16
16
|
|
|
17
17
|
function validateMetadataBaseShape(metadata, validationErrors) {
|
|
18
18
|
if (
|
|
@@ -139,6 +139,14 @@ function buildDesignIntentContractObject({
|
|
|
139
139
|
typeScaleMethod: 'Prefer fluid clamp() type scales when supported; name ratio, role contrast, balance/wrap behavior, and numeric typography needs.',
|
|
140
140
|
motionBudget: 'Name micro, layout, entrance, easing, stagger, and reduced-motion budgets; prefer transform/opacity for high-frequency motion.',
|
|
141
141
|
validationRule: 'Every semantic token role must trace to anchorReference; keep exact primitive values flexible unless locked by repo evidence, accessibility validation, implementation constraints, or explicit user approval.',
|
|
142
|
+
tokenContinuityClassification: {
|
|
143
|
+
typography: 'pending-research',
|
|
144
|
+
palette: 'pending-research',
|
|
145
|
+
motion: 'pending-research',
|
|
146
|
+
spacing: 'pending-research',
|
|
147
|
+
validValues: ['anchor-derived', 'continuity-retained', 'newly-introduced', 'pending-research'],
|
|
148
|
+
rule: 'For each token category, classify whether the choice is anchor-derived (causally tied to anchorReference real-world reality), continuity-retained (kept from a previous design iteration without re-derivation), or newly-introduced (fresh choice not tied to anchor). Continuity-retained is acceptable; pretending continuity is derivation is not.',
|
|
149
|
+
},
|
|
142
150
|
},
|
|
143
151
|
motionPaletteDecision: buildMotionPaletteDecisionSection(),
|
|
144
152
|
aiSafeUiAudit: buildAiSafeUiAuditSection({ projectName }),
|
package/lib/cli/utils.mjs
CHANGED
|
@@ -58,6 +58,7 @@ export function printUsage() {
|
|
|
58
58
|
console.log(' agentic-senior-core optimize [target-directory] [--agent <copilot|claude|cursor|windsurf|gemini|codex|cline>] [--enable|--disable] [--show]');
|
|
59
59
|
console.log(' agentic-senior-core mcp');
|
|
60
60
|
console.log(' agentic-senior-core rollback [target-directory]');
|
|
61
|
+
console.log(' agentic-senior-core audit:design-anti-repeat [target-directory] [--json] [--palette-advisory] [--threshold <number>]');
|
|
61
62
|
console.log(' agentic-senior-core --version');
|
|
62
63
|
console.log('');
|
|
63
64
|
console.log('Options:');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryuenn3123/agentic-senior-core",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Force your AI Agent to code like a Staff Engineer, not a Junior.",
|
|
6
6
|
"bin": {
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"audit:cache-layer-contract": "node ./scripts/audit-cache-layer-contract.mjs",
|
|
60
60
|
"audit:reflection-citations": "node ./scripts/audit-reflection-citations.mjs",
|
|
61
61
|
"audit:caching-scope-hygiene": "node ./scripts/audit-caching-scope-hygiene.mjs",
|
|
62
|
+
"audit:typography-palette-anti-repeat": "node ./scripts/audit-typography-palette-anti-repeat.mjs",
|
|
62
63
|
"audit:release-bundle": "node ./scripts/audit-release-bundle.mjs",
|
|
63
64
|
"audit:file-size": "node ./scripts/audit-file-size.mjs",
|
|
64
65
|
"audit:rule-id-uniqueness": "node ./scripts/audit-rule-id-uniqueness.mjs",
|
|
@@ -83,7 +84,7 @@
|
|
|
83
84
|
"report:governance-weekly": "node ./scripts/governance-weekly-report.mjs",
|
|
84
85
|
"clean:local": "node ./scripts/clean-local-artifacts.mjs",
|
|
85
86
|
"validate": "node ./scripts/validate.mjs",
|
|
86
|
-
"test": "node --test ./tests/cli-smoke.test.mjs ./tests/mcp-server.test.mjs ./tests/llm-judge.test.mjs ./tests/ui-rubric-calibration.test.mjs ./tests/operations.test.mjs ./tests/knowledge-injection.test.mjs ./tests/migrate-rule-format.test.mjs ./tests/audit-caching-scope-hygiene.test.mjs ./tests/research-dossier-migration.test.mjs ./benchmarks/token-usage/lib/token-counter.test.mjs ./benchmarks/token-usage/lib/provider-cache-matrix.test.mjs ./benchmarks/token-usage/lib/cache-layer-contract.test.mjs ./benchmarks/token-usage/lib/cache-economics.test.mjs"
|
|
87
|
+
"test": "node --test ./tests/cli-smoke.test.mjs ./tests/mcp-server.test.mjs ./tests/llm-judge.test.mjs ./tests/ui-rubric-calibration.test.mjs ./tests/operations.test.mjs ./tests/knowledge-injection.test.mjs ./tests/migrate-rule-format.test.mjs ./tests/audit-caching-scope-hygiene.test.mjs ./tests/research-dossier-migration.test.mjs ./tests/typography-palette-anti-repeat-audit.test.mjs ./tests/audit-design-anti-repeat-command.test.mjs ./benchmarks/token-usage/lib/token-counter.test.mjs ./benchmarks/token-usage/lib/provider-cache-matrix.test.mjs ./benchmarks/token-usage/lib/cache-layer-contract.test.mjs ./benchmarks/token-usage/lib/cache-economics.test.mjs"
|
|
87
88
|
},
|
|
88
89
|
"devDependencies": {
|
|
89
90
|
"@anthropic-ai/sdk": "^0.96.0",
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* audit-typography-palette-anti-repeat.mjs
|
|
6
|
+
*
|
|
7
|
+
* Thin CLI wrapper around lib/cli/audits/typography-palette-anti-repeat-audit.mjs.
|
|
8
|
+
* The wrapper defaults to scanning the current working directory so it works
|
|
9
|
+
* for both repo-internal `npm run audit:typography-palette-anti-repeat` (npm
|
|
10
|
+
* sets cwd to the repo root) and direct invocation from a user project. The
|
|
11
|
+
* user-facing entry point is the `audit:design-anti-repeat` subcommand on
|
|
12
|
+
* the published bin (see lib/cli/commands/audit-design-anti-repeat.mjs).
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* node scripts/audit-typography-palette-anti-repeat.mjs
|
|
16
|
+
* node scripts/audit-typography-palette-anti-repeat.mjs --json
|
|
17
|
+
* node scripts/audit-typography-palette-anti-repeat.mjs --palette-advisory
|
|
18
|
+
* node scripts/audit-typography-palette-anti-repeat.mjs --threshold 0.05
|
|
19
|
+
* node scripts/audit-typography-palette-anti-repeat.mjs --root /path/to/project
|
|
20
|
+
*
|
|
21
|
+
* Default severity:
|
|
22
|
+
* - Typography matches always block.
|
|
23
|
+
* - Palette matches block by default. Pass --palette-advisory to opt a
|
|
24
|
+
* project into advisory-only palette reporting (release stays unblocked
|
|
25
|
+
* by palette findings; typography still blocks).
|
|
26
|
+
*
|
|
27
|
+
* Exit codes:
|
|
28
|
+
* 0 = no blocking violations
|
|
29
|
+
* 1 = at least one BOUNDARY_TYPOGRAPHY_LEDGER_VIOLATION or, in default
|
|
30
|
+
* mode, BOUNDARY_PALETTE_LEDGER_VIOLATION
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { resolve } from 'node:path';
|
|
34
|
+
|
|
35
|
+
import { runTypographyPaletteAntiRepeatAudit } from '../lib/cli/audits/typography-palette-anti-repeat-audit.mjs';
|
|
36
|
+
|
|
37
|
+
const COMMAND_LINE_ARGS = process.argv.slice(2);
|
|
38
|
+
const SHOULD_OUTPUT_JSON_ONLY = COMMAND_LINE_ARGS.includes('--json');
|
|
39
|
+
const SHOULD_TREAT_PALETTE_AS_ADVISORY = COMMAND_LINE_ARGS.includes('--palette-advisory');
|
|
40
|
+
|
|
41
|
+
function readStringFlag(flagName) {
|
|
42
|
+
const flagIndex = COMMAND_LINE_ARGS.indexOf(flagName);
|
|
43
|
+
if (flagIndex === -1) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const flagValue = COMMAND_LINE_ARGS[flagIndex + 1];
|
|
47
|
+
if (typeof flagValue !== 'string' || flagValue.length === 0 || flagValue.startsWith('--')) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return flagValue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readNumericFlag(flagName) {
|
|
54
|
+
const stringValue = readStringFlag(flagName);
|
|
55
|
+
if (stringValue === null) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const numericValue = Number(stringValue);
|
|
59
|
+
return Number.isFinite(numericValue) ? numericValue : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function main() {
|
|
63
|
+
const oklchDistanceThreshold = readNumericFlag('--threshold');
|
|
64
|
+
const explicitRootPath = readStringFlag('--root');
|
|
65
|
+
const repositoryRootPath = resolve(explicitRootPath || process.cwd());
|
|
66
|
+
|
|
67
|
+
const auditReport = runTypographyPaletteAntiRepeatAudit({
|
|
68
|
+
repositoryRootPath,
|
|
69
|
+
treatPaletteAsAdvisory: SHOULD_TREAT_PALETTE_AS_ADVISORY,
|
|
70
|
+
...(typeof oklchDistanceThreshold === 'number' ? { oklchDistanceThreshold } : {}),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (SHOULD_OUTPUT_JSON_ONLY) {
|
|
74
|
+
process.stdout.write(`${JSON.stringify({ ...auditReport, repositoryRootPath }, null, 2)}\n`);
|
|
75
|
+
process.exit(auditReport.passed ? 0 : 1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log('===============================================');
|
|
79
|
+
console.log(' audit:typography-palette-anti-repeat');
|
|
80
|
+
console.log('===============================================');
|
|
81
|
+
console.log(` Target directory: ${repositoryRootPath}`);
|
|
82
|
+
console.log(` Files scanned: ${auditReport.filesScanned}`);
|
|
83
|
+
console.log(` Typography violations: ${auditReport.typographyViolationCount} (blocking)`);
|
|
84
|
+
console.log(` Palette findings: ${auditReport.paletteFindingCount} (${auditReport.paletteSeverity})`);
|
|
85
|
+
console.log(` OKLCH distance threshold: ${auditReport.oklchDistanceThreshold}`);
|
|
86
|
+
console.log('');
|
|
87
|
+
|
|
88
|
+
if (auditReport.skipped) {
|
|
89
|
+
console.log(` Audit skipped: ${auditReport.reason}`);
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (auditReport.typographyViolations.length > 0) {
|
|
94
|
+
console.log(' Typography violations (blocking):');
|
|
95
|
+
for (const violation of auditReport.typographyViolations) {
|
|
96
|
+
console.log(` [${violation.kind}] ${violation.file}:${violation.line} ${violation.detail}`);
|
|
97
|
+
}
|
|
98
|
+
console.log('');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (auditReport.paletteFindings.length > 0) {
|
|
102
|
+
console.log(` Palette findings (${auditReport.paletteSeverity}):`);
|
|
103
|
+
for (const finding of auditReport.paletteFindings) {
|
|
104
|
+
console.log(` [${finding.kind}] ${finding.file}:${finding.line} ${finding.detail}`);
|
|
105
|
+
}
|
|
106
|
+
console.log('');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (auditReport.passed) {
|
|
110
|
+
console.log(' No blocking violations against the anti-repeat ledger.');
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
const blockingPaletteCount = auditReport.paletteSeverity === 'blocking' ? auditReport.paletteFindingCount : 0;
|
|
114
|
+
console.log(` ${auditReport.typographyViolationCount} typography violation(s) and ${blockingPaletteCount} palette violation(s) found; release blocked.`);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (process.argv[1] && (import.meta.url === `file://${process.argv[1].replace(/\\/g, '/')}` || process.argv[1].endsWith('audit-typography-palette-anti-repeat.mjs'))) {
|
|
119
|
+
main();
|
|
120
|
+
}
|
|
@@ -74,6 +74,19 @@ const BOUNDARY_RULES = [
|
|
|
74
74
|
'README.md',
|
|
75
75
|
],
|
|
76
76
|
trigger(filePath) {
|
|
77
|
+
// Path-based exclusion: design-contract JSON shape lives under
|
|
78
|
+
// lib/cli/project-scaffolder/. Files like research-dossier-migration.mjs
|
|
79
|
+
// describe agent-prompt JSON contract migrations, not database schema
|
|
80
|
+
// or persistent-store migrations, so they must not trigger this boundary
|
|
81
|
+
// even when the filename contains the words "schema" or "migration".
|
|
82
|
+
// Tests for those design-contract migrations are excluded for the same
|
|
83
|
+
// reason.
|
|
84
|
+
if (filePath.startsWith('lib/cli/project-scaffolder/')) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (filePath.startsWith('tests/') && /research-dossier|design-contract/i.test(filePath)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
77
90
|
return !isDocumentationFilePath(filePath)
|
|
78
91
|
&& /(database|schema|migration|repository|sql|prisma|typeorm|knex)/i.test(filePath);
|
|
79
92
|
},
|
|
@@ -79,7 +79,12 @@ export function runStaticReleaseChecks(results, diagnostics) {
|
|
|
79
79
|
stackFileName: 'agent-decision-runtime.md',
|
|
80
80
|
blueprintFileName: 'agent-decision-architecture.md',
|
|
81
81
|
},
|
|
82
|
-
status:
|
|
82
|
+
// Seed status so `tokenContinuityClassification: pending-research`
|
|
83
|
+
// is a valid placeholder. This probe asserts seed-shape completeness,
|
|
84
|
+
// not active-contract behavior; an agent or user fills the contract
|
|
85
|
+
// with anchor-derived / continuity-retained / newly-introduced once
|
|
86
|
+
// the dossier is filled.
|
|
87
|
+
status: 'seed-needs-design-synthesis',
|
|
83
88
|
}));
|
|
84
89
|
const designContractIssues = validateDesignContractCompleteness(designIntentSeed);
|
|
85
90
|
|
|
@@ -376,6 +376,13 @@ export const REQUIRED_UI_DESIGN_AUTOMATION_SNIPPETS = [
|
|
|
376
376
|
'Anti-Repeat Ledger Gate',
|
|
377
377
|
'previousAnchors',
|
|
378
378
|
'previousMotionSignatures',
|
|
379
|
+
'Dimensional split',
|
|
380
|
+
'typographyClusters',
|
|
381
|
+
'paletteClusters',
|
|
382
|
+
'layoutClusters',
|
|
383
|
+
'motionClusters',
|
|
384
|
+
'imageryClusters',
|
|
385
|
+
'tokenContinuityClassification',
|
|
379
386
|
],
|
|
380
387
|
},
|
|
381
388
|
{
|
package/scripts/validate.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
20
20
|
import { ALLOWED_SEVERITIES } from './validate/config.mjs';
|
|
21
21
|
import { runCacheLayerContractAudit } from './audit-cache-layer-contract.mjs';
|
|
22
22
|
import { runCachingScopeHygieneAudit } from './audit-caching-scope-hygiene.mjs';
|
|
23
|
+
import { runTypographyPaletteAntiRepeatAudit } from '../lib/cli/audits/typography-palette-anti-repeat-audit.mjs';
|
|
23
24
|
import { runAuditFileSize } from './audit-file-size.mjs';
|
|
24
25
|
import { runReflectionCitationAudit } from './audit-reflection-citations.mjs';
|
|
25
26
|
import { runReleaseBundleAudit } from './audit-release-bundle.mjs';
|
|
@@ -153,6 +154,9 @@ async function validateRequiredFiles() {
|
|
|
153
154
|
'scripts/explain-on-demand-audit.mjs',
|
|
154
155
|
'scripts/single-source-lazy-loading-audit.mjs',
|
|
155
156
|
'scripts/audit-cache-layer-contract.mjs',
|
|
157
|
+
'scripts/audit-typography-palette-anti-repeat.mjs',
|
|
158
|
+
'lib/cli/audits/typography-palette-anti-repeat-audit.mjs',
|
|
159
|
+
'lib/cli/commands/audit-design-anti-repeat.mjs',
|
|
156
160
|
'scripts/sync-thin-adapters.mjs',
|
|
157
161
|
'scripts/v3-purge-audit.mjs',
|
|
158
162
|
'scripts/release-gate.mjs',
|
|
@@ -569,6 +573,34 @@ async function validateCachingScopeHygieneAudit() {
|
|
|
569
573
|
}
|
|
570
574
|
}
|
|
571
575
|
|
|
576
|
+
async function validateTypographyPaletteAntiRepeatAudit() {
|
|
577
|
+
console.log('\nChecking typography and palette anti-repeat ledger (audit:typography-palette-anti-repeat)...');
|
|
578
|
+
const report = runTypographyPaletteAntiRepeatAudit({ repositoryRootPath: ROOT_DIR });
|
|
579
|
+
|
|
580
|
+
if (report.skipped) {
|
|
581
|
+
pass(`Typography/palette anti-repeat audit skipped: ${report.reason}`);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (report.passed) {
|
|
586
|
+
pass(`Typography/palette anti-repeat audit clean: ${report.filesScanned} CSS/token file(s) scanned, 0 blocking typography violation(s), ${report.paletteFindingCount} palette finding(s) (${report.paletteSeverity})`);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
for (const violation of report.typographyViolations) {
|
|
591
|
+
fail(`Typography ledger violation [${violation.kind}] in ${violation.file}:${violation.line}: ${violation.detail}`);
|
|
592
|
+
}
|
|
593
|
+
if (report.paletteSeverity === 'blocking') {
|
|
594
|
+
for (const finding of report.paletteFindings) {
|
|
595
|
+
fail(`Palette ledger violation [${finding.kind}] in ${finding.file}:${finding.line}: ${finding.detail}`);
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
for (const finding of report.paletteFindings) {
|
|
599
|
+
warn(`Palette ledger advisory [${finding.kind}] in ${finding.file}:${finding.line}: ${finding.detail}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
572
604
|
async function validateReleaseBundleAudit() {
|
|
573
605
|
console.log('\nChecking release benchmark bundle (audit:release-bundle)...');
|
|
574
606
|
const report = runReleaseBundleAudit();
|
|
@@ -666,6 +698,7 @@ async function main() {
|
|
|
666
698
|
await validateCacheLayerContractAudit();
|
|
667
699
|
await validateReflectionCitationAudit();
|
|
668
700
|
await validateCachingScopeHygieneAudit();
|
|
701
|
+
await validateTypographyPaletteAntiRepeatAudit();
|
|
669
702
|
await validateReleaseBundleAudit();
|
|
670
703
|
await validateFileSizeAudit();
|
|
671
704
|
await validateRuleIdUniquenessAudit();
|