refactor(#2128): single-source 23 phase-token re-derivations; sanction 14 context-specific sites

Route 23 literal re-derivations of the canonical phase-number token through
phase-id.cjs `PHASE_NUMBER_TOKEN_SOURCE` (via new RegExp). Each conversion was
proven BYTE-IDENTICAL (old.source === new.source && old.flags === new.flags), so
the runtime regexes are unchanged — zero behavior change by construction.

The remaining 14 phase-token sites are genuine but context-specific and stay
literal with a `// phase-id-owner: <reason>` sanction: dir-name parses whose
dash-continuation semantics differ from extractPhaseToken, and the [A-Za-z]
case-variant / [.-] dot-or-dash separator forms that are not source-byte-equal
to the canonical token.

Scanner (`npm run check:phase-id-drift`) is now green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-10 08:49:36 -04:00
parent 09be501eb7
commit dfad3a7510
12 changed files with 60 additions and 30 deletions

View File

@@ -37,6 +37,7 @@ const {
phaseTokenMatches,
OPTIONAL_PROJECT_CODE_PREFIX_SOURCE,
OPTIONAL_PHASE_TAG_SOURCE,
PHASE_NUMBER_TOKEN_SOURCE,
} = phaseIdMod;
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
import phaseLocatorMod = require('./phase-locator.cjs');
@@ -374,8 +375,9 @@ function cmdFindPhase(cwd: string, phase: string, raw: boolean): void {
if (!match) continue;
const dirMatch =
match.match(new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}(\\d+[A-Z]?(?:\\.\\d+)*)-?(.*)`, 'i')) ||
match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
match.match(
new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}(${PHASE_NUMBER_TOKEN_SOURCE})-?(.*)`, 'i')
) || match.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})-?(.*)`, 'i'));
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
@@ -1672,7 +1674,7 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
.sort((a, b) => comparePhaseNum(a, b));
for (const dir of dirs) {
const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
const dm = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})-?(.*)`, 'i'));
if (dm) {
if (/^999(?:\.|$)/.test(dm[1])) continue;
if (comparePhaseNum(dm[1], phaseNum) > 0) {
@@ -1705,7 +1707,10 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
// #1729: `(?:\s*\([^)\n]*\))?` after the number tolerates a pre-colon
// ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE) so
// `### Phase N (Cluster B): X` resolves. Captures are unchanged.
const phasePattern = /(?:#{2,4}|-\s*\[[ xX]\])\s*(?:\*\*|__)?\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n*]+)/gi;
const phasePattern = new RegExp(
`(?:#{2,4}|-\\s*\\[[ xX]\\])\\s*(?:\\*\\*|__)?\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]*\\))?\\s*:\\s*([^\\n*]+)`,
'gi'
);
let pm: RegExpExecArray | null;
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
if (comparePhaseNum(pm[1], phaseNum) > 0) {
@@ -1741,8 +1746,10 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
if (isLastPhase && roadmapContent !== null) {
try {
const milestoneScope = extractCurrentMilestone(roadmapContent, cwd);
const cbPattern =
/-\s*\[(x| )\]\s*(?:\*\*|__)?\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n*]+)/gi;
const cbPattern = new RegExp(
`-\\s*\\[(x| )\\]\\s*(?:\\*\\*|__)?\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]*\\))?\\s*:\\s*([^\\n*]+)`,
'gi'
);
let cbm: RegExpExecArray | null;
let lowestOutstanding: { num: string; name: string } | null = null;
while ((cbm = cbPattern.exec(milestoneScope)) !== null) {