ADR implementation H2 partial matcher + PR table header mis-detection block
Goal
- The blog ADR detail-view visualizes the implementation section's PR table as PhaseStrip cards (Sprint 163). However,
resolveCanonicalhandled implementation only via exact alias matching, so headings with parenthetical/suffix content like## 구현 (8 PR squash merge, ...)/## Implementation (single PR, ...)were classified as'other', and the PR tables of sprint-153~165 (KR + EN) didn't render as Phase cards — this recovers that gap (Sprint 163 carryover follow-up). - After starting, browser verification surfaced a deeper pre-existing vulnerability:
parsePrTable's header detection mistook a data row containing the string "PR" for the header → applying the partial matcher made sprint-163 render garbage Phase cards. This is blocked together.
Decision
D1. Add an implementation tolerant-regex fallback to resolveCanonical
resolveCanonical (section-aliases.ts) does exact alias lookup after stripping a numbered prefix → only carryover/lessons had tolerant-regex fallbacks (CARRYOVER_RE/LESSONS_RE), and implementation had no tolerant matcher. So 구현 matched exactly, but 구현 (8 PR ...) fell through to 'other'.
Added IMPLEMENTATION_RE = /^(?:구현|implementation|execution)(?:\s|$|\()/i and placed it as a fallback after carryover/lessons.
- Matches only when the keyword is immediately followed by whitespace/end/
(→구현체(non-whitespace continuation) does not match (conservative). - Exhaustive FP measurement: across the whole corpus (KR sprints + EN sprints + permanent ADRs), all 66 old→new canonical changes were
other → implementation— zero changes that steal an existing correct classification (carryover/lessons/verification etc.). - Phase card generation is separately gated by a downstream guard (
extractPhaseEntries) requiring a PR table + Phase column, so sections without a PR table (구현 작업,구현 예정 위치, etc.) produce no bogus cards even when classified as implementation.
D2. Confine parsePrTable/stripPrTableLines header detection to "the line before the separator"
parsePrTable's (parser.ts) header detection used lines.findIndex(l => /\|/ && /pr/i), treating any line containing "PR" as the header. sprint-163's implementation table has the header | Phase | 담당 | 변경 | 라인 | (no PR column), but the data row | A — PR 표 strip 기반 | ... | contains "PR" → it was mistaken for the header, consuming A as header and B as separator, extracting C~R9 as garbage cards, and failing to strip the table.
Using the invariant that a GFM table header is always the line immediately before the separator (|---|), the header detection now requires "the next line is a separator" (isTableSeparatorRow helper). This makes data-row mis-detection structurally impossible. stripPrTableLines, which had the same vulnerability, was aligned to the same rule (removed its separate separator-validation branch, sharing the helper).
A table without a PR column (sprint-163) fails header detection → returns undefined → gracefully kept as a raw table in prose (same as before the change, no regression).
Implementation
PR #321 (single work branch fix/sprint-184-impl-h2-partial-matcher, 1 commit → squash, 2 source files +36/-10)
5640326fix —IMPLEMENTATION_RE+ 1-line resolveCanonical fallback in section-aliases.ts;isTableSeparatorRowhelper + parsePrTable/stripPrTableLines header detection hardening in parser.ts.
Core change (section-aliases.ts):
const IMPLEMENTATION_RE = /^(?:구현|implementation|execution)(?:\s|$|\()/i;
// resolveCanonical fallback:
if (IMPLEMENTATION_RE.test(stripped)) return 'implementation';
Core change (parser.ts):
const headerIdx = lines.findIndex(
(l, i) =>
/\|/.test(l) &&
/pr|pull\s*request/i.test(l) &&
i + 1 < lines.length &&
isTableSeparatorRow(lines[i + 1]),
);
Critic cycle
codex review --base main, 1 round.
- R1: 0 issues, passed — "The changes tighten PR table detection to require a following GFM separator and extend implementation heading aliasing without introducing an evident regression. No actionable correctness issues were found in the diff." Mergeable.
Verification
Browser end-to-end (blog build → static server → actual DOM check)
- sprint-157 (KR):
구현 PhasePhaseStrip renders 10 cards (P1~UX addition, with PR links/owner/summary) + the implementation section's raw PR table stripped (only the Mermaid작업 흐름diagram remains) ✓ - sprint-157 (EN):
Implementation Phases10 cards + table stripped ✓ - sprint-163: garbage cards (A/B missing, C~R9 mis-extracted) fully removed → no
구현 Phasesection generated, raw table kept gracefully ✓
Document-level simulation (old vs new)
- 11 KR sprints (153/154/155/156/157/158/159/160/161/164/165) + 11 EN sprints = 22 ADRs newly gain Phase cards (0→N).
- Zero regressions across the whole corpus (no N→0/N→M changes), permanent ADRs unaffected (PR-column-less
구현 작업etc. are graceful).
Local
tsc --noEmit0 errors ·npm run build244 pages.- ADR/blog gates 7, no regression: adr-conversion (fixtures 10/10) · doc-refs (329 files 0 broken) · en-coverage (131/131) · index-count (8/1/122) · i18n (max 2.19%<8%) · blog-crosscheck (0 violations) · adr-links (KR 1616 / EN 1614, 0 broken).
CI
- Work PR #321 all checks green (including Build Blog — triggered by blog source change), mergeStateStatus CLEAN. The ADR PR is green via the
sprints/**trigger.
Result
- Merge: origin/main →
c38a54f(PR #321 squash merge, work branch deleted). - Net change:
blog/src/lib/adr/section-aliases.ts(+16) +blog/src/lib/adr/parser.ts(+20/-10). No new files. - ADR sprint-184 (KR+EN) + README sprint ADR count 122→123, range 62~184 (separate ADR PR).
New patterns
- A plan whose scope is redefined by browser verification: started as a single "implementation H2 partial matcher" change, but the browser DOM check of the first build found that the partial matcher exposed sprint-163's pre-existing
parsePrTablevulnerability (data-row header mis-detection), producing garbage cards → addedparsePrTablehardening to scope. The static simulation ("a table with a PR column should render") differed from the actual render (garbage cards), and the actual DOM was the arbiter (inheriting Sprint 183's "browser is the arbiter"). - Block heuristic mis-detection with a structural invariant: the "any line containing PR = header" heuristic mis-identified a data row. Folding GFM's "the header is the line before the separator" invariant into the detection condition makes mis-detection "impossible by construction" rather than "luck" (inheriting Sprint 183's "confine the input with an exact pattern").