* fix(#3189): drop non-id prose from phase_req_ids before gap check * chore(#3189): pin changeset fragment to pr 3438 * fix(#3189): use hyphenated IDs in e2e fixtures (parseRequirements requires hyphen) * fix(#3189): assert e2e item set via table, not rows (check query has no rows) * fix(#3189): check prose fragments against table rows, not the table heading --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -194,6 +194,39 @@ const PHASE_REQ_RANGE_RE = /^(.+-)(\d+)\.\.(.+-)(\d+)$/;
|
||||
*/
|
||||
const MAX_PHASE_REQ_RANGE = 1000;
|
||||
|
||||
/**
|
||||
* Shape filter applied to `--phase-req-ids` tokens AFTER range expansion
|
||||
* (#3189). ROADMAP `**Requirements:**` lines routinely carry prose trailing
|
||||
* the real ID list — locked-decision annotations, ambiguity scores,
|
||||
* prohibitions, dates — and the function's own contract says callers may pass
|
||||
* that roadmap value through verbatim. Without a shape filter every prose word
|
||||
* survives the whitespace split and is reported as an individually-missing
|
||||
* requirement, drowning the real coverage signal.
|
||||
*
|
||||
* The filter is intentionally wider than `ID_PATTERN` (which mandates a hyphen
|
||||
* and so would reject the hyphen-less `R1`..`R8` family real roadmaps use).
|
||||
* Three load-bearing details (#3189):
|
||||
*
|
||||
* 1. The digit lookahead `(?=.*\d)` matters. Without it ALL-CAPS prose tokens
|
||||
* that appear in annotations (`LOCKED`, `TBD`-as-prose, `NONE`-as-prose)
|
||||
* would pass and still reach the report as fake IDs. Every real
|
||||
* requirement ID carries a digit (`R1`, `SEL-01`, `P1-P3`).
|
||||
* 2. It MUST run after `expandPhaseReqIdToken`. Filtering before would
|
||||
* discard the range tokens themselves (`SEL-01..SEL-03` matches no
|
||||
* single-ID shape), silently undoing the #1269 / #1419 range expansion.
|
||||
* 3. It is NOT reusable as `ID_PATTERN`. `ID_PATTERN` mandates a hyphen,
|
||||
* so it rejects `R1`..`R8` and would leave only range-shaped tokens
|
||||
* standing — a silently-empty report, which is worse.
|
||||
*
|
||||
* Accepts both hyphen-less digit-bearing IDs (`R1`, `R8`) and prefix-hyphen
|
||||
* IDs (`REQ-01`, `SEL-01`, `BACK-07`), including multi-segment prefixes
|
||||
* (`REQ2-01`). Rejects prose (`LOCKED`), punctuation (`—`, `##`, `+`),
|
||||
* dates, version-likes (`0.12;`), backtick fragments, and any token still
|
||||
* containing `.` (so invalid range tokens returned literal by
|
||||
* `expandPhaseReqIdToken` are dropped rather than surfaced as fake IDs).
|
||||
*/
|
||||
const PHASE_REQ_ID_SHAPE_RE = /^(?=.*\d)[A-Z][A-Z0-9]*(?:[-_][A-Za-z0-9]+)*$/;
|
||||
|
||||
/**
|
||||
* Expand a single `--phase-req-ids` token in place. If it is a valid ascending
|
||||
* same-prefix numeric range (`<PREFIX>-NN..<PREFIX>-MM`, identical prefix both
|
||||
@@ -250,6 +283,15 @@ function expandPhaseReqIdToken(token: string): string[] {
|
||||
* spanning more than MAX_PHASE_REQ_RANGE IDs) stays literal — no partial
|
||||
* expansion, no guessing.
|
||||
*
|
||||
* ID-shape filter (#3189): STRICTLY AFTER range expansion, every token is
|
||||
* filtered through `PHASE_REQ_ID_SHAPE_RE`. ROADMAP `**Requirements:**` lines
|
||||
* routinely carry prose trailing the real ID list (locked-decision annotations,
|
||||
* ambiguity scores, prohibitions, dates); without the filter every prose word
|
||||
* would be reported as an individually-missing requirement. Tokens that cannot
|
||||
* be requirement IDs (prose, punctuation, dates, invalid range tokens left
|
||||
* literal by `expandPhaseReqIdToken`) are dropped; an input whose every token
|
||||
* is dropped collapses to `null` (skip semantics).
|
||||
*
|
||||
* Tolerates JSON-array-ish input (`["REQ-01","REQ-02"]`) since callers may pass
|
||||
* the roadmap value through verbatim.
|
||||
*/
|
||||
@@ -264,7 +306,12 @@ function normalizePhaseReqIds(rawVal: unknown): string[] | null | undefined {
|
||||
const ids = v.split(/[\s,]+/).map(s => s.trim()).filter(Boolean);
|
||||
// Expand range tokens (#1269) per-token AFTER the split, preserving input order.
|
||||
const expanded = ids.flatMap(expandPhaseReqIdToken);
|
||||
return expanded.length === 0 ? null : expanded;
|
||||
// #3189: drop tokens that cannot be requirement IDs (prose, punctuation,
|
||||
// dates, invalid range tokens left literal by expandPhaseReqIdToken). MUST
|
||||
// run after expand so valid ranges still expand (filtering before would
|
||||
// discard `SEL-01..SEL-03` itself, undoing #1269 / #1419).
|
||||
const filtered = expanded.filter(t => PHASE_REQ_ID_SHAPE_RE.test(t));
|
||||
return filtered.length === 0 ? null : filtered;
|
||||
}
|
||||
|
||||
function runGapAnalysis(cwd: string, phaseDir: string, options: RunGapAnalysisOptions = {}): GapResult {
|
||||
|
||||
Reference in New Issue
Block a user