fix(design-md): edits follow a symlinked DESIGN.md, keep the BOM and the majority line ending, refuse an unclosed fence

- `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>
This commit is contained in:
Garry Tan
2026-09-08 18:33:22 +00:00
co-authored by Claude Fable 5.1
parent 8d709c8f29
commit cb9ba7a3e2
4 changed files with 103 additions and 34 deletions
+13 -4
View File
@@ -15,7 +15,7 @@ import fs from "fs";
import path from "path";
import { requireApiKey } from "./auth";
import { receiptedFetch } from "./receipted-fetch";
import { parseDesignMd, detectFormat, renderDesignMd, spliceSection, specSkeleton, tokensFlat, slug } from "../../lib/design-md";
import { parseDesignMd, detectFormat, renderDesignMd, spliceSection, specSkeleton, tokensFlat, slug, DesignMdEditRefused } from "../../lib/design-md";
import { atomicWriteSync } from "../../lib/fs-atomic";
/** The section the extraction owns in DESIGN.md (replaced on every run). */
@@ -118,15 +118,24 @@ export function updateDesignMd(
extracted: ExtractedDesign,
sourceMockup: string,
): void {
const designPath = path.join(repoRoot, "DESIGN.md");
const linkPath = path.join(repoRoot, "DESIGN.md");
const timestamp = new Date().toISOString().split("T")[0];
const body = formatExtractedSection(extracted, sourceMockup, timestamp);
if (fs.existsSync(designPath)) {
atomicWriteSync(designPath, spliceSection(fs.readFileSync(designPath, "utf-8"), EXTRACTED_SECTION_HEADING, body));
if (fs.existsSync(linkPath)) {
const designPath = fs.realpathSync(linkPath); // edit the file behind a symlink, never replace the link
let next: string;
try {
next = spliceSection(fs.readFileSync(designPath, "utf-8"), EXTRACTED_SECTION_HEADING, body);
} catch (err) {
if (err instanceof DesignMdEditRefused) { console.error(`DESIGN.md not updated: ${err.message}`); return; }
throw err;
}
atomicWriteSync(designPath, next);
console.error(`Updated DESIGN.md with extracted design language`);
return;
}
const designPath = linkPath;
const colors: Record<string, string> = {};
for (const c of extracted.colors) {