diff --git a/reports/technical_risk_register.md b/reports/technical_risk_register.md index 8071d55..f2ebe2d 100644 --- a/reports/technical_risk_register.md +++ b/reports/technical_risk_register.md @@ -1849,6 +1849,20 @@ The shared cause is not a regex bug. **The pattern described one way of writing **The residual, which is deliberate and should not be "fixed".** This remains a *syntax* match. A value merely named in a sentence — "the six stranded documents in ``" — is not a copy and does not fire. An earlier draft that matched any occurrence fired on a dozen documents, and the lesson recorded above applies to itself: a guard that cries wolf gets deleted, after which the real rule is unguarded (ADR-014 §3). The class this cannot catch is a value pasted into prose with no assignment syntax anywhere near it. That is accepted, because the alternative has been tried and was worse. +**Mitigation — landed 2026-08-12 (#243). The parse is gone; the scan matches the pair.** + +The scan no longer parses a line and compares what it captured. It knows both halves before it starts — the declared names and the declared values — so it matches `NAME = VALUE` directly, with `findall` rather than one match per line. Every blindness recorded above came from the parse: the swallowed comment, the missing terminator, the six-character lookbehind, the second assignment on a table row. None is expressible now. + +The two halves need not correspond: `APPWRITE_X=` is the ordinary copy-paste slip and is reported as a copy, naming the coordinate that *declares* the value rather than the one the line assigns. + +**The `secret` exemption is gone.** It was an inline name literal under a comment saying the scope came from the declared partition and not from an inline list. Measured, `[secret]` carries no values today, so removing it changes nothing now and closes the hole the day one gains a value. + +**The stopping rule is a test, not a paragraph.** `test_the_scan_understands_every_assignment_form_this_repo_writes` enumerates every line in this repository's own tracked markdown that assigns a declared coordinate, and fails if the matcher cannot read one. So the form list is derived from the corpus: a form no document here uses is not a gap, and a new form is added in the same change as the document that introduces it. That is what stops the fifth widening. + +**The four scope mutations routed from #242 are all closed**, by one richer fixture rather than four tests: two copies, in two sections, one short, one in markdown. Each element defeats a specific narrowing — stopping at the first finding, reading only `target`, reinstating a length floor, skipping the markdown branch. Plus one small test for a package module that does not parse, which must be refused rather than stepped over. All five measured surviving before, all five caught after. + +**And an exclusion list was built and then deleted**, which is the useful part. The plan called for excluding values spelled like this repository's own code. Measured against what the AST branch actually sees — non-docstring string constants — **the collision does not exist**: the only registry values appearing that way are the two `contract` rows, and `contract` is MIRRORED rather than CONSUMED, so it is never in the scanned set. The list narrowed a security scan for a problem this branch does not have. The residual is latent and recorded in C-97: the day someone writes a package directory name as a bare string constant, the scan will fire on it. + **Why this belongs on C-57 rather than in a new entry.** It is the same guard, the same failure direction, and the same lesson this entry already records one layer down: the earlier amendment found the scan's *scope* was never mutation-proven (it named `unfao` and missed `crafd`); this one finds its *matching* was never proven against the file formats it scans. Scope, matching, and now syntax-variant — three ways for a mutation-proven guard to be proven against the wrong thing. **⚠ CORRECTED 2026-08-12, and the correction is the same mistake one level up.** The amendment immediately above claims "thirteen forms are proven caught" and cites `` set `NAME=value` before … `` as evidence the mid-sentence class is covered. An independent review supplied twenty-nine forms and **fifteen missed**, including the *unbackticked* form of that very example: `set NAME=value before running` captures `'value before running'`. The cited case passes only because of its backticks. Measured, not argued. diff --git a/tests/test_env_declaration.py b/tests/test_env_declaration.py index 2672ed6..5a59f7c 100644 --- a/tests/test_env_declaration.py +++ b/tests/test_env_declaration.py @@ -1111,6 +1111,7 @@ def _docstring_nodes(tree: ast.AST) -> set[int]: return out + def test_no_coordinate_value_is_copied_into_this_repo(): """The registry's own rule: *"never bake a value into code, an example, or a dataclass default."* Consumers READ and VALIDATE; the launcher supplies values. @@ -1143,7 +1144,7 @@ def test_no_coordinate_value_is_copied_into_this_repo(): # environment. The no-copy rule protects values the launcher supplies; a mirror is # the inverse by construction. scanned_sections = tuple( - name for name, role in _TABLE_ROLE.items() if role == "CONSUMED" and name != "secret" + name for name, role in _TABLE_ROLE.items() if role == "CONSUMED" ) # value -> every coordinate declaring it. A list, because two coordinates may share # a value and measured against the live registry two pairs do. @@ -1156,7 +1157,6 @@ def test_no_coordinate_value_is_copied_into_this_repo(): if isinstance(body.get("value"), str) and body["value"].strip(): declared_by_value.setdefault(body["value"], []).append(name) values = set(declared_by_value) - copied = [] for source in sorted(_PKG.rglob("*.py")): tree = _parsed(source) @@ -1187,46 +1187,21 @@ def test_no_coordinate_value_is_copied_into_this_repo(): # and the identical lesson: when a guard cries wolf, the matching is wrong before the # scope is (ADR-014 §3). # - # The copy is a value **assigned to its own coordinate name** — `APPWRITE_X=value` — - # which is a reader's instruction to configure with that literal. That is precise - # enough to have caught README.md and to ignore every legitimate mention. - # - # **It has been blind in this repository's own house style twice, and both blindnesses - # had the same cause: the pattern described one way of writing markdown.** - # - # 1. `(.+?)\s*$` swallowed an inline comment into the captured value, so - # `NAME=value # note` compared `'value # note'` against the registry and - # matched nothing. README.md's Configuration block is written in exactly that form. - # 2. Anchoring the name at `^\s*` saw only an assignment that *starts a line*. A - # markdown bullet — ``- `NAME=value` `` — a table cell, or an assignment quoted - # mid-sentence were all invisible, and all three are ordinary ways to document - # configuration. The fenced-block form the guard was written against is the one - # form this repository happens to use today. + # A copy is a declared NAME assigned a declared VALUE. Both halves are known before + # the scan starts, so it matches the pair rather than parsing the line and comparing + # what it captured. Three rounds of blindness came from that parse, all of them the + # same shape — the pattern described one dialect of markdown (register C-57). # - # So the name may be preceded by anything that is not part of an identifier, and the - # value ends at whatever terminates it in prose: a comment, a closing backtick, or a - # table pipe. A `#` inside a QUOTED value is legitimate, so the quoted forms are tried - # first and taken whole; only an unquoted value is truncated. + # The two halves need not correspond: `APPWRITE_X=` is + # the ordinary copy-paste slip, and is a copy. # - # This stays a syntax match, deliberately. A value merely *named* in a sentence is not - # a copy — C-57 recorded a draft that fired on a dozen such documents, and a guard that - # cries wolf gets deleted, after which the real rule is unguarded (ADR-014 §3). - assignment = re.compile( - r"(?:^|(?<=[\s`|>*-]))(?:export\s+)?(" + "|".join(sorted(_EXPECTED_NAMES)) + r")\s*=" - r"""\s*(?:"([^"]*)"|'([^']*)'|([^#`|]*?))\s*(?:[#`|].*)?$""" + # It stays a syntax match. A value merely *named* in a sentence is not a copy, and a + # draft that fired on those was deleted for crying wolf (ADR-014 §3, C-57). + names_alt = "|".join(sorted(_EXPECTED_NAMES)) + values_alt = "|".join(re.escape(v) for v in sorted(values, key=len, reverse=True)) + pair = re.compile( + rf"""(?