mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-14 00:49:00 +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
+16
-14
@@ -14,8 +14,10 @@
|
||||
* ambiguous file (DESIGN_MD_CONVERT_REFUSED, exit 2) and a non-legacy one (exit 1).
|
||||
* tokens Flat token map as JSON ({"colors.primary": "#F59E0B", ...}); {path} refs resolved;
|
||||
* invalid refs listed under "errors" (DESIGN_MD_TOKEN_REF_INVALID). Exit 0.
|
||||
* mark Persist the user's one-time format choice inside the file: spec files get a YAML
|
||||
* comment on line 2, legacy files an HTML comment on line 1. Body bytes untouched.
|
||||
* mark Persist the user's one-time format choice inside the file: a file that opens with
|
||||
* front matter gets a YAML comment on line 2, any other file an HTML comment on
|
||||
* line 1. A text-level splice: every other byte is untouched. Refuses a choice that
|
||||
* contradicts the file (spec on a non-spec file, legacy-keep on a spec file), exit 2.
|
||||
*
|
||||
* Exit 3 + DESIGN_MD_INTERNAL_ERROR is a gstack bug. YAML errors never propagate: a file whose
|
||||
* front matter does not parse is `unknown` with a reason.
|
||||
@@ -23,25 +25,20 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { SENTINEL } from '../lib/design-detect-contract';
|
||||
import { atomicWriteSync } from '../lib/fs-atomic';
|
||||
import {
|
||||
parseDesignMd, detectFormat, convertLegacy, renderDesignMd, tokensFlat, setMarker,
|
||||
parseDesignMd, detectFormat, convertLegacy, renderDesignMd, tokensFlat, insertMarker,
|
||||
type DesignMdDoc, type FormatChoice,
|
||||
} from '../lib/design-md';
|
||||
|
||||
function resolveFile(arg?: string): string {
|
||||
return path.resolve(arg && !arg.startsWith('--') ? arg : 'DESIGN.md');
|
||||
return path.resolve(arg ?? 'DESIGN.md');
|
||||
}
|
||||
|
||||
function load(file: string): { text: string; doc: DesignMdDoc } | null {
|
||||
try { const text = fs.readFileSync(file, 'utf-8'); return { text, doc: parseDesignMd(text) }; } catch { return null; }
|
||||
}
|
||||
|
||||
function writeAtomic(file: string, content: string) {
|
||||
const tmp = `${file}.tmp-${process.pid}`;
|
||||
fs.writeFileSync(tmp, content);
|
||||
fs.renameSync(tmp, file);
|
||||
}
|
||||
|
||||
export function main(argv = process.argv.slice(2)): number {
|
||||
const verb = argv[0] ?? '';
|
||||
const flags = new Set(argv.filter(a => a.startsWith('--')));
|
||||
@@ -60,8 +57,8 @@ export function main(argv = process.argv.slice(2)): number {
|
||||
case 'convert': {
|
||||
const file = resolveFile(positional[0]);
|
||||
const loaded = load(file);
|
||||
const { format, reason } = detectFormat(loaded?.doc ?? null);
|
||||
if (format === 'unknown' && reason?.startsWith('ambiguous')) {
|
||||
const { format, code, reason } = detectFormat(loaded?.doc ?? null);
|
||||
if (code === 'ambiguous') {
|
||||
process.stderr.write(`${SENTINEL.DESIGN_MD_CONVERT_REFUSED}: ${reason}\n`);
|
||||
return 2;
|
||||
}
|
||||
@@ -72,7 +69,7 @@ export function main(argv = process.argv.slice(2)): number {
|
||||
const out = renderDesignMd(convertLegacy(loaded.doc), { emitFrontmatter: true });
|
||||
if (flags.has('--write')) {
|
||||
fs.writeFileSync(`${file}.legacy.bak`, loaded.text);
|
||||
writeAtomic(file, out);
|
||||
atomicWriteSync(file, out);
|
||||
process.stdout.write(`${SENTINEL.DESIGN_MD_FORMAT}: spec\n${SENTINEL.DESIGN_MD_WRITTEN}: ${file}\n${SENTINEL.DESIGN_MD_BACKUP}: ${file}.legacy.bak\n`);
|
||||
} else {
|
||||
process.stdout.write(out);
|
||||
@@ -96,7 +93,12 @@ export function main(argv = process.argv.slice(2)): number {
|
||||
const file = resolveFile(positional[1]);
|
||||
const loaded = load(file);
|
||||
if (!loaded) { process.stdout.write(`${SENTINEL.DESIGN_MD_FORMAT}: missing\n`); return 1; }
|
||||
writeAtomic(file, renderDesignMd(setMarker(loaded.doc, choice)));
|
||||
const { format } = detectFormat(loaded.doc);
|
||||
if ((choice === 'spec' && format !== 'spec') || (choice === 'legacy-keep' && format === 'spec')) {
|
||||
process.stderr.write(`${SENTINEL.DESIGN_MD_CONVERT_REFUSED}: mark ${choice} contradicts the file's format (${format}); file unchanged\n`);
|
||||
return 2;
|
||||
}
|
||||
atomicWriteSync(file, insertMarker(loaded.text, choice));
|
||||
process.stdout.write(`${SENTINEL.DESIGN_MD_MARKER}: ${choice}\n`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user