@ryuenn3123/agentic-senior-core 4.0.2 → 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.
@@ -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 {
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.2",
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
+ }
@@ -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();