mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-13 08:29:04 +02:00
fix(design-md): mark and updateDesignMd never rewrite the user's file; refuse a contradictory mark
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>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
60758c5ddf
commit
b4d88a0126
+19
-25
@@ -15,7 +15,11 @@ import fs from "fs";
|
||||
import path from "path";
|
||||
import { requireApiKey } from "./auth";
|
||||
import { receiptedFetch } from "./receipted-fetch";
|
||||
import { parseDesignMd, detectFormat, renderDesignMd, upsertSection, specSkeleton, tokensFlat } from "../../lib/design-md";
|
||||
import { parseDesignMd, detectFormat, renderDesignMd, spliceSection, specSkeleton, tokensFlat, slug } from "../../lib/design-md";
|
||||
import { atomicWriteSync } from "../../lib/fs-atomic";
|
||||
|
||||
/** The section the extraction owns in DESIGN.md (replaced on every run). */
|
||||
export const EXTRACTED_SECTION_HEADING = "Extracted Design Language";
|
||||
|
||||
export interface ExtractedDesign {
|
||||
colors: { name: string; hex: string; usage: string }[];
|
||||
@@ -102,12 +106,12 @@ function defaultDesign(): ExtractedDesign {
|
||||
/**
|
||||
* Write or update DESIGN.md with extracted design patterns.
|
||||
*
|
||||
* Existing file (spec, legacy, or anything with `##` sections): the
|
||||
* "## Extracted Design Language" section is upserted through lib/design-md.ts,
|
||||
* which splices the body only — front matter bytes are never re-emitted, and a
|
||||
* spec file keeps its canonical section order (the extracted section is an extra
|
||||
* after them). New file: a spec-format skeleton whose tokens come from the
|
||||
* extraction (colors by name, typography by role) plus the extracted section.
|
||||
* Existing file (spec, legacy, or anything at all): the "## Extracted Design
|
||||
* Language" section is spliced in at the text level through lib/design-md.ts,
|
||||
* so every other byte of the user's file (front matter, section order, prose)
|
||||
* is untouched; the section is replaced in place on a rerun, appended otherwise.
|
||||
* New file: a spec-format skeleton whose tokens come from the extraction
|
||||
* (colors by name, typography by role) plus the extracted section.
|
||||
*/
|
||||
export function updateDesignMd(
|
||||
repoRoot: string,
|
||||
@@ -116,32 +120,23 @@ export function updateDesignMd(
|
||||
): void {
|
||||
const designPath = path.join(repoRoot, "DESIGN.md");
|
||||
const timestamp = new Date().toISOString().split("T")[0];
|
||||
const section = formatExtractedSection(extracted, sourceMockup, timestamp);
|
||||
const heading = "Extracted Design Language";
|
||||
const body = section.split("\n").slice(1).join("\n"); // drop the "## Extracted Design Language" line
|
||||
|
||||
const write = (content: string) => {
|
||||
const tmp = `${designPath}.tmp-${process.pid}`;
|
||||
fs.writeFileSync(tmp, content);
|
||||
fs.renameSync(tmp, designPath);
|
||||
};
|
||||
const body = formatExtractedSection(extracted, sourceMockup, timestamp);
|
||||
|
||||
if (fs.existsSync(designPath)) {
|
||||
const doc = parseDesignMd(fs.readFileSync(designPath, "utf-8"));
|
||||
write(renderDesignMd(upsertSection(doc, heading, body)));
|
||||
atomicWriteSync(designPath, spliceSection(fs.readFileSync(designPath, "utf-8"), EXTRACTED_SECTION_HEADING, body));
|
||||
console.error(`Updated DESIGN.md with extracted design language`);
|
||||
return;
|
||||
}
|
||||
|
||||
const colors: Record<string, string> = {};
|
||||
for (const c of extracted.colors) {
|
||||
const key = c.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
if (key && /^#[0-9a-fA-F]{3,8}$/.test(c.hex) && !(key in colors)) colors[key] = c.hex;
|
||||
const key = slug(c.name);
|
||||
if (key !== "token" && /^#[0-9a-fA-F]{3,8}$/.test(c.hex) && !(key in colors)) colors[key] = c.hex;
|
||||
}
|
||||
const typography: Record<string, Record<string, string>> = {};
|
||||
for (const t of extracted.typography) {
|
||||
const role = t.role.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
if (!role || typography[role]) continue;
|
||||
const role = slug(t.role);
|
||||
if (role === "token" || typography[role]) continue;
|
||||
const entry: Record<string, string> = { fontFamily: t.family };
|
||||
if (t.size) entry.fontSize = t.size;
|
||||
if (t.weight) entry.fontWeight = t.weight;
|
||||
@@ -152,9 +147,9 @@ export function updateDesignMd(
|
||||
if (Object.keys(typography).length) frontmatter.typography = typography;
|
||||
const doc = specSkeleton("Design System", frontmatter, [
|
||||
{ heading: "Overview", body: `${extracted.mood}\n\nCreated by the gstack designer from an approved mockup (${path.basename(sourceMockup)}) on ${timestamp}.` },
|
||||
{ heading, body },
|
||||
{ heading: EXTRACTED_SECTION_HEADING, body },
|
||||
]);
|
||||
write(renderDesignMd(doc));
|
||||
atomicWriteSync(designPath, renderDesignMd(doc));
|
||||
console.error(`Created DESIGN.md with extracted design language`);
|
||||
}
|
||||
|
||||
@@ -164,7 +159,6 @@ function formatExtractedSection(
|
||||
date: string,
|
||||
): string {
|
||||
const lines: string[] = [
|
||||
"## Extracted Design Language",
|
||||
`*Auto-extracted from approved mockup on ${date}*`,
|
||||
`*Source: ${path.basename(sourceMockup)}*`,
|
||||
"",
|
||||
|
||||
Reference in New Issue
Block a user