* fix(#3280): read frontmatter current_phase in w011 and de-force w027 worktree advice * chore(#3280): add changeset * chore(#3280): reference pr 3452 in changeset --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -57,6 +57,9 @@ type PlanningSnapshot = ReturnType<typeof planningSnapshotMod.buildPlanningSnaps
|
||||
import phaseIdMod = require('../phase-id.cjs');
|
||||
const { getMilestoneFromPhaseId, matchPhaseDirs, normalizePhaseName, extractPhaseToken, PHASE_NUMBER_TOKEN_SOURCE } =
|
||||
phaseIdMod;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- the single owner of the persisted status vocabulary
|
||||
import stateDocumentMod = require('../state-document.cjs');
|
||||
const { normalizeStateStatus } = stateDocumentMod;
|
||||
|
||||
// ─── W024 — STATE.md commit-age freshness (DELIBERATELY INERT) ─────────────
|
||||
|
||||
@@ -197,7 +200,12 @@ const RULE_W002: Rule = {
|
||||
* (which uses `Phase: [X] of [Y] ([Phase name])` under `## Current
|
||||
* Position`) — `currentPhaseLabel` is the parsed owner of that exact field,
|
||||
* so extracting its leading number is the equivalent-intent read against
|
||||
* the template STATE.md actually ships.
|
||||
* the template STATE.md actually ships. #3280: the label's ladder
|
||||
* (`buildStateFields`, `src/planning-snapshot.cts`) now leads with the
|
||||
* frontmatter `current_phase` scalar — the key `gsd-tools state update` /
|
||||
* `state begin-phase` persist — so a bare `"2"` is the expected value on
|
||||
* the machine-readable format, and this leading-number extraction handles
|
||||
* it identically.
|
||||
*/
|
||||
function currentPhaseIdFromLabel(label: string | null): string | null {
|
||||
if (!label) return null;
|
||||
@@ -215,13 +223,21 @@ const RULE_W011: Rule = {
|
||||
if (phaseId === null) return [];
|
||||
const checked = snapshot.roadmapPhaseCheckboxes.value[phaseId];
|
||||
if (checked !== true) return [];
|
||||
const statusVal = (snapshot.stateStatus.value ?? '').trim().toLowerCase();
|
||||
if (statusVal === 'complete' || statusVal === 'done') return [];
|
||||
// #3280: the state writer persists `status` through `normalizeStateStatus`
|
||||
// (`state.cts`'s syncStateFrontmatter), whose completion token is
|
||||
// `completed` — an exact `'complete' || 'done'` comparison rejects the
|
||||
// exact vocabulary the product writes and turns every legitimately
|
||||
// completed frontmatter STATE.md into a false positive. Route the
|
||||
// comparison through the same seam that owns the vocabulary
|
||||
// (`state-document.cjs`'s `normalizeStateStatus`) rather than growing a
|
||||
// second bespoke token list here.
|
||||
const statusVal = (snapshot.stateStatus.value ?? '').trim();
|
||||
if (normalizeStateStatus(statusVal, null) === 'completed') return [];
|
||||
return [
|
||||
{
|
||||
code: 'W011',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: `STATE.md says current phase is ${phaseId} (status: ${statusVal || 'unknown'}) but ROADMAP.md shows it as [x] complete — state files may be out of sync`,
|
||||
message: `STATE.md says current phase is ${phaseId} (status: ${statusVal.toLowerCase() || 'unknown'}) but ROADMAP.md shows it as [x] complete — state files may be out of sync`,
|
||||
remedy: adviseRemedy('Run /gsd-progress to re-derive current position, or manually update STATE.md'),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -153,8 +153,16 @@ function checkW027(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
diagnostics.push({
|
||||
code: 'W027',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: `Stale git worktree: ${finding.path} (last modified ${finding.ageMinutes} minutes ago). Run: git worktree remove ${finding.path} --force`,
|
||||
remedy: adviseRemedy('git worktree remove <path> --force'),
|
||||
// #3280: staleness is a pure mtime heuristic and carries no information
|
||||
// about whether the tree is clean — git's own refusal of a non-forced
|
||||
// removal on a dirty tree is the safety net, so the remediation must
|
||||
// direct the operator (or an agent executing this literally) to check
|
||||
// for uncommitted work FIRST and keep `--force` an explicit discard
|
||||
// opt-in, never the default instruction.
|
||||
message: `Stale git worktree: ${finding.path} (last modified ${finding.ageMinutes} minutes ago). Inspect uncommitted work first: git -C ${finding.path} status --porcelain; if clean run: git worktree remove ${finding.path}; only add --force to discard changes`,
|
||||
remedy: adviseRemedy(
|
||||
'git -C <path> status --porcelain; if clean: git worktree remove <path>; add --force only to discard changes',
|
||||
),
|
||||
});
|
||||
}
|
||||
return diagnostics;
|
||||
|
||||
@@ -307,13 +307,22 @@ function buildStateFields(statePath: string): StateFields {
|
||||
const section = stateCurrentPositionSlice(body);
|
||||
const currentPositionScope = section === null ? SCOPE.TRUNCATED : SCOPE.COMPLETE;
|
||||
|
||||
// #1760 fallback ladder (mirrors `state.cts:1499-1500`'s `resolveStatePhase`
|
||||
// exactly, same `section ?? body` scope for both reads): the legacy bold
|
||||
// `**Current Phase:**` field (what `verify.cts:2109-2111` originally
|
||||
// matched, and what pre-template-migration STATE.md fixtures still use)
|
||||
// takes priority over the current template's bare `Phase: [X] of [Y]`
|
||||
// field — a document carrying both is read the same way `resolveStatePhase`
|
||||
// reads it elsewhere.
|
||||
// #1760 fallback ladder — now a full mirror of `state.cts`'s
|
||||
// `resolveStatePhase` (its three-source ladder at `state.cts:1494-1516`),
|
||||
// including the frontmatter step that ladder leads with:
|
||||
// 1. frontmatter `current_phase` scalar — the machine-readable key
|
||||
// `gsd-tools state update` / `state begin-phase` persist via
|
||||
// `syncStateFrontmatter` (`state.cts:2023`), so it takes PRIORITY over
|
||||
// any body field (#3280: a body-only ladder left W011 structurally
|
||||
// blind to the one format the product itself writes — a stale body
|
||||
// `Phase:` remnant even SHADOWED the current frontmatter value).
|
||||
// 2. the legacy bold `**Current Phase:**` field (what `verify.cts:2109-
|
||||
// 2111` originally matched, and what pre-template-migration STATE.md
|
||||
// fixtures still use).
|
||||
// 3. the current template's bare `Phase: [X] of [Y]` field.
|
||||
// A document carrying several is read the same way `resolveStatePhase`
|
||||
// reads it elsewhere — frontmatter first, then body, in that order.
|
||||
const frontmatterCurrentPhase = stateFieldValue(frontmatter, body, 'current_phase', null);
|
||||
const legacyCurrentPhaseLabel = stateFieldValue(frontmatter, section ?? body, null, 'Current Phase', {
|
||||
scope: currentPositionScope,
|
||||
});
|
||||
@@ -321,8 +330,14 @@ function buildStateFields(statePath: string): StateFields {
|
||||
scope: currentPositionScope,
|
||||
});
|
||||
const currentPhaseLabel = {
|
||||
value: legacyCurrentPhaseLabel.value ?? templateCurrentPhaseLabel.value,
|
||||
scope: legacyCurrentPhaseLabel.value !== null ? legacyCurrentPhaseLabel.scope : templateCurrentPhaseLabel.scope,
|
||||
value:
|
||||
frontmatterCurrentPhase.value ?? legacyCurrentPhaseLabel.value ?? templateCurrentPhaseLabel.value,
|
||||
scope:
|
||||
frontmatterCurrentPhase.value !== null
|
||||
? frontmatterCurrentPhase.scope
|
||||
: legacyCurrentPhaseLabel.value !== null
|
||||
? legacyCurrentPhaseLabel.scope
|
||||
: templateCurrentPhaseLabel.scope,
|
||||
};
|
||||
const stateStatus = stateFieldValue(frontmatter, section ?? body, 'status', 'Status', {
|
||||
scope: currentPositionScope,
|
||||
|
||||
Reference in New Issue
Block a user