* test(#1956): failing-first contract for cross-artifact fact-drift pass * feat(#1956): flag cross-artifact fact drift in the plan drift guard * fix(#1956): correct config-key assertion and bidirectional lifecycle-lag exemption * docs(#1956): document the cross-artifact axis in the architecture reference * feat(#1956): decide the phase-status drift axis deterministically * fix(#1956): scope the progress-table lookup, abstain without a position section, rank deferred * docs(#1956): backfill changeset pr number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -13,7 +13,8 @@
|
||||
* - ./phase-id.cjs (escapeRegex, phaseMarkdownRegexSource)
|
||||
* - ./planning-workspace.cjs (planningDir)
|
||||
* - ./shell-command-projection.cjs (platformReadSync)
|
||||
* - ./markdown-sectionizer.cjs (tokenizeHeadings, stripTaggedBlocks, withSection)
|
||||
* - ./markdown-sectionizer.cjs (tokenizeHeadings, stripTaggedBlocks, withSection, collectSection)
|
||||
* - ./markdown-table.cjs (findTableWithColumns)
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
@@ -38,8 +39,10 @@ import { platformReadSync } from './shell-command-projection.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import unusableInputMod = require('./unusable-input.cjs');
|
||||
const { UNUSABLE_REASON, warnUnusableInput } = unusableInputMod;
|
||||
import { tokenizeHeadings, stripTaggedBlocks, withSection, stripFencedCode } from './markdown-sectionizer.cjs';
|
||||
import { tokenizeHeadings, stripTaggedBlocks, withSection, stripFencedCode, collectSection } from './markdown-sectionizer.cjs';
|
||||
import type { HeadingToken } from './markdown-sectionizer.cjs';
|
||||
import { findTableWithColumns } from './markdown-table.cjs';
|
||||
import type { MarkdownTable } from './markdown-table.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import planningScopeMod = require('./planning-scope.cjs');
|
||||
const { SCOPE } = planningScopeMod;
|
||||
@@ -882,6 +885,42 @@ function reportUnreadableRoadmap(err: unknown, roadmapPath: string): void {
|
||||
warnUnusableInput({ reason: UNUSABLE_REASON.ROADMAP_UNREADABLE, source: roadmapPath });
|
||||
}
|
||||
|
||||
// ─── Roadmap progress table (#1956/#2012 decoy avoidance) ─────────────────────
|
||||
|
||||
/**
|
||||
* Locate ROADMAP.md's "Progress" table — the sole owner of the #2012
|
||||
* decoy-avoidance scope for the `drift-guard phase-status` CLI seam (#1956).
|
||||
*
|
||||
* Scopes to the `## Progress` heading first (level-2, exact case-insensitive
|
||||
* text `'progress'`, `{ levelBounded: true }`) via `collectSection` — the
|
||||
* same CRLF-safe seam `stateCurrentPositionSlice` (state-document.cts) uses
|
||||
* to scope STATE.md's `## Current Position` — so a differently-headed table
|
||||
* that happens to share the same column names (e.g. an "Archive Notes"
|
||||
* table) is never picked up instead of the real one (#2012). Falls back to
|
||||
* scanning the WHOLE document when no `## Progress` heading exists, so a
|
||||
* headingless milestone slice (#1445) still resolves rather than going
|
||||
* uncheckable — the same fallback `deriveProgressFromRoadmap`
|
||||
* (phase-lifecycle.cts) deliberately preserves.
|
||||
*
|
||||
* `deriveProgressFromRoadmap` independently expresses this same "scope to
|
||||
* `## Progress`, else whole document" rule via its own regex-based scope
|
||||
* (kept there deliberately rather than refactored onto this function — its
|
||||
* blast radius is large). The two locators are therefore separate
|
||||
* implementations of the same scoping rule and must agree about WHICH table
|
||||
* is the Progress table; a parity test in
|
||||
* tests/adr-22-plan-drift-guard.test.cjs asserts they do, per the repo's
|
||||
* generative-fix-divergence guard.
|
||||
*
|
||||
* Returns the same shape `findTableWithColumns` returns (or `null`).
|
||||
*/
|
||||
function findRoadmapProgressTable(roadmapContent: string): MarkdownTable | null {
|
||||
const isProgressHeading = (h: HeadingToken): boolean =>
|
||||
h.level === 2 && h.text.trim().toLowerCase() === 'progress';
|
||||
const section = collectSection(roadmapContent, isProgressHeading, { levelBounded: true });
|
||||
const scoped = section ? section.body : roadmapContent;
|
||||
return findTableWithColumns(scoped, ['Phase', 'Plans Complete', 'Status', 'Completed']);
|
||||
}
|
||||
|
||||
// ─── Milestone info lookup ────────────────────────────────────────────────────
|
||||
|
||||
interface MilestoneInfo {
|
||||
@@ -1465,4 +1504,7 @@ export = {
|
||||
sliceMilestoneWindow,
|
||||
hasVersionedMilestones,
|
||||
hasMilestoneSectioning,
|
||||
// #1956: sole owner of the #2012 decoy-avoidance scope for the
|
||||
// `drift-guard phase-status` CLI seam.
|
||||
findRoadmapProgressTable,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user