- 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>
- `mark`, `convert --write`, and the design binary's extraction replaced a
symlinked DESIGN.md (a docs-site layout) with a regular file and left the
real target untouched; both writers resolve the link first.
- A single stray CRLF flipped a whole LF file to CRLF: the editors now keep
the majority ending. A UTF-8 BOM broke format detection and ended up
mid-file after `mark`; it is recognized and kept at byte 0.
- Re-running `mark` on a marked file deleted the blank line after the marker
(`\s*$` matched across the newline); the marker regexes use `[ \t]*`.
- Fences: readers follow markdown (an unclosed fence runs to EOF); the
text-level editors refuse such a file with DesignMdEditRefused
(DESIGN_MD_EDIT_REFUSED) instead of splicing the wrong section, and the
design binary reports that and leaves the file alone.
- needsQuotes also quotes a scalar containing ` #` (an inline-comment
shape parsed back as a truncated value).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- insertMarker and spliceSection normalized every line ending to LF, so a CRLF
DESIGN.md came back rewritten beyond the one line they promised to touch.
Both detect the file's dominant line ending and restore it.
- parseDesignMd and spliceSection each walked headings with their own fence
tracking; they now share headingLines (and upsertSection shares
headingMatches). An unclosed ``` is treated as prose for that file: it used
to swallow every later section on a splice.
- A token value carrying a control character (an LLM-extracted font family
with an embedded newline) was emitted as a bare multi-line scalar that
Bun.YAML rejects, turning a freshly written DESIGN.md into
frontmatter-unparsable; needsQuotes routes it through the quoted form.
- The marker-line regex variants are built once beside YAML_MARKER_RE; the
dead setMarker export and a no-op ternary are gone; LEGACY_HEADINGS derives
from the identity list; the header diagram names the text-level editors as
the write path for user-owned files; the bin validates and prints the mark
choices from FORMAT_CHOICES.
Tests: CRLF round-trips for both editors, a fenced ## inside a section and an
unclosed fence, and a newline-bearing scalar parsing back.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
renderDesignMd re-sorted canonical section names into spec order on every
render, so `gstack-design-md mark legacy-keep` (the "leave it alone" answer)
and the design binary's mockup extraction reordered a legacy DESIGN.md
(Typography and Layout jumped to the top) and normalized its whitespace, while
the bin promised "body bytes untouched". `mark` now splices only the marker
line (insertMarker) and `updateDesignMd` splices only its own section
(spliceSection); every other byte of an existing file is preserved, and spec
order applies only to files that open with front matter. `mark` refuses a
choice that contradicts the file's format (spec on a non-spec file,
legacy-keep on a spec file) with DESIGN_MD_CONVERT_REFUSED, exit 2, file
unchanged. convertLegacy keeps intro prose under the title instead of
rebuilding the preamble from the title alone. detectFormat returns a
machine-readable `code` beside the prose reason (the bin no longer branches on
reason text); the marker regexes derive from FORMAT_MARKER_PREFIX and
FORMAT_CHOICES; the hop limit and legacy identity headings are named
constants; slug is exported and reused; both writers use lib/fs-atomic.ts.
Tests pin byte identity for mark and updateDesignMd on the legacy fixture,
the refusal paths, and the preserved preamble.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lib/design-md.ts implements the open DESIGN.md format (google-labs-code/
design.md, Apache-2.0): YAML front matter with the five token groups (colors,
typography, rounded, spacing, components) and eight canonical `##` sections
in spec order (Overview, Colors, Typography, Layout, Elevation & Depth,
Shapes, Components, Do's and Don'ts), aliases mapped, extras preserved after
them in their original order. parseDesignMd never throws (unparsable front
matter → `unknown` with a reason); renderDesignMd re-emits the preserved front
matter bytes and only `convert` writes fresh YAML through a small block-style
emitter (Bun.YAML.stringify is flow style); upsertSection splices the body
only; tokensFlat resolves `{path}` references to primitives and reports group,
self, dangling, and cyclic refs as DESIGN_MD_TOKEN_REF_INVALID. convertLegacy
turns gstack's pre-spec DESIGN.md into the open format: Product Context and
Aesthetic Direction fold into Overview, Typography roles become
display/body/label/mono tokens (mono carries fontFeature: tnum), Color hexes
become colors (mode-qualified labels keep their qualifier; strategy lines are
not colors), the Spacing scale and Layout radii become spacing and rounded,
Motion / Grain Texture / Decisions Log survive as extras. The format marker
lives inside the file: a YAML comment on line 2 of a spec file, an HTML
comment on line 1 of a legacy file.
bin/gstack-design-md.ts: `check` (DESIGN_MD_FORMAT + marker), `convert
[--write]` (backup to DESIGN.md.legacy.bak, temp+rename, refuses ambiguous
input with DESIGN_MD_CONVERT_REFUSED), `tokens` (flat JSON), `mark
<spec|legacy-keep>`. Exit 3 + DESIGN_MD_INTERNAL_ERROR is a gstack bug.
design/src/memory.ts: updateDesignMd upserts "Extracted Design Language"
through the lib (front matter bytes untouched, canonical order kept, section
replaced on rerun) and creates a spec skeleton with tokens from the extraction
when no file exists; readDesignConstraints leads with the flat tokens and the
Overview for spec files. The design binary still bundles.
test/design-md.test.ts pins all of it against gstack's own DESIGN.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>