* test(#3163): phase add must insert in the active milestone, not the trailing archive Regression for #3163: cmdPhaseAdd/cmdPhaseAddBatch pick the insertion point via rawContent.lastIndexOf('\n---'), the file's last horizontal rule — which on a roadmap with shipped/history material after the active phase list sits deep in archive. Rows 1/2/4 fail RED on next (entry lands after the archive heading); row 3 guards the no-milestone legacy fallback. * fix(#3163): scope phase.add insertion to the current milestone window cmdPhaseAdd and cmdPhaseAddBatch picked the insertion point via rawContent.lastIndexOf('\n---') — the file's last horizontal rule, which on a roadmap with shipped/history material after the active phase list sits deep in archive. Extract phaseEntryInsertOffset(rawContent, cwd): scope the search to currentMilestoneRawRanges' primary window so the entry lands at the end of the active phase list. Fall back to the legacy whole-file heuristic when no current milestone resolves, preserving simple no-milestone roadmaps. Applies to both cmdPhaseAdd and cmdPhaseAddBatch (identical expression); the decimal insert path was already header-anchored and is untouched. * docs(#3163): add changeset * docs(#3163): backfill changeset PR number (3400) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -993,6 +993,27 @@ function describeGoalShapedTitle(description: string): string | null {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* #3163: compute the byte offset in `rawContent` where a new `### Phase N:`
|
||||
* entry should be inserted — at the end of the active phase list, scoped to the
|
||||
* CURRENT MILESTONE so the entry can never land before a trailing `---` in
|
||||
* shipped/history/backlog material (the file's last `---` on a long roadmap
|
||||
* sits deep in archive). When no current milestone can be resolved (no
|
||||
* STATE.md `milestone:` and no in-progress `🚧`/`🔄` marker), fall back to the
|
||||
* legacy whole-file lastIndexOf('\n---') so simple no-milestone roadmaps keep
|
||||
* their existing behavior.
|
||||
*/
|
||||
function phaseEntryInsertOffset(rawContent: string, cwd: string): number {
|
||||
const ranges = currentMilestoneRawRanges(rawContent, cwd);
|
||||
if (!ranges) {
|
||||
const legacy = rawContent.lastIndexOf('\n---');
|
||||
return legacy > 0 ? legacy : rawContent.length;
|
||||
}
|
||||
const window = rawContent.slice(ranges.primary.start, ranges.primary.end);
|
||||
const lastSeparator = window.lastIndexOf('\n---');
|
||||
return lastSeparator > 0 ? ranges.primary.start + lastSeparator : ranges.primary.end;
|
||||
}
|
||||
|
||||
function cmdPhaseAdd(cwd: string, description: string, raw: boolean, customId?: string): void {
|
||||
if (!description) {
|
||||
error('description required for phase add');
|
||||
@@ -1083,13 +1104,8 @@ function cmdPhaseAdd(cwd: string, description: string, raw: boolean, customId?:
|
||||
const phaseEntry =
|
||||
`\n### Phase ${_newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${formatGsdSlash('plan-phase', resolveRuntime(cwd)) as string} ${_newPhaseId} to break down)\n`;
|
||||
|
||||
let updatedContent: string;
|
||||
const lastSeparator = rawContent.lastIndexOf('\n---');
|
||||
if (lastSeparator > 0) {
|
||||
updatedContent = rawContent.slice(0, lastSeparator) + phaseEntry + rawContent.slice(lastSeparator);
|
||||
} else {
|
||||
updatedContent = rawContent + phaseEntry;
|
||||
}
|
||||
const insertAt = phaseEntryInsertOffset(rawContent, cwd);
|
||||
const updatedContent = rawContent.slice(0, insertAt) + phaseEntry + rawContent.slice(insertAt);
|
||||
|
||||
platformWriteSync(roadmapPath, updatedContent);
|
||||
return { newPhaseId: _newPhaseId, dirName: _dirName };
|
||||
@@ -1174,11 +1190,8 @@ function cmdPhaseAddBatch(cwd: string, descriptions: string[], raw: boolean): vo
|
||||
: `\n**Depends on:** Phase ${typeof newPhaseId === 'number' ? newPhaseId - 1 : 'TBD'}`;
|
||||
const phaseEntry =
|
||||
`\n### Phase ${newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run ${formatGsdSlash('plan-phase', resolveRuntime(cwd)) as string} ${newPhaseId} to break down)\n`;
|
||||
const lastSeparator = rawContent.lastIndexOf('\n---');
|
||||
rawContent =
|
||||
lastSeparator > 0
|
||||
? rawContent.slice(0, lastSeparator) + phaseEntry + rawContent.slice(lastSeparator)
|
||||
: rawContent + phaseEntry;
|
||||
const insertAt = phaseEntryInsertOffset(rawContent, cwd);
|
||||
rawContent = rawContent.slice(0, insertAt) + phaseEntry + rawContent.slice(insertAt);
|
||||
added.push({
|
||||
phase_number: typeof newPhaseId === 'number' ? newPhaseId : String(newPhaseId),
|
||||
padded:
|
||||
|
||||
Reference in New Issue
Block a user