* test(#4764): failing-first — dep_phases must extract only Phase-prefixed references, never dates/shas/ledger ids/self * fix(#4764): dep_phases anchors phase references to their 'Phase' prose context and never emits the row's own number * fix(#4764): review fold-ins — hoist the anchored dep-reference grammar to phase-id, cover Oxford lists and hyphen ranges, repair the property test Adversarial review found: Oxford-comma lists under-extracted ('Phases 1, 2, and 3' dropped the tail member — a silent real-blocker clear, the dangerous direction); hyphen ranges ('Phases 1-3') kept only the first endpoint; the property test called fc.hexaString (absent in fast-check 4.8, threw every run) and passed the junk arbitrary unspread (vacuous guard) with no completeness assertion; planning-inspect's extractDependencyTokens carried the same whole-field scrape (generative-fix divergence). The anchored grammar now lives beside PHASE_NUMBER_TOKEN_SOURCE in phase-id.cts and both readers interpolate it. * chore(#4764): backfill changeset PR number (4876) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
36
src/init.cts
36
src/init.cts
@@ -105,6 +105,7 @@ const {
|
||||
matchPhaseDirs,
|
||||
stripProjectCodePrefix,
|
||||
PHASE_NUMBER_TOKEN_SOURCE,
|
||||
PHASE_DEP_REF_SOURCE,
|
||||
isForeignPrefixedPhaseQuery,
|
||||
isSentinelPhaseId,
|
||||
extractPhaseToken,
|
||||
@@ -3105,6 +3106,23 @@ function cmdInitManager(cwd: string, raw: boolean): void {
|
||||
return reaches(numA, numB) || reaches(numB, numA);
|
||||
}
|
||||
|
||||
// #4764: a phase reference in depends_on prose is a PHASE-SHAPED token in
|
||||
// context — directly following "Phase"/"Phases" — never a bare digit run.
|
||||
// The previous whole-field scrape matched the token grammar against every
|
||||
// digit run, so calendar dates ("2026-09-14" → 2026, 09, 14), git shas
|
||||
// ("8bf403100d" → 8b, 403100d, …), bracketed ledger ids (WINDOWS #1843) and
|
||||
// the row's OWN number all became "dependencies", and deps_satisfied came
|
||||
// back false for phases whose prose declares none (50 of 92 phases in the
|
||||
// reporter's milestone). The anchored grammar (owned by phase-id.cts as
|
||||
// PHASE_DEP_REF_SOURCE, shared with planning-inspect's dependencies) keeps
|
||||
// lists fully extracted ("Phases 601 and 602", "Phase 601, 602, and 603",
|
||||
// "Phase 1-3") — silently dropping a REAL dependency would clear
|
||||
// deps_satisfied prematurely, the dangerous direction. Negation prose
|
||||
// ("dropped the dependency on Phase 654") is NOT detected: the issue's own
|
||||
// minimum keeps such tokens.
|
||||
const depPhaseRefRe = new RegExp(`${PHASE_DEP_REF_SOURCE}`, 'gi');
|
||||
const depTokenRe = new RegExp(`${PHASE_NUMBER_TOKEN_SOURCE}`, 'gi');
|
||||
|
||||
for (const phase of phases) {
|
||||
if (
|
||||
!phase['depends_on'] ||
|
||||
@@ -3112,7 +3130,23 @@ function cmdInitManager(cwd: string, raw: boolean): void {
|
||||
) {
|
||||
phase['deps_satisfied'] = true;
|
||||
} else {
|
||||
const depNums = (phase['depends_on'] as string).match(new RegExp(`${PHASE_NUMBER_TOKEN_SOURCE}`, 'gi')) || [];
|
||||
const prose = phase['depends_on'] as string;
|
||||
const ownNumber = normalizePhaseNumber(phase['number'] as string);
|
||||
const depNums: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
let refMatch: RegExpExecArray | null;
|
||||
depPhaseRefRe.lastIndex = 0;
|
||||
while ((refMatch = depPhaseRefRe.exec(prose)) !== null) {
|
||||
let tok: RegExpExecArray | null;
|
||||
depTokenRe.lastIndex = 0;
|
||||
while ((tok = depTokenRe.exec(refMatch[1])) !== null) {
|
||||
const normalized = normalizePhaseNumber(tok[0]);
|
||||
if (normalized === ownNumber) continue; // #4764: never the row's own phase
|
||||
if (seen.has(normalized)) continue;
|
||||
seen.add(normalized);
|
||||
depNums.push(tok[0]);
|
||||
}
|
||||
}
|
||||
phase['deps_satisfied'] = depNums.every((n) => completedNums.has(normalizePhaseNumber(n)));
|
||||
phase['dep_phases'] = depNums;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user