Skip to content

feat(#9): Phase 5 — baseline & stale detection - #10

Merged
larspohlmann merged 16 commits into
developfrom
feature/9-baseline
Aug 11, 2026
Merged

larspohlmann merged 16 commits into
developfrom
feature/9-baseline

Conversation

@larspohlmann

Copy link
Copy Markdown
Owner

Closes #9

Implements Phase 5: refactor-stable finding baselines and stale-entry detection.

What's new

  • --generate-baseline <file> — writes current findings (errors + warnings) to a baseline document. Maintenance mode: exits 0 regardless of findings.
  • --baseline <file> — ignores findings recorded in the baseline; a baselined finding is invisible to every reporter and the exit code. Generation wins if both flags are set.
  • --fail-on-stale — exit 1 when a baseline entry or suppression matched nothing (full runs only; skipped under --changed-only, where the diff filter would make almost everything look stale).
  • Refactor-stable fingerprints — Fingerprint hashes only the semantic identity (origin, param, terminal token), never chain shape or source locations, so shortening a chain, moving a file, or shifting a line does not re-open a baselined finding. generate() round-trips byte-stably on an unchanged tree.
  • Observable suppression — the drop decision that ChainTraversal used to make silently is extracted into SuppressionFilter, which returns a SuppressionOutcome recording every suppression key that fired. That's what makes stale-suppression reporting possible.

Design notes

  • Baseline is stored as a hash => line map: membership is O(1), and staleness is entries − live.
  • Baseline vs. suppression stale detection is unified at the reporting layer in StaleReporter, kept separate from Application to stay under the PHPMD complexity ceiling.
  • Strict parse throughout — an unknown key or wrong-typed value throws, never silently un-gating CI (same philosophy as ConfigLoader).

Verification

  • composer check green (PSR-12, PHPStan level max, PHPMD codesize, 374 tests / 1245 assertions).
  • composer infection:diff — MSI 94% (gate is 80).
  • End-to-end fixtures cover the known-findings-filtered and stale-entry paths.

Docs (README.md, docs/ci.md, docs/plan.md, docs/plans/phase-5.md) updated for baseline usage and Phase 5 status.

Baseline against the shipped Phase 4 interfaces. Two refinements of the
master sketch, both recorded as normative: the fingerprint's terminal
token is the terminal FQMN or the TerminalKind value, not a per-reason
category (coarser survives resolver improvements without re-opening
grandfathered findings), and suppression moves out of ChainTraversal
into a fired-tracking SuppressionFilter because stale-ignore detection
needs to know what fired. Stale detection is skipped under
--changed-only, where almost every entry would look stale.
Two new fixture cases exercising the baseline flow end-to-end through
the existing folder-mode harness:

- baseline-filters-known: two distinct chains; a committed baseline.json
  covers exactly one (the Controller->ServiceA->Mailer chain); the
  harness confirms the other chain is reported while the baselined one
  is invisible to stdout JSON, the exit code, and the stale reporter.

- baseline-stale-entry: a baseline containing only a fabricated 40-zero
  hash that matches nothing; the run's single chain is reported
  unchanged on stdout, and the stale line is written to stderr only —
  proving the harness (which compares stdout JSON) is unaffected by
  stale reporting.
- README: status line bumped to Phase 5; new Baseline section with the
  adoption story (generate -> commit -> gate -> shrink), the
  refactor-stable fingerprint semantics, --fail-on-stale, and the
  --changed-only interaction note (stale detection skipped under
  --changed-only — full runs only). Dropped the 'reserved for Phase 5'
  wording from the two baseline flag rows and the config 'baseline' key
  row; added --fail-on-stale to the CLI listing.
- docs/ci.md: legacy-adoption recipes — a full-scan baseline-gated job
  with --fail-on-stale, and a diff-aware PR job composing
  --changed-only --baseline (no --fail-on-stale there: stale detection
  is skipped under --changed-only).
- docs/plan.md: frozen rule 10 records the fingerprint-token refinement
  made normative this phase (terminal FQMN when present, else the
  TerminalKind backing value, not a per-reason category); Phase 5 marked
  complete with 5.1/5.2/5.3 checked off, and 5.1's superseded
  'truncated:<reason-category>' sketch rewritten to match the
  refinement.
- Application help text: --fail-on-stale added to the Baseline section;
  Status line now mentions Phase 5 baseline shipped. None of the three
  baseline flags carry 'reserved' wording.
Baseline held two index-aligned lists (fingerprint hashes + raw entry
lines) and probed them with in_array, so has() and staleEntries() were
O(findings × entries) linear scans and carried a "keep these two arrays
in step" invariant. Collapse them into one hash => line map: membership
is now an O(1) isset, staleEntries builds a live-hash set once, and the
parallel-array coupling is gone.

generate() reuses Report\JsonEncoder instead of hand-rolling the same
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES pair, picking up its
encode-failure guard — the JSON reporters already call it from the same
uncaught path.
matchedKeysFor kept its own $seen map to dedup keys across a finding's
hops, but filter() already dedups every key against a run-global $seen
before recording it — the inner pass was dead work duplicating the outer
loop. Remove it; the returned list feeds only the emptiness test and that
global dedup, neither of which duplicates affect.

Add a test pinning the invariant this now leans on: two chains through
one suppressed method fire its key exactly once (the existing dedup test
used two distinct keys, so never exercised a repeated key).
@larspohlmann
larspohlmann merged commit 2d77c23 into develop Aug 11, 2026
5 checks passed
@larspohlmann
larspohlmann deleted the feature/9-baseline branch August 11, 2026 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 5 — Baseline: fingerprints, --generate-baseline/--baseline, stale detection

1 participant