Skip to content

quality: re-runnable Steps-view coverage scan (BACKLOG #239) - #81

Merged
wshallwshall merged 4 commits into
mainfrom
lens-coverage-scan
Jul 30, 2026
Merged

quality: re-runnable Steps-view coverage scan (BACKLOG #239)#81
wshallwshall merged 4 commits into
mainfrom
lens-coverage-scan

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Adds scripts/quality/lens_coverage.py and reports its first result.

ADR 0089 §5 promised the coverage scan was repeatable — "re-running it after each phase measures the coverage lift and surfaces the shrinking residual" — and then nobody re-ran it after Phase A, the ADR 0106 palette, ADR 0108 fan-out and ADR 0104 picker all shipped. This makes re-running it a one-liner.

python scripts/quality/lens_coverage.py <config-dir> --python .venv/Scripts/python.exe

Design choice worth reviewing

It drives the shipped lens parse --json instead of re-implementing the grammar with a second ast walk. The lens is the surface under evaluation; asking it directly means the number can't drift from what the Steps view actually renders. The cost is one subprocess per module, which is irrelevant at estate scale.

PHI: lens parse never imports or executes a config module and no message enters the path, so this runs against a production estate unmodified. --hash-names for when results leave the building.

First result — samples/config

14 files · 12 handlers · 28 rows · 0 parse refusals

Row kind Count Share
code 12 42.9%
send 12 42.9%
control 3 10.7%
control (unrecognized) 1 3.6%

OPAQUE 57.1% (46.4% under --strict-control) · EDITABLE 42.9%
handlers with zero editable rows: 0/12 · handlers 100% typed: 5/12 · median code rows/handler: 1 (max 4)

This does not answer #239, and the reason is the interesting part

samples/config contains zero action rows and zero lookup rows. Every editable row in it is a send. ADR 0089 §4 puts the pre-Phase-A baseline at "~13% (sends only)" — so this corpus sits at exactly that baseline shape and cannot demonstrate Phase A's lift at all.

The cause: the demo handlers delegate their transform work to helper modules (_demo_oru_transforms.py, _pdf_mdm_transforms.py) that the lens does not descend into, so it renders as code. That is ADR 0089's Phase D helper-descent gap, reproduced in miniature — and it suggests the sample corpus is a poor advertisement for the palette as well as a poor measurement subject.

28 rows also cannot speak for an estate ADR 0089 measured at 3,852 statements. The decision-grade number needs the production estate, whose path is owner-confirmed per #239 and deliberately not recorded here.

Claimed via scripts/coord/claim.ps1 -Take 239.

🤖 Generated with Claude Code

@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Pre-registered decision rule (posted BEFORE the estate scan is run)

The estate number is about to be measured. Recording the rule first so it can't become a post-hoc justification in either direction. Owner delegated the threshold choice; this is mine.

Metrics (both on the default reading, where a recognized control row still counts opaque because stepsView.ts renders it read-only; --strict-control reported as a sensitivity check only):

  • A = editable shareaction/lookup/send as a share of all projected rows. This is ADR 0089's own lift metric: ~13% at baseline (sends only) → ~42% after Phase A → §4's projected 80–90% after A–D.
  • B = fully-typed handlers — share of handlers with zero opaque rows. This is ADR 0089 §1's owner goal restated: "Open any item in the estate and see everything in editable action rows."

Bands:

Band Condition Decision
🟢 Deepen A ≥ 55% and B ≥ 40% The projection reaches most of the estate. Build #235 / #236 / #237 as planned — richer forms on a surface that demonstrably works.
🟡 Breadth before depth A 30–55%, or B 15–40% The lens reaches a meaningful minority. ADR 0089 Phases B–D (recognition breadth, esp. helper descent) outrank #235#237: widening what is recognized beats improving the widgets on what already is.
🔴 Model is the problem A < 30%, or B < 15%, or median opaque rows per handler ≥ 3 Stop deepening. ADR 0076 §7's parked exit path becomes the live question and #235#237 defer.

Reported but not band-driving — the cause mix of the opaque mass, in particular the share attributable to delegating calls into helper modules. If helper descent dominates, ADR 0089 Phase D is the highest-leverage fix regardless of which band we land in, because it converts opaque rows without widening the grammar.

Corpus: the de-identified estate (375 config modules — 75 handlers, 75 routers, 167 connections). Note it measures a different population than ADR 0089's 486 msg-manipulating functions, so the comparison to ~66%/~42% is directional, not like-for-like.

Ledger note: the tool skips .venv, __pycache__, site-packages and dot-directories — a real estate keeps its own venv beside the config modules, and without that filter the scan spawns a CLI call per site-packages file and buries the estate's own numbers.

@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Result — de-identified estate, scored against the pre-registered rule

388 files · 145 handlers · 1,423 rows · 0 parse refusals

Row kind Count Share
code 522 36.7%
action 456 32.0%
control 271 19.0%
send 140 9.8%
control (unrecognized) 33 2.3%
lookup 1 0.1%

A (editable share) = 42.0% · B (fully-typed handlers) = 14.5% (21/145) · median opaque rows/handler = 3 (max 50) · handlers with zero editable rows: 5/145 (3.4%)

Rule verdict: 🔴 RED — but at the boundary on both triggers

Applying the rule as posted, two RED conditions fire independently:

  • B = 14.5% < 15% — fires by 0.5pp. One additional fully-typed handler (22/145 = 15.2%) would have cleared it.
  • median opaque rows/handler = 3 ≥ 3 — fires at exactly the threshold value.

Meanwhile A = 42.0% sits mid-AMBER (30–55%). So: RED is the honest verdict under the rule I committed to, and I'm not going to relitigate a threshold after seeing the data — but nobody should read this as a decisive RED. Had I written median ≥ 4, this would be AMBER. Both triggers are at their edges.

The diagnostic changes what RED means here

The rule declared the cause mix "reported but not band-driving." It is nonetheless the most decision-relevant output:

Why a code row is opaque Count Share of code rows
comment/blank only 146 28.0%
return 114 21.8%
assignment from call 114 21.8%
bare helper call 104 19.9%
other 36 6.9%
assignment (literal/expr) 8 1.5%
→ helper-descent candidates 218 41.8%

RED in my rule meant "the projection model is the problem." The diagnostic says otherwise. The opacity is overwhelmingly mechanical, not fundamental:

  1. 28% of opaque code rows contain nothing but comments and blank lines. They carry no logic at all. "Comment" is already an Add-palette item (ADR 0106) that emits # <text> — the lens just doesn't recognize one back as its own row kind. Reclassifying comment-only rows as a non-opaque note row mechanically moves opacity from 58.0% → 47.8% with no grammar widening whatsoever. This looks like the cheapest coverage win available anywhere in the Steps view.
  2. 41.8% is helper delegation — exactly ADR 0089's Phase D, and exactly what samples/config showed in miniature. Phase D converts it without widening the grammar either.
  3. 21.8% is return — worth a look, since ADR 0108 already recognizes return sends scaffold and return [] as Filter. Some of these 114 may be near-miss forms rather than genuine tail.

Together, comments + helper descent account for ~70% of the opaque code mass, and neither requires the exit path the RED band points at.

Recommendation

Take the AMBER prescription (breadth before depth) despite the RED trigger, on the strength of the diagnostic — and treat the two cheap conversions as the next work:

  1. Recognize comment-only rows — smallest change, ~10pp of opacity, no grammar risk.
  2. ADR 0089 Phase D (helper descent) — the single largest remaining block at 41.8%.
  3. Triage the 114 return rows before assuming they are irreducible tail.
  4. Defer docs: untrack the multisession plan record and two root handoffs (ADR 0160 Phase 1) #235/backlog: file #1062, check reads env values from cwd after validating them under --project-root #236/backlog: file #1063, setup-leak-gate.ps1 arms the checkout it is standing in #237 until the above lands, then re-run this scan. Richer parameter forms on 42% coverage is polish on the wrong surface; at ~70% it is the right one.

This is a recommendation, not a decision — the RED trigger is real and the call on whether the diagnostic overrides it is the owner's.

Comparison note: ADR 0089's ~66%-opaque baseline counted a different population (486 msg-manipulating functions vs 145 projected handlers here), so 66% → 58% is directional, not like-for-like.

@wshallwshall
wshallwshall enabled auto-merge (squash) July 30, 2026 19:36
@wshallwshall
wshallwshall force-pushed the lens-coverage-scan branch 2 times, most recently from 400227f to db99c98 Compare July 30, 2026 19:47
wshallwshall added a commit that referenced this pull request Jul 30, 2026
PR #80 appended eight Steps-view items to docs/BACKLOG.md without the
status banner every numbered item must carry, so
tests/test_backlog_status_check.py::test_the_real_backlog_satisfies_the_invariant
began failing on main itself (8 errors, first at line 6905). Because
GitHub tests each PR merged into main, every open PR inherited the
failure: #81, #74, #71, #66 and #60 were all blocked, three of them with
auto-merge armed and unable to fire.

Adds exactly one leading banner per item. Seven use the open/prioritized
form; #239 uses the partial form, because its measurement ran and is
recorded on PR #81 while the re-runnable scripts/quality/lens_coverage.py
is still unmerged -- the number is not yet reproducible from main.

The banners are deliberately minimal and do not re-litigate any item's
verdict. #234 in particular is left explicitly unsettled rather than
entrenched: it was filed as "revisit, not a bug" after the owner asked
for a fix, and that framing is still open.

Verified: scripts/docs/backlog_status_check.py exits 0 (237 items) and
tests/test_backlog_status_check.py is 15 passed, was 14 passed 1 failed.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
wshallwshall and others added 4 commits July 30, 2026 16:01
ADR 0089 §5 promised the coverage scan was repeatable, and then nobody re-ran
it after Phase A, the ADR 0106 palette, ADR 0108 fan-out and ADR 0104 picker
shipped. This makes re-running it a one-liner.

It drives the SHIPPED `lens parse --json` rather than re-implementing the
grammar with a second ast walk, so the number describes what the Steps view
actually renders and cannot drift from it.

PHI-safe by construction: `lens parse` never imports or executes a config
module and no message enters the path, so it runs against a production estate
unmodified. --hash-names for when results leave the building.

First result, samples/config (14 files, 12 handlers, 28 rows, 0 refusals):

    code                    42.9%      OPAQUE   57.1%  (46.4% --strict-control)
    send                    42.9%      EDITABLE 42.9%
    control                 10.7%
    control (unrecognized)   3.6%

    handlers with zero editable rows : 0/12
    handlers 100% typed              : 5/12 (41.7%)
    median code rows per handler     : 1   (max 4)

This does NOT answer #239, and the reason is worth recording: samples/config
contains ZERO action rows and ZERO lookup rows. Every editable row in it is a
`send`. ADR 0089 §4 puts the pre-Phase-A baseline at "~13% (sends only)" --
so this corpus sits at exactly that baseline shape and cannot demonstrate
Phase A's lift at all. The demo handlers delegate their transform work to
helper modules (_demo_oru_transforms.py, _pdf_mdm_transforms.py) which the
lens does not descend into, so it renders as `code`. That is ADR 0089's
Phase D helper-descent gap, reproduced in miniature.

28 rows also cannot speak for an estate ADR 0089 measured at 3,852
statements. The decision-grade number needs the production estate; its path
is owner-confirmed per #239 and deliberately not recorded here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two gaps found by running the scan against a real estate rather than samples/.

1. A real estate keeps its own .venv beside the config modules. A naive rglob
   walked ~1,900 site-packages files, spawned a CLI call per file, and buried
   the estate's own numbers. Dot-dirs, __pycache__, site-packages, node_modules,
   build and dist are now skipped.

2. The pre-registered decision rule (PR comment) referenced "median opaque rows
   per handler" and a cause mix, and the tool reported neither -- so it could
   not actually adjudicate its own rule. Both now reported.

The cause classifier is a deliberate HEURISTIC on first-statement shape, not a
parse. It exists to answer the one question the row contract cannot: how much
of the opaque mass is a delegating call into a helper module, i.e. how much
ADR 0089 Phase D would convert WITHOUT widening the grammar. Shapes only --
no source is ever emitted, only counts, so it stays estate-safe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ASVS 11.1.3 crypto-inventory gate failed this branch: --hash-names used
hashlib.sha256 to redact file names, and hashlib is a tracked crypto module
with no inventory entry for a dev script.

Adding an inventory entry would have been the wrong fix, because the hash was
also the wrong PRIMITIVE. Estate modules follow a rigid naming convention
(IB_<id>_<system>_<type>_handler.py), so a candidate list is cheap to generate
and digest -- a truncated SHA-256 of a file name is reversible by dictionary
attack. The flag is now --anonymize and assigns stable per-run opaque indices
(<module-0001>.py), which carry no preimage at all.

More private, and no crypto import in a quality script. Gate now reports
"OK - 57 documented crypto call site(s), no drift."

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… override

The item's banner said the tooling was unmerged; this PR merges it. Flips
it to shipped and carries the numbers the scan produced, so the backlog
states the result rather than pointing at a PR comment.

Also records, in the item itself, that the pre-registered decision rule
fired RED and that the RED prescription was NOT adopted. Both triggers
landed exactly on their boundaries and the AMBER prescription was taken
instead on a delegated judgment call that the owner never explicitly
ratified. That override is load-bearing for the next several items, so it
belongs on the record next to the number rather than only in a handoff.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wshallwshall
wshallwshall merged commit 5d93cfb into main Jul 30, 2026
32 checks passed
@wshallwshall
wshallwshall deleted the lens-coverage-scan branch July 30, 2026 21:23
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.

1 participant