* test(#4134): fail-first regression — refuse punctuation-fragment milestone names A first-milestone ROADMAP.md H1 that puts the version after the name (# Roadmap: Project — Name (v1.13)) leaves exactly ')' after the heading's own version token, which the ADR-3180 §7.2 pinned name rule returns as a COMPLETE-scope milestone name. Failing-first coverage: - getMilestoneInfo: name-then-version H1 (STATE-anchored + ROADMAP-only fallback) must yield TRUNCATED {version, name: null}, never ')' - the refusal is level-agnostic (H2/H3) - punctuation-family remainders (')', '()', '**', '.,;:', ']}', emoji-only) - listMilestoneHeadings enumerates the heading with name: null - init manager CLI reports milestone_name: null and no lone ')' anywhere - property (seed 20260905, 300 runs): a word-char remainder is always a name, a punctuation-only remainder never is - negative space: canonical delimiter forms, parenthetical names (#3171), trailing markers, digit-only names, CRLF headings, version-last-no-parens control * fix(#4134): refuse punctuation-only milestone heading names extractMilestoneHeadingName returns everything after the heading's own version token as the name (ADR-3180 §7.2 pinned rule), which assumes version-then-name. A name-then-version heading — the H1 a first-ever ROADMAP.md drifts into ('# Roadmap: Project — Name (v1.13)') — leaves exactly ')' after the token, and that fragment was returned as a COMPLETE-scope milestone name, propagating into init.* JSON output and buildStateFrontmatter's STATE.md writes. A remainder with no letter or digit anywhere (any script) is heading structure, not a curated name: refuse it as name: null so callers report the honest §7.2 rule-6 answer (version kept, TRUNCATED scope). Names that merely contain punctuation are unaffected — '(' stays an ordinary name character (#3171) — and digit-only names qualify. Also closes the template gap that lets the shape occur: the roadmapper agent's output_formats now templates the version-free canonical H1 ('# Roadmap: [Project Name]', per templates/roadmap.md) instead of leaving a first milestone's title line to invention. The new section shifts the file's existing bare-gsd-tools prose mention from line 647 to 660, so its line-keyed PROSE_ALLOWLIST entry moves with it. Emitted-Drift-Ack-Growth: gsd-roadmapper.md — deliberate +498 bytes: new '### 0. Top-Level Title (H1)' output_formats section templating the canonical version-free H1, closing the first-milestone template gap that lets an H1 drift into 'Name (vX.Y)' and corrupt milestone_name extraction (#4134) * chore(#4134): add changeset * chore(#4134): backfill PR number in changeset --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -495,6 +495,55 @@ describe('init manager', () => {
|
||||
assert.strictEqual(output.phases[0].is_active, false);
|
||||
});
|
||||
|
||||
// #4134 — a first-ever ROADMAP.md whose H1 puts the version after the name
|
||||
// (`# Roadmap: <Project> — <Milestone Name> (v1.13)` — the shape nothing
|
||||
// templates for a project's first milestone) used to make the §7.2 pinned
|
||||
// name rule return the literal `)` left after the version token as the
|
||||
// milestone's "name", and init manager displayed it verbatim. The refusal
|
||||
// reports the honest ADR-3180 §7.2 rule-6 answer instead: the version is
|
||||
// real, the name is not resolvable from that heading, milestone_name is
|
||||
// null — never a punctuation fragment.
|
||||
test('#4134 — milestone_name is null, never ")", for a name-then-version H1', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
[
|
||||
'# Roadmap: GSD Core — Native OMP Runtime Support (v1.13)',
|
||||
'',
|
||||
'## Phases',
|
||||
'',
|
||||
'- [ ] **Phase 1: Runtime Adapter Interface**',
|
||||
'',
|
||||
'## Phase Details',
|
||||
'',
|
||||
'### Phase 1: Runtime Adapter Interface',
|
||||
'',
|
||||
'**Goal:** Define the adapter contract.',
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
[
|
||||
'---',
|
||||
'gsd_state_version: 1.0',
|
||||
'milestone: v1.13',
|
||||
'milestone_name: Native OMP Runtime Support',
|
||||
'status: planning',
|
||||
'---',
|
||||
'',
|
||||
'# Project State',
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
const result = runGsdTools('init manager', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.milestone_version, 'v1.13');
|
||||
assert.strictEqual(output.milestone_name, null);
|
||||
assert.ok(!result.output.includes('")"'), 'a lone ")" value must not appear anywhere in the JSON');
|
||||
});
|
||||
|
||||
// #3314 — ADR-456 subprocess clock pin: cmdInitManager's `is_active` gate
|
||||
// (nowMs - newestMtime < 300000) is CLI-subprocess tested, so only the
|
||||
// GSD_TEST_MODE+GSD_NOW_MS pin can control "now" here (t.mock.timers in the
|
||||
|
||||
Reference in New Issue
Block a user