fix(design-md): text-level edits keep CRLF, one section-boundary rule, control characters quoted

- 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>
This commit is contained in:
Garry Tan
2026-09-08 18:05:21 +00:00
co-authored by Claude Fable 5.1
parent b2e67d0097
commit ae5a5298e0
3 changed files with 107 additions and 58 deletions
+4 -5
View File
@@ -28,8 +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,
} from '../lib/design-md';
type DesignMdDoc, type FormatChoice, FORMAT_CHOICES } from '../lib/design-md';
function resolveFile(arg?: string): string {
return path.resolve(arg ?? 'DESIGN.md');
@@ -86,8 +85,8 @@ export function main(argv = process.argv.slice(2)): number {
}
case 'mark': {
const choice = positional[0] as FormatChoice | undefined;
if (choice !== 'spec' && choice !== 'legacy-keep') {
process.stderr.write('usage: gstack-design-md.ts mark <spec|legacy-keep> [DESIGN.md]\n');
if (!(FORMAT_CHOICES as readonly string[]).includes(choice)) {
process.stderr.write(`usage: gstack-design-md.ts mark <${FORMAT_CHOICES.join('|')}> [DESIGN.md]\n`);
return 2;
}
const file = resolveFile(positional[1]);
@@ -103,7 +102,7 @@ export function main(argv = process.argv.slice(2)): number {
return 0;
}
default:
process.stderr.write('usage: gstack-design-md.ts check [file] | convert [file] [--write] | tokens [file] | mark <spec|legacy-keep> [file]\n');
process.stderr.write(`usage: gstack-design-md.ts check [file] | convert [file] [--write] | tokens [file] | mark <${FORMAT_CHOICES.join('|')}> [file]\n`);
return 2;
}
}