Re-scoped 2026-08-12. #244 is merged into this issue. The first draft's acceptance criterion was "catch 29 forms"; that is the whack-a-mole this epic exists to stop. This story makes the scanner smaller and gives it a stopping rule.
Part of #241, Gate 2 — does not block PR #240. Registers C-57, C-89. Depends on #242.
Problem
The markdown half of the no-copy scan parses an assignment out of free-form prose and captures the value, then compares the capture against the registry. It has been widened four times and is still blind, and it always will be — capturing "the value" from prose is not a solvable problem, because registry values contain spaces:
NAME=Production Forecasts # note must capture two words
set NAME=production_forecasts before running must not capture three
Any terminator that fixes one breaks the other. Measured: the author proposed 13 forms and proved 13 caught; an independent review proposed 29 and 15 missed.
The scanner already knows both halves before it starts. It has the coordinate name and it has the registry value. Parsing is the wrong tool.
Work — this is a simplification, not a widening
1. Match the pair, do not parse the line. Per (name, value), with re.escape:
(?<![A-Za-z0-9_])NAME\s*=\s*["'`]?VALUE["'`]?(?![A-Za-z0-9_])
This deletes, in one move: the terminator problem, the six-character lookbehind allow-list, the once-per-line search, the quoted/unquoted alternation, and the captured value that #242 had to stop printing. There is nothing to capture, so there is nothing to leak. Cost is 17 names × ~11k lines, which is nothing.
Expect the diff to be net negative. If it is not, the design is wrong — say so and stop.
2. The secret exemption goes (was #244). scanned_sections filters _TABLE_ROLE for CONSUMED and then subtracts secret by name literal, two lines under a comment saying the scope comes from the declared partition and not from an inline list, with no reason recorded anywhere. Measured: [secret] has 7 rows, none carries a value, so the clause removes zero entries today — and the isinstance(..., str) filter already does that job. Its only effect is a future hole in the one table class whose leak matters most.
3. Exclude the package-name collisions by declaration (was #244). The registry's two shortest values are this repo's own package directory names — the exact contents of PARTNER_PACKAGES — compared by exact string equality against every non-docstring string constant under views_postprocessing/. The next logging.getLogger(<partner>) fails a security guard for code that copied nothing, and a guard that cries wolf gets deleted (ADR-014 §3). Exclude them by name with the reason written down, not by a length threshold that excluded them as a side effect. State the hole plainly: a real copy of a value that equals a package name is not caught by the AST branch.
Stopping rule — the actual deliverable
Write this into the test file:
The scan is finished when, for every configuration-shaped line in this repository's own tracked documents (git ls-files '*.md'), the matcher either understands that line's form or the form is on a declared exempt list with a reason.
The form list is derived from the corpus, never from imagination. A form no document here uses is not a gap. When a new document introduces a new form, the form is added in the same change as the document — never speculatively, never because a review invented it.
Make that rule a test: enumerate candidate lines from the corpus, assert each is understood or exempt. It goes red exactly when someone writes a new form, which is the only moment widening is warranted.
The 29 forms from the review are a one-time seed, not a standard. Seed the corpus test with the forms this repo actually uses and move on.
Acceptance criteria
Labels
story, implementation, testing
Re-scoped 2026-08-12. #244 is merged into this issue. The first draft's acceptance criterion was "catch 29 forms"; that is the whack-a-mole this epic exists to stop. This story makes the scanner smaller and gives it a stopping rule.
Part of #241, Gate 2 — does not block PR #240. Registers C-57, C-89. Depends on #242.
Problem
The markdown half of the no-copy scan parses an assignment out of free-form prose and captures the value, then compares the capture against the registry. It has been widened four times and is still blind, and it always will be — capturing "the value" from prose is not a solvable problem, because registry values contain spaces:
NAME=Production Forecasts # notemust capture two wordsset NAME=production_forecasts before runningmust not capture threeAny terminator that fixes one breaks the other. Measured: the author proposed 13 forms and proved 13 caught; an independent review proposed 29 and 15 missed.
The scanner already knows both halves before it starts. It has the coordinate name and it has the registry value. Parsing is the wrong tool.
Work — this is a simplification, not a widening
1. Match the pair, do not parse the line. Per
(name, value), withre.escape:This deletes, in one move: the terminator problem, the six-character lookbehind allow-list, the once-per-line
search, the quoted/unquoted alternation, and the captured value that #242 had to stop printing. There is nothing to capture, so there is nothing to leak. Cost is 17 names × ~11k lines, which is nothing.Expect the diff to be net negative. If it is not, the design is wrong — say so and stop.
2. The
secretexemption goes (was #244).scanned_sectionsfilters_TABLE_ROLEforCONSUMEDand then subtractssecretby name literal, two lines under a comment saying the scope comes from the declared partition and not from an inline list, with no reason recorded anywhere. Measured:[secret]has 7 rows, none carries avalue, so the clause removes zero entries today — and theisinstance(..., str)filter already does that job. Its only effect is a future hole in the one table class whose leak matters most.3. Exclude the package-name collisions by declaration (was #244). The registry's two shortest values are this repo's own package directory names — the exact contents of
PARTNER_PACKAGES— compared by exact string equality against every non-docstring string constant underviews_postprocessing/. The nextlogging.getLogger(<partner>)fails a security guard for code that copied nothing, and a guard that cries wolf gets deleted (ADR-014 §3). Exclude them by name with the reason written down, not by a length threshold that excluded them as a side effect. State the hole plainly: a real copy of a value that equals a package name is not caught by the AST branch.Stopping rule — the actual deliverable
Write this into the test file:
Make that rule a test: enumerate candidate lines from the corpus, assert each is understood or exempt. It goes red exactly when someone writes a new form, which is the only moment widening is warranted.
The 29 forms from the review are a one-time seed, not a standard. Seed the corpus test with the forms this repo actually uses and move on.
Acceptance criteria
git diff --statfortests/test_env_declaration.pyis net negative for this story.[secret.*]row gaining avalueis scanned; mutation-proven.logging.getLogger(<a partner name>)in a package module does not fail; a real coordinate value does.Labels
story,implementation,testing