mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-13 00:19:03 +02:00
fix(design-md): markdown edge cases: rule-opened legacy files, spaced fences, ~~~ blocks, duplicate headings, YAML 1.2 numerics
- insertMarker keyed on "starts with ---", so a legacy file opening with a horizontal rule got a `# gstack:` line rendered as a heading that the parser then never read back (the conversion question re-asked every run). It keys on parsed front matter. - A closing front-matter fence with trailing spaces (`--- `) made a valid spec file `unknown`; the closer is any whole `---` line. - `~~~` fences hid nothing, so a `## ` inside one was a section boundary and a splice corrupted the fence; both fence kinds are tracked and only the same kind closes an opener. - convertLegacy silently kept the first of two `## Layout` bodies (and one of `## Color` / `## Colors`); it refuses with DESIGN_MD_CONVERT_REFUSED and the bin leaves the file and writes no backup. - needsQuotes covers 0x / 0o / .inf / .nan (YAML 1.2 numerics that changed type on round-trip); emitYamlBlock throws on an object inside an array instead of writing "[object Object]". - The design binary coerces the model's extraction JSON at the parse boundary (null names, missing arrays) so the paid call's result survives. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
3994ddfdd7
commit
69b0856d42
@@ -618,3 +618,62 @@ describe('bin/gstack-design-md.ts follows a symlinked DESIGN.md', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('adversarial round: markdown edge cases the editors must survive', () => {
|
||||
test('a legacy file that opens with a horizontal rule gets the HTML-comment marker, and the marker is read back', () => {
|
||||
const legacy = '---\n\n# Design\n\n## Product Context\n\np\n\n## Aesthetic Direction\n\na\n';
|
||||
expect(detectFormat(parseDesignMd(legacy)).format).toBe('legacy');
|
||||
const marked = insertMarker(legacy, 'legacy-keep');
|
||||
expect(marked).toBe('<!-- gstack: design-md-format=legacy-keep -->\n' + legacy);
|
||||
expect(parseDesignMd(marked).marker).toBe('legacy-keep');
|
||||
});
|
||||
|
||||
test('a closing front-matter fence with trailing spaces still closes; a `---x` value line does not', () => {
|
||||
const doc = parseDesignMd('---\nname: x\ncolors:\n a: "#fff"\n--- \n\n## Overview\n\no\n');
|
||||
expect(doc.frontmatterText).toBe('name: x\ncolors:\n a: "#fff"\n');
|
||||
expect(detectFormat(doc).format).toBe('spec');
|
||||
expect(doc.sections.map(s => s.heading)).toEqual(['Overview']);
|
||||
const odd = parseDesignMd('---\nname: x\ndescription: ---x\ncolors:\n a: "#fff"\n---\n\n## Overview\n\no\n');
|
||||
expect(odd.frontmatter).toEqual({ name: 'x', description: '---x', colors: { a: '#fff' } });
|
||||
});
|
||||
|
||||
test('~~~ fences hide headings like ``` fences, and only the same kind closes an opener', () => {
|
||||
const src = '## Overview\n\n~~~\n## Fake\n```\nstill inside\n~~~\n\n## Colors\n\nc\n';
|
||||
expect(parseDesignMd(src).sections.map(s => s.heading)).toEqual(['Overview', 'Colors']);
|
||||
expect(spliceSection(src, 'Overview', 'NEW')).toBe('## Overview\n\nNEW\n\n## Colors\n\nc\n');
|
||||
});
|
||||
|
||||
test('YAML 1.2 numeric shapes and nested array items are handled by the emitter', () => {
|
||||
const yaml = emitYamlBlock({ typography: { body: { fontSize: '0x1F', fontWeight: '.inf', lineHeight: '0o17' } } });
|
||||
expect(Bun.YAML.parse(yaml)).toEqual({ typography: { body: { fontSize: '0x1F', fontWeight: '.inf', lineHeight: '0o17' } } });
|
||||
expect(() => emitYamlBlock({ components: [{ a: 1 }] } as never)).toThrow(/array items must be scalars/);
|
||||
});
|
||||
|
||||
test('convert refuses a legacy file whose consumed heading repeats, instead of dropping a body', () => {
|
||||
const dup = '# T\n\n## Product Context\n\np\n\n## Aesthetic Direction\n\na\n\n## Layout\n\nl1\n\n## Layout\n\nl2\n';
|
||||
expect(() => convertLegacy(parseDesignMd(dup))).toThrow(/appears more than once/);
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-design-md-dup-'));
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, 'DESIGN.md'), dup);
|
||||
const r = runBin(['convert', 'DESIGN.md', '--write'], dir);
|
||||
expect(r.code).toBe(2);
|
||||
expect(r.err).toContain('DESIGN_MD_CONVERT_REFUSED: legacy heading "## Layout" appears more than once');
|
||||
expect(fs.readFileSync(path.join(dir, 'DESIGN.md'), 'utf-8')).toBe(dup);
|
||||
expect(fs.existsSync(path.join(dir, 'DESIGN.md.legacy.bak'))).toBe(false);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('the design binary tolerates unvalidated extraction output (null names, missing arrays)', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-design-md-null-'));
|
||||
try {
|
||||
updateDesignMd(dir, { colors: [{ name: null, hex: '#111111', usage: null }], typography: [{ role: null, family: 'X', size: null, weight: null }], spacing: [], layout: [], mood: '' } as never, 'm.png');
|
||||
const out = fs.readFileSync(path.join(dir, 'DESIGN.md'), 'utf-8');
|
||||
expect(out.startsWith('---\n')).toBe(true);
|
||||
expect(Bun.YAML.parse(parseDesignMd(out).frontmatterText!)).toBeTruthy();
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user