* fix: remove hardcoded phase project-code prefix cap
Centralize project-code prefix stripping/matching and replace fixed {1,6} caps so long codes (for example MANIFOLD-117) resolve across phase, roadmap parser, roadmap upgrade, and validate flows.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address review feedback on prefix parsing
- allow project_code prefixes with digits and underscores while preserving milestone parsing
- use shared optional project-code prefix source in phase dir parsing
- extend regression coverage for APP1/APP_1 prefixed phases
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: resolve review nit in phase-id test comment
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: resolve project-code-prefixed roadmap headings
Make getRoadmapPhaseInternal recover from drifted project-code-prefixed ROADMAP headings while preserving canonical bare-heading preference. Add init.phase-op and parser regressions for #1455 and guard gsd-roadmapper against emitting project_code in headings.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: add changeset for project-code phase fix
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: update roadmapper agent size baseline
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(#1455): tighten project-code prefix regex to [A-Z] start; add boundary test; document source order
Resolves blockers from review:
- Regex changed from [A-Z_][A-Z0-9_]* to [A-Z][A-Z0-9_]* so leading
underscores (_FOO-7, _-7) are never misread as project-code prefixes;
adds boundary test asserting both do NOT strip.
- Adds comment to roadmapPhaseLookupSources explaining why 3 sources are
needed (order-dependent canonical-heading preference).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Solvely-Colin <211764741+Solvely-Colin@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
133 lines
5.4 KiB
JavaScript
133 lines
5.4 KiB
JavaScript
// allow-test-rule: runtime-contract-is-the-product agent .md instruction surface see #1205
|
|
// agents/gsd-roadmapper.md is the deployed agent — the Granularity Calibration table
|
|
// AND the phase_id_convention instructions ARE the deployed behavior. Asserting on
|
|
// their prose asserts what runs in production (#163, #1205).
|
|
'use strict';
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
|
|
|
function readAgent(name) {
|
|
return fs.readFileSync(path.join(AGENTS_DIR, `${name}.md`), 'utf8');
|
|
}
|
|
|
|
// Extract the "## Granularity Calibration" section (up to the next "## " heading)
|
|
// so number-range assertions are scoped and cannot be satisfied by unrelated text
|
|
// elsewhere in the agent file.
|
|
function granularitySection(content) {
|
|
const start = content.indexOf('## Granularity Calibration');
|
|
assert.ok(start !== -1, 'Granularity Calibration section must exist');
|
|
const rest = content.slice(start + '## Granularity Calibration'.length);
|
|
const nextHeading = rest.indexOf('\n## ');
|
|
return nextHeading === -1 ? rest : rest.slice(0, nextHeading);
|
|
}
|
|
|
|
// Extract a named XML-tag block (e.g. <phase_identification>…</phase_identification>)
|
|
function extractBlock(content, tag) {
|
|
const open = `<${tag}>`;
|
|
const close = `</${tag}>`;
|
|
const start = content.indexOf(open);
|
|
const end = content.indexOf(close);
|
|
assert.ok(start !== -1, `<${tag}> block must exist in agent`);
|
|
assert.ok(end !== -1, `</${tag}> must close the block`);
|
|
return content.slice(start + open.length, end);
|
|
}
|
|
|
|
describe('gsd-roadmapper granularity calibration (#163)', () => {
|
|
const section = granularitySection(readAgent('gsd-roadmapper'));
|
|
|
|
test('Coarse bucket is tightened to 2-4', () => {
|
|
assert.ok(/\|\s*Coarse\s*\|\s*2-4\s*\|/.test(section), 'Coarse must be 2-4');
|
|
});
|
|
|
|
test('Standard bucket is tightened to 4-6', () => {
|
|
assert.ok(/\|\s*Standard\s*\|\s*4-6\b/.test(section), 'Standard must be 4-6');
|
|
});
|
|
|
|
test('Fine bucket is tightened to 6-10', () => {
|
|
assert.ok(/\|\s*Fine\s*\|\s*6-10\s*\|/.test(section), 'Fine must be 6-10');
|
|
});
|
|
|
|
test('no granularity row maps to an old bucket (3-5 / 5-8 / 8-12)', () => {
|
|
// Scope to the second ("Typical Phases") column of each row so the approved
|
|
// explanatory footnote mentioning "5-8" in the third column does not false-fail.
|
|
assert.ok(!/\|\s*Coarse\s*\|\s*3-5\b/.test(section), 'Coarse must not map to 3-5');
|
|
assert.ok(!/\|\s*Standard\s*\|\s*5-8\b/.test(section), 'Standard must not map to 5-8');
|
|
assert.ok(!/\|\s*Fine\s*\|\s*8-12\b/.test(section), 'Fine must not map to 8-12');
|
|
});
|
|
|
|
test('Key paragraph names the thin-phase pattern and prefers folding into a neighbor', () => {
|
|
assert.ok(
|
|
section.includes('fold it into the most-related neighbor'),
|
|
'Key guidance must instruct folding thin phases into the most-related neighbor'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('gsd-roadmapper phase_id_convention support (#1205)', () => {
|
|
const content = readAgent('gsd-roadmapper');
|
|
|
|
test('phase_identification section reads phase_id_convention from config', () => {
|
|
const section = extractBlock(content, 'phase_identification');
|
|
assert.ok(
|
|
section.includes('phase_id_convention'),
|
|
'phase_identification block must reference phase_id_convention config key'
|
|
);
|
|
});
|
|
|
|
test('output_formats documents milestone-prefixed header format', () => {
|
|
const section = extractBlock(content, 'output_formats');
|
|
assert.ok(
|
|
section.includes('milestone-prefixed'),
|
|
'output_formats block must document the milestone-prefixed convention'
|
|
);
|
|
});
|
|
|
|
test('output_formats shows milestone-prefixed phase header example (e.g. ### Phase 1-01:)', () => {
|
|
const section = extractBlock(content, 'output_formats');
|
|
assert.ok(
|
|
/###\s+Phase\s+\d+-\d{2}:/.test(section),
|
|
'output_formats must show a milestone-prefixed header example like "### Phase 1-01: Name"'
|
|
);
|
|
});
|
|
|
|
test('output_formats shows both sequential and milestone-prefixed summary checklist forms', () => {
|
|
const section = extractBlock(content, 'output_formats');
|
|
assert.ok(
|
|
/- \[ \] \*\*Phase \d+:/.test(section),
|
|
'output_formats must still show sequential summary checklist form "- [ ] **Phase N:"'
|
|
);
|
|
assert.ok(
|
|
/- \[ \] \*\*Phase \d+-\d{2}:/.test(section),
|
|
'output_formats must show milestone-prefixed checklist form "- [ ] **Phase N-NN:"'
|
|
);
|
|
});
|
|
|
|
test('phase_identification section falls back to sequential when convention absent or "sequential"', () => {
|
|
const section = extractBlock(content, 'phase_identification');
|
|
assert.ok(
|
|
section.includes('sequential'),
|
|
'phase_identification block must document that sequential is the default/fallback'
|
|
);
|
|
});
|
|
|
|
test('phase headings and checklists must not include project_code (#1455)', () => {
|
|
const phaseIdentification = extractBlock(content, 'phase_identification');
|
|
const outputFormats = extractBlock(content, 'output_formats');
|
|
const combined = `${phaseIdentification}\n${outputFormats}`;
|
|
|
|
assert.ok(
|
|
combined.includes('project_code'),
|
|
'roadmapper instructions must explicitly mention project_code'
|
|
);
|
|
assert.ok(
|
|
/project_code[\s\S]{0,120}Never include|Do not include `project_code`/.test(combined),
|
|
'roadmapper must state that project_code is not part of phase headings/checklists'
|
|
);
|
|
});
|
|
});
|