fix(types): add §3.4 exclusion list to cover-rate script, reach 100% (P1.1.6)
10 builder-private hex literals excluded from denominator per Codex B-route decision. 4 uncovered semantic hex added as new single-value tokens (color-accent-dark, color-info-surface, color-warning-text-strong, color-danger-text-strong) → semantic cover rate 87.5% → 100%. Hard gate passes.
This commit is contained in:
parent
b23c55ea10
commit
7eefa08acf
|
|
@ -20,9 +20,9 @@ import {
|
|||
*/
|
||||
|
||||
describe('getSemanticPalette', () => {
|
||||
it('returns 14 original + 8 alert + 6 chart = 28 color variables', () => {
|
||||
it('returns 14 original + 8 alert + 4 extended + 6 chart = 32 color variables', () => {
|
||||
const p = getSemanticPalette();
|
||||
expect(Object.keys(p.variables).length).toBe(28);
|
||||
expect(Object.keys(p.variables).length).toBe(32);
|
||||
});
|
||||
|
||||
it('defines the Mode theme axis with Light + Dark', () => {
|
||||
|
|
@ -156,7 +156,7 @@ describe('applySemanticPalette', () => {
|
|||
it('seeds an empty document with palette + theme axis', () => {
|
||||
const doc: PenDocument = createEmptyDocument();
|
||||
const out = applySemanticPalette(doc);
|
||||
expect(Object.keys(out.variables ?? {}).length).toBe(28);
|
||||
expect(Object.keys(out.variables ?? {}).length).toBe(32);
|
||||
expect(out.themes?.Mode).toEqual(['Light', 'Dark']);
|
||||
});
|
||||
|
||||
|
|
@ -179,7 +179,7 @@ describe('applySemanticPalette', () => {
|
|||
expect(out.variables!['color-accent'].value).toBe('#FF0000');
|
||||
// All other palette vars still added
|
||||
expect(out.variables!['color-surface']).toBeDefined();
|
||||
expect(Object.keys(out.variables!).length).toBe(28);
|
||||
expect(Object.keys(out.variables!).length).toBe(32);
|
||||
});
|
||||
|
||||
it('user-defined theme axis WINS (does not overwrite existing Mode)', () => {
|
||||
|
|
@ -206,7 +206,7 @@ describe('applySemanticPalette', () => {
|
|||
it('applies cleanly to a doc without variables or themes', () => {
|
||||
const doc: PenDocument = { version: '1.0.0', children: [] };
|
||||
const out = applySemanticPalette(doc);
|
||||
expect(Object.keys(out.variables!).length).toBe(28);
|
||||
expect(Object.keys(out.variables!).length).toBe(32);
|
||||
expect(out.themes!.Mode).toEqual(['Light', 'Dark']);
|
||||
});
|
||||
});
|
||||
|
|
@ -399,19 +399,19 @@ describe('chart color tokens (6 single-value)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('palette count after 1.1.5 additions', () => {
|
||||
it('getSemanticPalette() now has 28 variables (14 original + 8 alert + 6 chart)', () => {
|
||||
describe('palette count after P1.1.5 + P1.1.6 color additions', () => {
|
||||
it('getSemanticPalette() now has 32 color variables', () => {
|
||||
const p = getSemanticPalette();
|
||||
expect(Object.keys(p.variables).length).toBe(28);
|
||||
expect(Object.keys(p.variables).length).toBe(32);
|
||||
});
|
||||
|
||||
it('SEMANTIC_PALETTE_NAMES has 28 entries', () => {
|
||||
expect(SEMANTIC_PALETTE_NAMES.length).toBe(28);
|
||||
it('SEMANTIC_PALETTE_NAMES has 32 entries', () => {
|
||||
expect(SEMANTIC_PALETTE_NAMES.length).toBe(32);
|
||||
});
|
||||
|
||||
it('applySemanticPalette seeds 28 variables on an empty document', () => {
|
||||
it('applySemanticPalette seeds 32 variables on an empty document', () => {
|
||||
const doc: PenDocument = createEmptyDocument();
|
||||
const out = applySemanticPalette(doc);
|
||||
expect(Object.keys(out.variables ?? {}).length).toBe(28);
|
||||
expect(Object.keys(out.variables ?? {}).length).toBe(32);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -189,6 +189,24 @@ const PALETTE: Record<string, PaletteEntry> = {
|
|||
description: 'Danger / error alert text / icon',
|
||||
},
|
||||
|
||||
// ── Extended alert/accent shades (4 semantic-but-uncovered hex) ──────────
|
||||
'color-accent-dark': {
|
||||
single: '#1D4ED8',
|
||||
description: 'Darker accent — featured badge text, active pill on white',
|
||||
},
|
||||
'color-info-surface': {
|
||||
single: '#EFF6FF',
|
||||
description: 'Ultra-light info surface — featured pricing card highlight bg',
|
||||
},
|
||||
'color-warning-text-strong': {
|
||||
single: '#B45309',
|
||||
description: 'Stronger warning text — tag "warning" label on light bg',
|
||||
},
|
||||
'color-danger-text-strong': {
|
||||
single: '#B91C1C',
|
||||
description: 'Stronger danger text — tag "error" label on light bg',
|
||||
},
|
||||
|
||||
// ── Chart palette (6 single-value colors, no theme axis) ─────────────────
|
||||
'color-chart-1': { single: '#3B82F6', description: 'Chart series 1 — blue' },
|
||||
'color-chart-2': { single: '#8B5CF6', description: 'Chart series 2 — purple' },
|
||||
|
|
|
|||
|
|
@ -5,6 +5,33 @@ import { getSemanticPaletteHex } from '../packages/pen-core/src/variables/semant
|
|||
const BUILDERS_DIR = 'packages/pen-core/src/element-builders';
|
||||
const HEX_REGEX = /#[0-9A-Fa-f]{6}\b/g;
|
||||
|
||||
/**
|
||||
* §3.4 Builder-private literals — these hex values are intentionally
|
||||
* hardcoded in v0/v1 builders and are NOT candidates for semantic tokens.
|
||||
* They are excluded from the cover-rate denominator.
|
||||
*
|
||||
* Rationale per design decision (2026-05-03 Codex review, B route):
|
||||
* - Gray/neutral scale (#111827 … #F9FAFB): CSS-reset / skeleton shades
|
||||
* that don't carry semantic meaning and would pollute the palette.
|
||||
* - iOS system green (#34C759), iOS separator (#E5E5EA): platform-specific
|
||||
* literals used only in iOS-flavored builders.
|
||||
* - Pure black (#000000): universal default, not a design token.
|
||||
*/
|
||||
const BUILDER_PRIVATE_HEX = new Set<string>(
|
||||
[
|
||||
'#000000',
|
||||
'#111827',
|
||||
'#4B5563',
|
||||
'#6B7280',
|
||||
'#9CA3AF',
|
||||
'#D1D5DB',
|
||||
'#E5E7EB',
|
||||
'#F9FAFB',
|
||||
'#34C759',
|
||||
'#E5E5EA',
|
||||
].map((h) => h.toUpperCase()),
|
||||
);
|
||||
|
||||
function extractHexFromFile(path: string): string[] {
|
||||
const content = readFileSync(path, 'utf-8');
|
||||
return Array.from(content.matchAll(HEX_REGEX), (m) => m[0].toUpperCase());
|
||||
|
|
@ -20,17 +47,21 @@ function main() {
|
|||
f !== 'resolve-theme.ts',
|
||||
);
|
||||
|
||||
const v0HexSet = new Set<string>();
|
||||
const v0HexSetRaw = new Set<string>();
|
||||
const hexUsageMap = new Map<string, string[]>(); // hex → list of files using it
|
||||
for (const f of v0Files) {
|
||||
const hexes = extractHexFromFile(join(BUILDERS_DIR, f));
|
||||
hexes.forEach((h) => {
|
||||
v0HexSet.add(h);
|
||||
v0HexSetRaw.add(h);
|
||||
if (!hexUsageMap.has(h)) hexUsageMap.set(h, []);
|
||||
hexUsageMap.get(h)!.push(f);
|
||||
});
|
||||
}
|
||||
|
||||
// Apply §3.4 exclusion list — remove builder-private literals from denominator
|
||||
const v0HexSet = new Set([...v0HexSetRaw].filter((h) => !BUILDER_PRIVATE_HEX.has(h)));
|
||||
const excludedCount = v0HexSetRaw.size - v0HexSet.size;
|
||||
|
||||
const lightTokens = getSemanticPaletteHex('Light');
|
||||
const darkTokens = getSemanticPaletteHex('Dark');
|
||||
const tokenHexSet = new Set<string>([
|
||||
|
|
@ -46,20 +77,25 @@ function main() {
|
|||
const uncovered = [...v0HexSet].filter((h) => !tokenHexSet.has(h)).sort();
|
||||
const coverRate = covered.length / v0HexSet.size;
|
||||
|
||||
console.log(`v0 distinct hex literals: ${v0HexSet.size}`);
|
||||
console.log(`v0 distinct hex literals (raw): ${v0HexSetRaw.size}`);
|
||||
console.log(`§3.4 builder-private excluded: ${excludedCount}`);
|
||||
console.log(`v0 semantic hex (post-exclusion): ${v0HexSet.size}`);
|
||||
console.log(`covered by palette: ${covered.length}`);
|
||||
console.log(`uncovered: ${uncovered.length}`);
|
||||
console.log(`cover rate: ${(coverRate * 100).toFixed(1)}%`);
|
||||
console.log(`\nFirst 30 uncovered hex (with usage):`);
|
||||
uncovered.slice(0, 30).forEach((h) => {
|
||||
const users = hexUsageMap.get(h)!;
|
||||
console.log(
|
||||
` ${h} — used in ${users.length} file(s): ${users.slice(0, 3).join(', ')}${users.length > 3 ? ` (+${users.length - 3} more)` : ''}`,
|
||||
);
|
||||
});
|
||||
console.log(`semantic cover rate: ${(coverRate * 100).toFixed(1)}%`);
|
||||
|
||||
if (uncovered.length > 0) {
|
||||
console.log(`\nUncovered semantic hex (with usage):`);
|
||||
uncovered.forEach((h) => {
|
||||
const users = hexUsageMap.get(h)!;
|
||||
console.log(
|
||||
` ${h} — used in ${users.length} file(s): ${users.slice(0, 3).join(', ')}${users.length > 3 ? ` (+${users.length - 3} more)` : ''}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (coverRate < 0.95) {
|
||||
console.error(`\n❌ HARD GATE FAILED: cover rate ${(coverRate * 100).toFixed(1)}% < 95%`);
|
||||
console.error(`\n❌ HARD GATE FAILED: semantic cover rate ${(coverRate * 100).toFixed(1)}% < 95%`);
|
||||
console.error('Per spec §7.4, must pause and revisit D2 namespace boundary before P2.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue