Skip to content

Commit d8206d1

Browse files
fix(prd): stop PRD titles corrupting the README index (#31)
regenerateIndex() passed the new index block to String.replace() as a replacement string, so a PRD title containing a special pattern ($&, $`, $', $$) was interpreted instead of inserted literally — splicing the old index back into itself and corrupting prd/README.md. Use a function replacement (() => block) so the block is written verbatim regardless of title content. Adds a regression test. Co-authored-by: clawedassistant26 <307253840+clawedassistant26@users.noreply.github.com>
1 parent 42e6322 commit d8206d1

2 files changed

Lines changed: 32 additions & 2 deletions

File tree

src/prd.mjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,13 @@ export function regenerateIndex(root = process.cwd()) {
217217
? ["| # | Title | Status |", "|---|---|---|",
218218
...prds.map((p) => `| [${p.id}](${p.file}) | ${p.title} | ${p.status} |`)].join("\n")
219219
: "_No PRDs yet._";
220+
// Use a function replacement so `$`-sequences in a PRD title (e.g. `$&`, `$1`,
221+
// `$\`` ) are inserted verbatim instead of being read as String.replace special
222+
// patterns, which would otherwise splice the match back in and corrupt the index.
223+
const block = `${INDEX_START}\n${rows}\n${INDEX_END}`;
220224
const next = body.replace(
221225
new RegExp(`${INDEX_START}[\\s\\S]*${INDEX_END}`),
222-
`${INDEX_START}\n${rows}\n${INDEX_END}`,
226+
() => block,
223227
);
224228
if (next === body) return false;
225229
fs.writeFileSync(readme, next);

test/prd.test.mjs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import assert from "node:assert/strict";
22
import test from "node:test";
3+
import fs from "node:fs";
4+
import os from "node:os";
5+
import path from "node:path";
36

4-
import { renderPrd } from "../src/prd.mjs";
7+
import { createPrd, renderPrd } from "../src/prd.mjs";
58

69
test("renderPrd quotes titles so YAML metacharacters stay valid", () => {
710
const body = renderPrd({
@@ -13,3 +16,26 @@ test("renderPrd quotes titles so YAML metacharacters stay valid", () => {
1316

1417
assert.match(body, /^title: "Ship CLI: handle \\"quoted\\" flags"$/m);
1518
});
19+
20+
21+
test("regenerateIndex keeps the README intact when a title holds a String.replace pattern", () => {
22+
// `$&`, `$\``, `$'`, `$$` are special in a String.replace *replacement string*.
23+
// A PRD title carrying one must not splice the old index block back into itself.
24+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "moshcode-prd-"));
25+
try {
26+
createPrd("Add $& live support", root);
27+
createPrd("Improve docs", root);
28+
29+
const readme = fs.readFileSync(path.join(root, "prd", "README.md"), "utf8");
30+
const starts = (readme.match(/PRD-INDEX:START/g) || []).length;
31+
const ends = (readme.match(/PRD-INDEX:END/g) || []).length;
32+
33+
assert.equal(starts, 1, "index start marker must appear exactly once");
34+
assert.equal(ends, 1, "index end marker must appear exactly once");
35+
// The literal title text survives; it is not expanded into the match.
36+
assert.match(readme, /\$& live support/);
37+
assert.match(readme, /Improve docs/);
38+
} finally {
39+
fs.rmSync(root, { recursive: true, force: true });
40+
}
41+
});

0 commit comments

Comments
 (0)