feat(#2128): phase-id anti-divergence guard — canonical token source + drift scanner + guards
Phase 4 of epic #2121 (ADR-2121 Decision 7), closing the recurrence loop that produced #2111 / #2114 / #2104: no module outside src/phase-id.cts may re-implement phase-ID parsing without failing CI. - phase-id.cts: add PHASE_NUMBER_TOKEN_SOURCE — the canonical phase-number-token grammar (\d+[A-Z]?(?:\.\d+)*) for enumeration/scan call sites, the ANY-phase counterpart to phaseMarkdownRegexSource(n)'s known-number lookup. Extend-only (never touches normalizePhaseName; blast radius 79 fns / CRITICAL). - scripts/lint-phase-id-drift.cjs: pure findPhaseIdRegexDrift(text) + scanRepo(root), wired to `npm run check:phase-id-drift`. Flags a literal re-derivation of the canonical token (both /\d/ and new-RegExp `\\d` escaping, plus the [A-Za-z] and [.-] near-variants) anywhere in src/** outside phase-id.cts, unless sanctioned with `// phase-id-owner: <reason>`. Narrow by design: bare \d+, digits-only captures, \w ids, status-message text and pipe-tables are not flagged. - tests/phase-id-drift-guard.test.cjs: fail-first drift cases (AC1) + live scanRepo(ROOT) zero-drift (AC3) + identity guard — phase-id.cjs exports the complete locked surface and no consumer re-exports a divergent copy (AC2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,18 @@ const OPTIONAL_PROJECT_CODE_PREFIX_SOURCE = '(?:[A-Z][A-Z0-9_]*-)?';
|
||||
// source. Both forms must change together; see the #1729 regression test.
|
||||
const OPTIONAL_PHASE_TAG_SOURCE = '(?:\\s*\\([^)\\n]*\\))?';
|
||||
|
||||
// #2128: the canonical phase-NUMBER-TOKEN grammar — a phase number with an
|
||||
// optional single-letter variant suffix and optional dotted sub-phases
|
||||
// (1, 01, 12A, 12.1, 3.2.1). This is the ENUMERATION/scan counterpart to
|
||||
// phaseMarkdownRegexSource: use phaseMarkdownRegexSource(n) to build a source
|
||||
// for ONE KNOWN number; reference this constant when a call site must match ANY
|
||||
// phase and capture its token. Enumeration/parse sites inline this into a
|
||||
// `new RegExp(...)` instead of re-deriving the grammar as a literal, so every
|
||||
// phase-token producer shares one owner. The anti-divergence guard
|
||||
// (scripts/lint-phase-id-drift.cjs) fails CI if a literal re-derivation is
|
||||
// introduced outside this module without a `// phase-id-owner:` justification.
|
||||
const PHASE_NUMBER_TOKEN_SOURCE = '\\d+[A-Z]?(?:\\.\\d+)*';
|
||||
|
||||
function stripProjectCodePrefix(value: unknown, caseInsensitive = true): string {
|
||||
const input = String(value);
|
||||
const re = caseInsensitive ? PROJECT_CODE_PREFIX_STRIP_RE_I : PROJECT_CODE_PREFIX_STRIP_RE;
|
||||
@@ -350,6 +362,7 @@ export = {
|
||||
escapeRegex,
|
||||
OPTIONAL_PROJECT_CODE_PREFIX_SOURCE,
|
||||
OPTIONAL_PHASE_TAG_SOURCE,
|
||||
PHASE_NUMBER_TOKEN_SOURCE,
|
||||
stripProjectCodePrefix,
|
||||
normalizePhaseName,
|
||||
getMilestoneFromPhaseId,
|
||||
|
||||
Reference in New Issue
Block a user