🐛 Fix DESIGN.md Layout and Shapes parsing (#481)

* 🐛 Fix DESIGN.md Layout and Shapes parsing

Prepared with AI assistance.

* ♻️ Refine canonical design parser coverage

Prepared with AI assistance.
This commit is contained in:
CypherPoet
2026-08-03 14:59:00 -07:00
committed by GitHub
parent 1a3f588c71
commit 85f84bf620
5 changed files with 138 additions and 11 deletions
+32 -9
View File
@@ -2,15 +2,20 @@
// the live-mode design-system panel can render. Deterministic, dependency-free.
//
// Two-layer: YAML frontmatter (machine-readable tokens) + markdown body
// (prose with six canonical H2 sections). When frontmatter is present, it's
// (prose with eight canonical H2 sections). When frontmatter is present, it's
// exposed on `model.frontmatter` alongside the prose-scraped sections;
// consumers can prefer frontmatter values and fall back to prose.
// Array order is also match precedence: matchCanonicalSection's keyword-contained
// pass returns the first entry a heading contains, so reordering this changes
// which section an ambiguous heading resolves to.
const CANONICAL_SECTIONS = [
'Overview',
'Colors',
'Typography',
'Layout',
'Elevation',
'Shapes',
'Components',
"Do's and Don'ts",
];
@@ -662,11 +667,19 @@ function parseTypeBullet(bullet) {
};
}
function extractElevation(section) {
function extractGuidance(section) {
if (!section) return null;
const subs = splitSubsections(section.lines);
return {
subtitle: section.subtitle,
description: collectParagraphs(subs[0].lines).join(' ') || null,
rules: extractNamedRules(section.lines),
};
}
const description = collectParagraphs(subs[0].lines).join(' ') || null;
function extractElevation(section) {
const guidance = extractGuidance(section);
if (!guidance) return null;
const shadows = [];
const seen = new Set();
@@ -691,12 +704,7 @@ function extractElevation(section) {
for (const inline of extractInlineShadows(b)) dedupe(inline);
}
return {
subtitle: section.subtitle,
description,
shadows,
rules: extractNamedRules(section.lines),
};
return { ...guidance, shadows };
}
function extractInlineShadows(text) {
@@ -828,6 +836,15 @@ function extractDosDonts(section) {
// ---------- Coverage assessment ----------
// Sections whose model is description-plus-rules only (see extractGuidance).
const guidanceCoverage = (guidance) =>
guidance
? {
description: Boolean(guidance.description),
rules: guidance.rules.length,
}
: 'missing';
function assessCoverage(model) {
const report = {};
@@ -856,6 +873,8 @@ function assessCoverage(model) {
}
: 'missing';
report.layout = guidanceCoverage(model.layout);
report.elevation = model.elevation
? {
shadows: model.elevation.shadows.length,
@@ -864,6 +883,8 @@ function assessCoverage(model) {
}
: 'missing';
report.shapes = guidanceCoverage(model.shapes);
report.components = model.components
? {
count: model.components.components.length,
@@ -893,7 +914,9 @@ export function parseDesignMd(md) {
overview: extractOverview(sections['Overview']),
colors: extractColors(sections['Colors']),
typography: extractTypography(sections['Typography']),
layout: extractGuidance(sections['Layout']),
elevation: extractElevation(sections['Elevation']),
shapes: extractGuidance(sections['Shapes']),
components: extractComponents(sections['Components']),
dosDonts: extractDosDonts(sections["Do's and Don'ts"]),
};
+2
View File
@@ -11975,7 +11975,9 @@ void main() {
rules: [
...(md.colors?.rules || []).map((r) => ({ ...r, section: 'colors' })),
...(md.typography?.rules || []).map((r) => ({ ...r, section: 'typography' })),
...(md.layout?.rules || []).map((r) => ({ ...r, section: 'layout' })),
...(md.elevation?.rules || []).map((r) => ({ ...r, section: 'elevation' })),
...(md.shapes?.rules || []).map((r) => ({ ...r, section: 'shapes' })),
],
dos: md.dosDonts?.dos || [],
donts: md.dosDonts?.donts || [],
+1 -1
View File
@@ -873,7 +873,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
// { present, parsed, sidecar, hasMd, hasSidecar,
// mdNewerThanJson, parseError?, sidecarError? }
// - parsed: output of parseDesignMd (frontmatter
// + six canonical sections) when DESIGN.md exists.
// + the canonical sections) when DESIGN.md exists.
// - sidecar: .impeccable/design.json contents when present.
// Expected shape: schemaVersion 2, carrying
// extensions + components + narrative.
+87 -1
View File
@@ -5,7 +5,7 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { parseDesignMd } from '../skill/scripts/lib/design-parser.mjs';
import { assessCoverage, parseDesignMd } from '../skill/scripts/lib/design-parser.mjs';
describe('parseDesignMd frontmatter branch', () => {
it('returns null frontmatter when the file has no YAML header', () => {
@@ -227,3 +227,89 @@ describe('parseDesignMd overview branch', () => {
assert.deepEqual(overview.philosophy, []);
});
});
describe('parseDesignMd canonical sections', () => {
it('preserves content and named rules from all eight canonical sections', () => {
const md = `# Design System: Complete
## Overview
**Creative North Star: "Structured clarity"**
## Colors
### Primary
- **Ink** (#111111): Primary text.
## Typography
**Body Font:** Inter (with sans-serif)
## Layout: Responsive rhythm
Primary regions use a twelve-column grid that collapses to one column on narrow screens.
### Named Rules
**The Spatial Hierarchy Rule.** Primary content must remain visually dominant.
## Elevation & Depth
Surfaces use tonal layering instead of shadows.
## Shapes
Selected objects use a double outline and clipped corners.
### The "Recognizable Silhouette" Rule
Repeated geometry must remain recognizable without color.
## Components
### Button
- **Primary:** Uses the accent color.
## Do's and Don'ts
### Do
- Preserve the spatial hierarchy.
### Don't
- Flatten every surface.
`;
const model = parseDesignMd(md);
assert.equal(model.layout.subtitle, 'Responsive rhythm');
assert.equal(
model.layout.description,
'Primary regions use a twelve-column grid that collapses to one column on narrow screens.',
);
assert.deepEqual(model.layout.rules, [{
name: 'The Spatial Hierarchy Rule',
body: 'Primary content must remain visually dominant.',
}]);
assert.equal(model.shapes.description, 'Selected objects use a double outline and clipped corners.');
assert.deepEqual(model.shapes.rules, [{
name: 'The Recognizable Silhouette Rule',
body: 'Repeated geometry must remain recognizable without color.',
}]);
const coverage = assessCoverage(model);
assert.deepEqual(
Object.entries(coverage).filter(([, v]) => v === 'missing').map(([k]) => k),
[],
'a section present in the markdown must not be reported as missing',
);
assert.deepEqual(Object.keys(coverage), [
'overview',
'colors',
'typography',
'layout',
'elevation',
'shapes',
'components',
'dosDonts',
]);
assert.deepEqual(coverage.layout, { description: true, rules: 1 });
assert.deepEqual(coverage.shapes, { description: true, rules: 1 });
});
});
+16
View File
@@ -1333,6 +1333,22 @@ describe('live-browser.js regression guards', () => {
);
});
it('includes named rules from every canonical narrative section', () => {
const narrativeStart = SOURCE.indexOf(' function synthesizeNarrative(parsed) {');
const narrativeEnd = SOURCE.indexOf('\n function renderColorTiles', narrativeStart);
assert.notEqual(narrativeStart, -1, 'synthesizeNarrative must exist');
assert.notEqual(narrativeEnd, -1, 'synthesizeNarrative must end before renderColorTiles');
const narrativeSource = SOURCE.slice(narrativeStart, narrativeEnd);
for (const section of ['colors', 'typography', 'layout', 'elevation', 'shapes']) {
assert.match(
narrativeSource,
new RegExp(`md\\.${section}\\?\\.rules[^\\n]*section: '${section}'`),
`the design panel must carry named rules tagged with their ${section} section`,
);
}
});
it('editing focus timeout does not read a stale inline edit row', () => {
assert.doesNotMatch(
SOURCE,