Skip to content

#242: the no-copy guard must not publish what it forbids — and then, delete most of the guard - #252

Merged
Polichinel merged 5 commits into
developmentfrom
fix/242-no-copy-scan-must-not-print
Aug 12, 2026
Merged

Polichinel merged 5 commits into
developmentfrom
fix/242-no-copy-scan-must-not-print

Conversation

@Polichinel

Copy link
Copy Markdown
Collaborator

Closes #242. First story of epic #241.

The defect (C-89)

The no-copy scan had two reporting branches. One was taught not to print the coordinate value; the other was not. So on the single event the guard exists to catch, it published a coordinate into a world-readable CI log. Public repo, unredactable logs.

Four iterations, and the fourth deletes most of the first three

tests/test_env_declaration.py
before 1145
after iteration 3 1340 (+195)
after iteration 4 1200 (+55)

Of the +195, +95 was prose restating the register, and all 149 lines of new function bodies were tests of another test in the same file. Meta tests went 37% → 45%.

Two of my own additions were defective, both caught by design review:

  • The drift-report test was a lie. It claimed to drive _describe_changes's appeared/removed branches. Those branch on was is None; the fixtures passed _ABSENT, a sentinel object. Never entered — one path run three times while asserting three. And those branches return constant strings and cannot leak. Deleted.
  • _carries was strictly worse than plain in. Its 8-char window degrades to exact match below 8 characters, and this package's two directory names are 5-character declared values. Measured: _carries("views_postprocessing/unfao/leaky.py …", "unfao") → True. It condemns a bare, safe file path. Deleted.

_report_a_copy also went — 2 lines of code under 14 of docstring, 11 explaining what it does not guarantee. Two inline f-strings now; WET-before-DRY, since the second incident did not have the first's shape.

The output guard survives at 19 lines instead of 78. ADR-014 §1 doesn't permit a guarantee with no check, and this regression already happened once silently.

The one real open leak, closed at both sites

Iteration 3 claimed a malformed package module could no longer publish itself because _dotenv_use takes a Path. Measured: that changes nothing — the leak is ast.parse's own frame, which pytest renders with the source as its argument. The fix is raise ... from None; from None is load-bearing because the chained SyntaxError carries the same text. Applied to both parse sites — the scan itself parses every package .py, and fixing one and not the other is C-89's shape verbatim.

Proven: a malformed module carrying a real value fails with does not parse (line 92) and the value appears zero times. It appeared before.

Mutations

mutation verdict
the original C-89 defect fails ✓
assertion widened to print the ban-set fails ✓
digest() returns the raw value fails ✓
malformed module carrying a value fails, publishes nothing ✓
a legitimate refactor passes ✓

Deliberately not chased

Four surviving mutations narrow the scan's scope — routed to #243, which owns scope. A fifth deletes the no-print assertion: infinite regress, carried in the register. Two residuals recorded: the markdown branch isn't behaviourally proven (it holds no value by construction), and a leak shorter than the fixture would pass.

ruff check .    # clean
pytest -q       # 410 passed, 1 skipped, 40 xfailed

The one failure is #248's — red since views-crafdapi refactored on 2026-08-12.

🤖 Generated with Claude Code

Polichinel and others added 5 commits August 12, 2026 12:38
…ecisions it forced

Registered before any code moves, so the reasoning survives the fixes.

FIVE NEW FROM /code-review max ON THE MERGED #239 (all reproduced here, none taken
on trust): C-89 the no-copy guard prints the value it forbids, in one branch of three
and proven on one side of two; C-90 the replacement drift checks assert less than
their docstrings say, and one mutation proof is a tautology; C-91 the git plumbing
turns ordinary developer states into hard errors with the wrong diagnosis; C-92 this
repository lost its only check that the FAO consumer still SELECTS by the delivery
label; C-93 a mutation proof written by whoever wrote the guard tests that author's
imagination — 13 forms proposed by the author, 13 caught; 29 proposed independently,
15 missed.

THREE MORE FROM THE EXPERT REVIEW OF THE STANDING DECISIONS: C-94 nothing anywhere
detects an invisible delivery at the time it happens — every mechanism aimed at it
is a CI-time proxy — deferred with a trigger and an owner rather than omitted;
C-95 the integration-test prohibition is cited to þing-02 D2 in three places when it
is þing-01 D2, and the real ruling is CONDITIONAL and GRANTS read-only preflight;
C-96 the registry table carrying that permission is classified "not about this
package".

C-95 and C-96 compound into the finding that matters most for planning: a whole class
of mechanism was believed closed for weeks because a citation pointed at the wrong
verdict and the table stating the permission was declared none of our business.

AMENDED: C-57 (my own amendment overclaimed — it cited a backticked example as proof
the mid-sentence class was covered; it passes only because of the backticks, and a
fourth failure direction was found: removing the length floor admitted this repo's own
package names into a security guard's ban-set); C-86 (its partial-mitigation paragraph
overstates what shipped — one of the three replacement checks does not match on the
facts this repository declares); C-87 (one partner's residual got worse, not better).

Header 93/20/73 -> 96/23/73. Epic #241, tracking #251.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
C-89's first two thirds. The guard that exists to stop a coordinate value being
published was publishing one, into a world-readable CI log, on precisely the event it
exists to catch.

THE VALUE IS NO LONGER A PARAMETER. Both branches build their finding through
`_report_a_copy(where, coordinates)`, which is never handed the value — so a future call
site cannot print one however it is written. That is the difference between a rule and a
design: the markdown branch was taught not to print on 2026-08-11, the AST branch was
not, and the two drifted for a day because each read fine on its own.

It names ALL coordinates sharing the value, not one. Measured against the live registry:
two pairs share a value (the prod-forecasts bucket and collection share both their id and
their name), so a `value -> name` dict would keep the last and name the wrong coordinate
half the time — in the one message a maintainer uses to find the copy.

THE ROTATION PROOF CHECKED ONE SIDE OF TWO. It asserted the pinned value was absent and
said nothing about the freshly rotated one, which is the more damaging half. Mutation-
proven: leaking the post-rotation side while keeping the pinned side digested fails now
and passed before.

AND THE RULE NOW HAS A CHECK, which is the part that matters. `test_every_finding_goes_
through_the_one_reporter` asserts by AST that every `copied.append` in the scan calls the
one reporter. Nothing compared the two branches, which is why prose saying "never print a
value" coexisted with a branch printing one. Read as source rather than run, because the
leaking branch only executes when a coordinate has actually been copied — a state this
repository must never be in, so a behavioural test of it would have nothing to observe.

Three mutations, each reverted: (1) restore the original AST f-string -> the new guard
fails, naming the line; (2) leak the post-rotation side -> the rotation proof fails;
(3) inject a real value declared by TWO coordinates into a package module -> caught, both
coordinates named, no value in the message.

Deliberately NOT in this change: the `secret` exemption and the ban-set's package-name
collision. Those are the scan's SCOPE, not its reporting, and belong with the matcher
rewrite in #243. C-89 stays open rather than being closed on two thirds of its content.

Suite 411 passed / 1 skipped / 40 xfailed, ruff clean. The one pre-existing failure
(`test_the_consumer_still_filters_on_the_name_until_it_checks_itself[crafd]`) is #248's,
red since views-crafdapi refactored a literal into a named constant on 2026-08-12.

Closes #242. Epic #241.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sage instead

Five independent reviewers were run against the first attempt. They agreed, and they
were right: the guard I wrote was worse than nothing.

IT MISSED SIX OF EIGHT MUTATIONS AND FIRED ON A LEGITIMATE REFACTOR. It asserted that
every finding was ROUTED through `_report_a_copy` — an AST walk over the scan's own
source. But routing is not safety. The value could be smuggled through either of that
helper's two parameters, appended with `extend` or `+=`, or reported from a renamed
accumulator, and in every case the guard stayed green while a coordinate reached the
log. Meanwhile `finding = _report_a_copy(...); copied.append(finding)` — an ordinary
refactor that leaks nothing — failed it. A guard that misses the thing and cries wolf
on the innocent is C-82's shape and ADR-014 §3's deletion criterion at once, and my
docstring's claim that a caller "cannot print one however it is written" was false on
the day I wrote it.

SO READ THE FINISHED MESSAGE, NOT THE SYNTAX THAT PRODUCED IT. `test_the_scan_reports_
a_copy_without_reprinting_it` plants a value in a fixture tree, runs the real scan
through both halves, and asserts the message names the coordinate and never the value.
It does not matter how a finding is built or what the final assertion interpolates. All
five surviving mutations are now caught — including one that leaked the entire ban-set
through the assertion message rather than through any finding.

That is also the answer to C-93: not a longer list of mutants I thought of, but a test
whose shape makes the class unreachable.

AND THE SCAN'S OWN DOCSTRING CARRIED THREE REGISTRY VALUES. pytest prints the failing
function's source, so the guard would have published them on precisely the event it
exists to catch — message clean, traceback not. Now it names coordinates. The wider
measurement is registered as C-97: 33 standalone values in docstrings and comments
across nine files, production modules included, excluded from the scan by a deliberate
C-57 decision taken before anyone counted them. Three of them cannot be told from
ordinary code at all — two are this repo's package names, one is a function name.

Also fixed, both found by review and both real:
- the markdown half named the coordinate the ASSIGNMENT names, not the one that
  DECLARES the value. Those differ exactly when a document pastes one coordinate's
  value beside another's name — the ordinary copy-paste slip — so it asserted a false
  fact and hid the coordinate actually leaked.
- the rotation loop hardcoded the fixture's two literals a dozen lines from where the
  fixture builds them. Renaming the fixture strings turned the assertion into "two dead
  strings are absent". Now taken from the fixture's own variables.

Register: C-89 amended with what was tried and what failed; C-97 opened; header 96/23
-> 97/24. The earlier "Mitigated" heading corrected to "Partial mitigation", which is
one of the two phrasings this register declares — inventing a third passed the
integrity guard by evading its string rather than complying, the identical escape
ADR-014 §5 records C-15 making. C-89's Location line numbers repointed; the fix had
moved all three and the amendment cited the old ones.

The file is +112 lines against `development`, which is the wrong direction for this
epic. This story is inherently additive — it adds a guarantee's check where there was
none. #245 and #248 are deletions and are where the budget comes back.

Suite 411 (410 passed / 1 skipped / 40 xfailed), ruff clean. The one failure is #248's,
red since views-crafdapi refactored on 2026-08-12.

Closes #242. Epic #241.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An adversarial mutation review ran 14 mutations against iteration 2. Twelve survived.
Seven were leak channels; all seven are now closed. Five are refused in writing.

A MESSAGE TEST ONLY SEES THE MESSAGE. `print(node.value)` beside the append, or a
`logging.warning`, left the assertion text clean while pytest published the value under
Captured stdout and Captured log call. The behavioural test now reads capsys and caplog
as well. A stray debug print is an ordinary accident, not an adversarial one.

SUBSTRING ABSENCE IS NOT ABSENCE. A finding carrying `value[:12]` or `value[::-1]`
walked straight past `planted not in message`, and a reader recovers both instantly.
Replaced by `_carries`: does any eight-character window, forwards or backwards, appear.

AND THE DRIFT REPORTER HAD NO TEST AT ALL. `_describe_changes` is exercised only by the
rotation proof, through one shape, with fixture values SHORTER THAN EVERY REAL
COORDINATE — so a digest leaking only above a length threshold passed, as did a
reversing digest, as did the appeared/removed branches a both-sides fixture never
reaches. Three leaks, all green. It now has its own test, driven through all three
shapes with an over-long opaque value.

A HELPER'S PARAMETER IS A PUBLICATION CHANNEL. `_dotenv_use` took file text; pytest
renders a frame's arguments and `ast.parse` raises on any malformed module, so a package
file that both failed to parse and carried a value would have printed itself in full.
It takes a Path now and reads inside.

And the fixture values are opaque now because a readable one bit me: the first planted
string contained the word "coordinate", which also appears in the check's own failure
prose, so the window test fired on the test's own English. My cry-wolf, caught by my own
guard, fixed in the fixture rather than by weakening the check.

WHAT I AM NOT CHASING, and why it is written down rather than left to be rediscovered.
Five mutations survive. Four narrow the scan's SCOPE — a length floor, a dropped
section, a swallowed SyntaxError, a `break` after the first finding — and none is
visible to a test that plants its own fixture, because the fixture satisfies whatever
narrowing is applied. They are #243's subject and are routed there with reproductions;
fixing them here would be doing that story badly and twice. The fifth deletes the
no-print assertion itself, which is infinite regress: a guard on a guard on a guard has
no fixed point, and the register carries that risk instead. The eight-character window
is a judgement, not a law — a six-character leak would pass, and chasing it is the
whack-a-mole this epic exists to refuse.

Register: C-89 amended with all three passes, including what was tried and failed.

Suite 412 (411 passed / 1 skipped / 40 xfailed), ruff clean. The one failure is #248's.

Closes #242. Epic #241.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The maintainer's verdict on iterations 1-3: "so complicated and heavy with technical
debt that you are not even able to maintain it. Then no one can." He is right, and the
measurement is worse than the impression. Four commits had grown one file by +195 lines
(1145 -> 1340, +17%). +95 of that was PROSE restating the risk register. All 149 lines
of new function bodies were tests of another test in the same file. Meta tests went from
37% to 45% of the file.

This commit deletes 140 of those lines. Every one of the four outstanding review
findings is resolved by subtraction.

TWO OF MY OWN ADDITIONS WERE DEFECTIVE, AND A DESIGN REVIEW CAUGHT BOTH.

`test_the_drift_report_never_carries_a_value_in_any_recoverable_form` claimed to drive
`_describe_changes`'s appeared/removed branches. It did not. Those branch on
`was is None`; my fixtures passed `_ABSENT`, a sentinel OBJECT. The branch is never
entered — it ran one path three times while asserting three, and the register repeated
the claim. That is an ADR-014 §1 defect inside the fix for an ADR-014 §1 defect. The
branches it named return constant strings and cannot leak at all. DELETED.

`_carries`, my eight-character-window "recoverability" predicate, is STRICTLY WORSE THAN
PLAIN `in`. The window degrades to exact match below eight characters, and this package's
two directory names are five-character declared values. Measured:
`_carries("views_postprocessing/unfao/leaky.py …", "unfao")` is True — it condemns a
bare, safe file path while screaming that the scan published a value. Simultaneously
stricter than `in` on opaque fixtures and identical to it on the values that matter.
DELETED; both sites use plain `not in`, which is the vocabulary this file already uses in
`test_the_environment_refusal_logs_names_and_never_values`.

ALSO DELETED: `_report_a_copy` — 2 lines of code under 14 lines of docstring, 11 of them
explaining what it does NOT guarantee. Its safety claim was falsified in iteration 2 and
it survived anyway. Two inline f-strings now, which is what WET-before-DRY prescribes:
the second incident did not have the same shape as the first, and the helper's signature
forced the markdown branch into a detour it had to justify with a six-line comment. The
behavioural guard is what holds the rule, not the formatter.

The output guard SURVIVES, at 19 lines instead of 78. ADR-014 §1 does not permit a
guarantee with no check, and this regression already happened once, silently. But it no
longer monkeypatches four globals, shells out to `git init`, or guards a `print` nobody
has written.

AND THE ONE REAL OPEN LEAK IS NOW CLOSED, AT BOTH SITES. Iteration 3 claimed a malformed
package module could no longer publish itself because `_dotenv_use` takes a Path.
Measured: that changes nothing — the leak is `ast.parse`'s own frame, which pytest
renders with the source as its argument, and `path.read_text()` still hands it there. The
fix is `raise ... from None`, where `from None` is load-bearing because the chained
SyntaxError carries the same text. Applied to BOTH parse sites; the scan itself parses
every package .py, and fixing one and not the other is C-89's shape verbatim.
Proven: a malformed module carrying a real value now fails with "does not parse (line
92)" and the value appears ZERO times, where it appeared before.

Five mutations re-proven: the original C-89 defect fails; a widened assertion fails; a
raw digest fails; a malformed module fails without publishing itself; a legitimate
refactor passes.

Register: C-89's Location is now FUNCTION NAMES — it has cited stale line numbers twice,
because each fix moved them. The false "it takes a Path now" sentence is corrected. The
narration of both deleted additions is removed, including one claim that was never true.
Two deferrals get triggers: consolidating into tests/test_redaction_guard.py when #243
finishes with this file, and a pre-existing proof that re-types its subject inline.

tests/test_env_declaration.py: 1340 -> 1200 (+55 over the pre-story 1145, not +195).
Suite 410 passed / 1 skipped / 40 xfailed, ruff clean. The one failure is #248's.

Closes #242. Epic #241.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Polichinel
Polichinel merged commit 3e88305 into development Aug 12, 2026
4 checks passed
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