Skip to content

Commit 4abd6e5

Browse files
fix(prd): stop body lines being read as a PRD's title and status (#42)
listPrds scanned the first sixteen lines of each PRD and kept the last `title:`/`status:` match it found, without stopping at the closing `---`. A PRD whose body starts a line with one of those keys — a YAML sample in a fenced code block, say — therefore had its real front matter overwritten, so `prd list` and the README index showed the sample's values instead. Parse the delimited front-matter block instead. Files with no front matter now fall back to the slug and `?` rather than lifting values out of prose. Existing PRDs render byte-identically. Co-authored-by: clawedassistant26 <307253840+clawedassistant26@users.noreply.github.com>
1 parent b85fc94 commit 4abd6e5

2 files changed

Lines changed: 73 additions & 1 deletion

File tree

src/prd.mjs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,19 @@ function unquoteYaml(value) {
199199
return raw;
200200
}
201201

202+
// The front matter is the block between the leading `---` and its closing
203+
// `---`; everything after that is prose. Reading a fixed window of leading
204+
// lines instead let a body line that merely starts with `title:`/`status:` —
205+
// a YAML sample in a fenced code block, say — win, because the scan kept the
206+
// last match it saw anywhere in the window. It also cut off longer front
207+
// matter. Return the block itself, or nothing when the file has none.
208+
function frontMatter(text) {
209+
const lines = text.split(/\r?\n/);
210+
if (lines[0]?.trim() !== "---") return [];
211+
const end = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
212+
return end === -1 ? [] : lines.slice(1, end);
213+
}
214+
202215
/** List numbered PRDs (NNNN-slug.md, excluding the 0000 template). */
203216
export function listPrds(root = process.cwd()) {
204217
const base = prdDir(root);
@@ -211,7 +224,7 @@ export function listPrds(root = process.cwd()) {
211224
const file = path.join(base, name);
212225
let title = m[2], status = "?";
213226
try {
214-
const head = fs.readFileSync(file, "utf8").split(/\r?\n/).slice(0, 16);
227+
const head = frontMatter(fs.readFileSync(file, "utf8"));
215228
for (const l of head) {
216229
const t = l.match(/^title:\s*(.+)$/); if (t) title = unquoteYaml(t[1]);
217230
const s = l.match(/^status:\s*(.+)$/); if (s) status = unquoteYaml(s[1]);

test/prd.test.mjs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,62 @@ test("createPrd hides the whole idea even when it contains a comment terminator"
108108
fs.rmSync(root, { recursive: true, force: true });
109109
}
110110
});
111+
112+
test("listPrds ignores body lines that look like front matter", () => {
113+
// A PRD documenting an API often shows a YAML sample in a fenced block. Those
114+
// lines start at column 0 like real keys do, so a scan that runs past the
115+
// closing `---` reads them as the PRD's own title and status.
116+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "moshcode-prd-"));
117+
try {
118+
const dir = path.join(root, "prd");
119+
fs.mkdirSync(dir, { recursive: true });
120+
fs.writeFileSync(path.join(dir, "0001-job-api.md"), [
121+
"---",
122+
'title: "Job API v2"',
123+
"status: Accepted",
124+
"---",
125+
"",
126+
"## Requirements",
127+
"",
128+
"- R1 [P0] Return the job record:",
129+
"",
130+
"```yaml",
131+
"title: nightly-import",
132+
"status: queued",
133+
"```",
134+
"",
135+
].join("\n"));
136+
137+
const [prd] = listPrds(root);
138+
assert.equal(prd.title, "Job API v2");
139+
assert.equal(prd.status, "Accepted");
140+
} finally {
141+
fs.rmSync(root, { recursive: true, force: true });
142+
}
143+
});
144+
145+
test("listPrds does not take a title or status from a PRD with no front matter", () => {
146+
// A doc dropped into prd/ without front matter has no declared status. Prose
147+
// is not front matter, so fall back to the slug and the unknown marker rather
148+
// than lifting whatever a line happens to start with.
149+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "moshcode-prd-"));
150+
try {
151+
const dir = path.join(root, "prd");
152+
fs.mkdirSync(dir, { recursive: true });
153+
fs.writeFileSync(path.join(dir, "0002-import-notes.md"), [
154+
"# Import notes",
155+
"",
156+
"Fields the importer needs from each upstream row:",
157+
"",
158+
"title: taken from the H1",
159+
"status: derived, never authored by hand",
160+
"",
161+
].join("\n"));
162+
163+
const [prd] = listPrds(root);
164+
assert.equal(prd.title, "import-notes");
165+
assert.equal(prd.status, "?");
166+
} finally {
167+
fs.rmSync(root, { recursive: true, force: true });
168+
}
169+
});

0 commit comments

Comments
 (0)