mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
* fix(#2660): capture prose after label in extractOneLinerFromBody The regex `\*\*([^*]+)\*\*` matched the first bold span, so for the new SUMMARY template `**One-liner:** Real prose here.` it captured the label `One-liner:` instead of the prose. MILESTONES.md then wrote bullets like `- One-liner:` with no content. Handle both template forms: - Labeled: `**One-liner:** prose` → prose - Bare: `**prose**` → prose (legacy) Empty prose after a label returns null so no bogus bullets are emitted. Note: existing MILESTONES.md entries generated under the bug are not regenerated here — that is a follow-up. Closes #2660 * fix(#2660): normalize CRLF before one-liner extraction Windows-authored SUMMARY files use CRLF line endings; the LF-only regex in extractOneLinerFromBody would fail to match. Normalize \r\n and \r to \n before stripping frontmatter and matching the one-liner pattern. Adds test case (h) covering CRLF input.
This commit is contained in:
@@ -1756,11 +1756,28 @@ function resolveReasoningEffortInternal(cwd, agentType) {
|
|||||||
*/
|
*/
|
||||||
function extractOneLinerFromBody(content) {
|
function extractOneLinerFromBody(content) {
|
||||||
if (!content) return null;
|
if (!content) return null;
|
||||||
|
// Normalize EOLs so matching works for LF and CRLF files.
|
||||||
|
const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||||
// Strip frontmatter first
|
// Strip frontmatter first
|
||||||
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, '');
|
const body = normalized.replace(/^---\n[\s\S]*?\n---\n*/, '');
|
||||||
// Find the first **...** line after a # heading
|
// Find the first **...** span on a line after a # heading.
|
||||||
const match = body.match(/^#[^\n]*\n+\*\*([^*]+)\*\*/m);
|
// Two supported template forms:
|
||||||
return match ? match[1].trim() : null;
|
// 1) Labeled: **One-liner:** Real prose here. (bug #2660 — new template)
|
||||||
|
// 2) Bare: **Real prose here.** (legacy template)
|
||||||
|
// For (1), the first bold span ends in a colon and the prose that follows
|
||||||
|
// on the same line is the one-liner. For (2), the bold span itself is the
|
||||||
|
// one-liner.
|
||||||
|
const match = body.match(/^#[^\n]*\n+\*\*([^*\n]+)\*\*([^\n]*)/m);
|
||||||
|
if (!match) return null;
|
||||||
|
const boldInner = match[1].trim();
|
||||||
|
const afterBold = match[2];
|
||||||
|
// Labeled form: bold span is a "Label:" prefix — capture prose after it.
|
||||||
|
if (/:\s*$/.test(boldInner)) {
|
||||||
|
const prose = afterBold.trim();
|
||||||
|
return prose.length > 0 ? prose : null;
|
||||||
|
}
|
||||||
|
// Bare form: the bold content itself is the one-liner.
|
||||||
|
return boldInner.length > 0 ? boldInner : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Misc utilities ───────────────────────────────────────────────────────────
|
// ─── Misc utilities ───────────────────────────────────────────────────────────
|
||||||
|
|||||||
82
tests/bug-2660-one-liner-extraction.test.cjs
Normal file
82
tests/bug-2660-one-liner-extraction.test.cjs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Bug #2660: `gsd-tools milestone complete <version>` writes MILESTONES.md
|
||||||
|
* bullets that read "- One-liner:" (the literal label) instead of the prose
|
||||||
|
* after the label.
|
||||||
|
*
|
||||||
|
* Root cause: extractOneLinerFromBody() matches the first **...** span. In
|
||||||
|
* `**One-liner:** prose`, the first span contains only `One-liner:` so the
|
||||||
|
* function returns the label instead of the prose after it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { describe, test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const { extractOneLinerFromBody } = require(
|
||||||
|
path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'core.cjs')
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('bug #2660: extractOneLinerFromBody', () => {
|
||||||
|
test('a) body-style **One-liner:** label returns prose after the label', () => {
|
||||||
|
const content =
|
||||||
|
'# Phase 2 Plan 01: Foundation Summary\n\n**One-liner:** Real prose here.\n';
|
||||||
|
assert.strictEqual(extractOneLinerFromBody(content), 'Real prose here.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('b) frontmatter-only one-liner returns null (caller handles frontmatter)', () => {
|
||||||
|
const content =
|
||||||
|
'---\none-liner: Set up project\n---\n\n# Phase 1: Foundation Summary\n\nBody prose with no bold line.\n';
|
||||||
|
assert.strictEqual(extractOneLinerFromBody(content), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('c) no one-liner at all returns null', () => {
|
||||||
|
const content =
|
||||||
|
'# Phase 1: Foundation Summary\n\nJust some narrative, no bold line.\n';
|
||||||
|
assert.strictEqual(extractOneLinerFromBody(content), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('d) bold spans inside the prose are preserved', () => {
|
||||||
|
const content =
|
||||||
|
'# Phase 1: Foundation Summary\n\n**One-liner:** This is **important** stuff.\n';
|
||||||
|
assert.strictEqual(
|
||||||
|
extractOneLinerFromBody(content),
|
||||||
|
'This is **important** stuff.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('e) empty prose after label returns null (no bogus bullet)', () => {
|
||||||
|
const empty =
|
||||||
|
'# Phase 1: Foundation Summary\n\n**One-liner:**\n\nRest of body.\n';
|
||||||
|
const whitespace =
|
||||||
|
'# Phase 1: Foundation Summary\n\n**One-liner:** \n\nRest of body.\n';
|
||||||
|
assert.strictEqual(extractOneLinerFromBody(empty), null);
|
||||||
|
assert.strictEqual(extractOneLinerFromBody(whitespace), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('f) legacy bare **prose** format still works (no label, no colon)', () => {
|
||||||
|
// Preserve pre-existing behavior: SUMMARY files historically used
|
||||||
|
// `**bold prose**` with no label. See tests/commands.test.cjs:366 and
|
||||||
|
// tests/milestone.test.cjs:451 — both assert this form.
|
||||||
|
const content =
|
||||||
|
'---\nphase: "01"\n---\n\n# Phase 1: Foundation Summary\n\n**JWT auth with refresh rotation using jose library**\n\n## Performance\n';
|
||||||
|
assert.strictEqual(
|
||||||
|
extractOneLinerFromBody(content),
|
||||||
|
'JWT auth with refresh rotation using jose library'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('g) other **Label:** prefixes (e.g. Summary:) also capture prose after label', () => {
|
||||||
|
const content =
|
||||||
|
'# Phase 1: Foundation Summary\n\n**Summary:** Built the thing.\n';
|
||||||
|
assert.strictEqual(extractOneLinerFromBody(content), 'Built the thing.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('h) CRLF line endings (Windows) are handled', () => {
|
||||||
|
const content =
|
||||||
|
'---\r\nphase: "01"\r\n---\r\n\r\n# Phase 1: Foundation Summary\r\n\r\n**One-liner:** Windows-authored prose.\r\n';
|
||||||
|
assert.strictEqual(
|
||||||
|
extractOneLinerFromBody(content),
|
||||||
|
'Windows-authored prose.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user