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:
Garry Tan
2026-09-08 18:55:37 +00:00
co-authored by Claude Fable 5.1
parent 3994ddfdd7
commit 69b0856d42
4 changed files with 110 additions and 19 deletions
+8 -2
View File
@@ -28,7 +28,7 @@ import { SENTINEL } from '../lib/design-detect-contract';
import { atomicWriteSync } from '../lib/fs-atomic';
import {
parseDesignMd, detectFormat, convertLegacy, renderDesignMd, tokensFlat, insertMarker,
type DesignMdDoc, type FormatChoice, FORMAT_CHOICES } from '../lib/design-md';
type DesignMdDoc, type FormatChoice, FORMAT_CHOICES, DesignMdEditRefused } from '../lib/design-md';
/** The file itself, through any symlink (a `DESIGN.md -> docs/DESIGN.md` layout must edit the target, never replace the link). */
function resolveFile(arg?: string): string {
@@ -67,7 +67,13 @@ export function main(argv = process.argv.slice(2)): number {
process.stderr.write(`${SENTINEL.DESIGN_MD_FORMAT}: ${format}${reason ? ` (${reason})` : ''}; convert only accepts a legacy gstack DESIGN.md\n`);
return 1;
}
const out = renderDesignMd(convertLegacy(loaded.doc), { emitFrontmatter: true });
let out: string;
try {
out = renderDesignMd(convertLegacy(loaded.doc), { emitFrontmatter: true });
} catch (err) {
if (err instanceof DesignMdEditRefused) { process.stderr.write(`${SENTINEL.DESIGN_MD_CONVERT_REFUSED}: ${err.message.replace(/^[A-Z_]+: /, '')}\n`); return 2; }
throw err;
}
if (flags.has('--write')) {
fs.writeFileSync(`${file}.legacy.bak`, loaded.text);
atomicWriteSync(file, out);