fix(#3864): smart-entry classify() matches the verif* status stem (#4052)

* test(#3864): smart-entry classify must match the verif* status stem (failing first)

* fix(#3864): classify() matches the verif* status stem, aligning with normalizeStateStatus

"verified"/"verification" contain no "verify" substring, so the
exact-word branch never matched: a STATE.md declaring status: verified
fell through to situation "unknown" (or idle-stranded on a clean tree
with unpushed commits — differently wrong, which made it look
intermittent). state-document's normalizeStateStatus already matches
any verif* stem; the classifier now uses the same stem, so the two
owners agree on the invariant. verify_failed is tested earlier in
classify(), so failed verification still wins. Negative controls
(completed/executing/planning/paused) unchanged and pinned.

* test(#3864): review fold-in — the real handler-written verification status pinned both ways

'Phase complete — ready for verification' (state-document.cts's own
Status default) carries the verif stem but names completion: at 5/5 it
must stay complete (isComplete beats the stem), mid-project it must
route to verify-pending (pre-fix it fell through to unknown/idle-
stranded — a real behavior change beyond the literal verified repro,
sanctioned by the issue's own verification→verify-pending table).

* chore(#3864): changeset fragment (pr number backfilled after PR creation)

* chore(#3864): backfill changeset PR number (4052)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-29 15:07:14 -04:00
committed by GitHub
parent 9932929e37
commit d9e906a744
3 changed files with 58 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 4052
---
smart-entry classifies a STATE.md status of verified (or verification) as verify-pending instead of falling through to unknown — the classifier matched the exact word verify and missed the verif stem its own normalizeStateStatus uses (#3864)

View File

@@ -599,7 +599,13 @@ export function classify(s: SmartEntrySignals): Situation {
if (isComplete(s)) return 'complete';
if (/\bplanning|planned\b/i.test(s.status)) return 'planning';
if (/\bexecut(e|ing)|active|in.progress|building\b/i.test(s.status)) return 'executing';
if (/\bverify|review|needs.review|pending.review\b/i.test(s.status)) return 'verify-pending';
// #3864: `verif` (not `verify`) — the same stem state-document's
// normalizeStateStatus matches. "verified" and "verification" contain no
// "verify" substring, so the exact word left them falling through to
// `unknown` (or idle-stranded on a clean unpushed tree — differently
// wrong). verify_failed is tested above, so a failed verification still
// wins over this branch.
if (/\bverif|review|needs.review|pending.review\b/i.test(s.status)) return 'verify-pending';
if (isIdleStranded(s)) return 'idle-stranded';
return 'unknown';
}

View File

@@ -114,6 +114,52 @@ describe('smart-entry: situation coverage', () => {
});
}
// #3860-family guard is NOT wanted here; these are #3864's missing pins:
// the classifier's verify branch must match the same verif* stem that
// state-document's normalizeStateStatus matches, or a STATE.md declaring
// `status: verified` falls through to `unknown` (and, on a clean tree with
// unpushed commits, to idle-stranded — differently wrong).
test('#3864: status "verified" classifies as verify-pending', () => {
const dir = track(makeProject({ state: state({ status: 'verified', total_phases: 5, current_phase: 2 }), roadmap: true }));
const result = classifyProject(dir);
assert.equal(result.situation, 'verify-pending');
});
test('#3864: status "verification" classifies as verify-pending', () => {
const dir = track(makeProject({ state: state({ status: 'verification', total_phases: 5, current_phase: 2 }), roadmap: true }));
const result = classifyProject(dir);
assert.equal(result.situation, 'verify-pending');
});
test('#3864: the stem does not over-match — "completed" still reaches complete', () => {
const dir = track(makeProject({ state: state({ status: 'completed', total_phases: 5, current_phase: 5 }), roadmap: true }));
const result = classifyProject(dir);
assert.equal(result.situation, 'complete');
});
test('#3864: "Phase complete — ready for verification" at 5/5 stays complete (complete beats a verif-bearing status)', () => {
// A real handler-written Status default (state-document.cts). It carries
// the verif stem but names COMPLETION — isComplete must win over the
// verify-pending branch when the project is actually done.
const dir = track(makeProject({ state: state({ status: 'Phase complete — ready for verification', total_phases: 5, current_phase: 5 }), roadmap: true }));
const result = classifyProject(dir);
assert.equal(result.situation, 'complete');
});
test('#3864: "Phase complete — ready for verification" mid-project is verify-pending', () => {
// Same real status word, phases remaining: pre-fix this fell through to
// unknown/idle-stranded (no "verify" substring); the stem now routes it.
const dir = track(makeProject({ state: state({ status: 'Phase complete — ready for verification', total_phases: 5, current_phase: 2 }), roadmap: true }));
const result = classifyProject(dir);
assert.equal(result.situation, 'verify-pending');
});
test('#3864: verify-failed still wins over the verify-pending stem (ordering pinned)', () => {
const dir = track(makeProject({ state: state({ status: 'verify-failed', total_phases: 5, current_phase: 2 }), roadmap: true, verifyFail: true }));
const result = classifyProject(dir);
assert.equal(result.situation, 'verify-failed');
});
test('SITUATIONS constant lists all 11 (incl unknown) and is frozen', () => {
assert.equal(SITUATIONS.length, 11);
assert.ok(SITUATIONS.includes('unknown'));