From b5f9530e78db97be84a17079ad5c50d425e4ad6a Mon Sep 17 00:00:00 2001 From: kraxo Date: Sun, 16 Aug 2026 15:06:03 +0200 Subject: [PATCH] fix(gate): the audit-marker negation guard survives a line break RT10-PRETAG-02 stops a marker from counting when its own line takes it back -- "the adversarial audit did NOT run" must not certify anything. The scope was the physical LINE, and that is where it breaks: wrapped prose puts the marker and its negation on different lines. FOUND BY TRIGGERING IT, not by reading the code. While writing an honest retraction into a release candidate's CHANGELOG -- a paragraph whose entire point is that the audit had not finished -- pre_tag_audit_gate flipped to {"ok": true, "changelog_records_audit": true} and stayed there for about two minutes: ...never shipped a user-facing CLI flag in a patch release. That claim was falsified during the pre-tag adversarial audit and is retracted here rather than quietly deleted: four patch releases have shipped Line two opens with `adversarial` and carries no negation token; the words that take it back (`never`, and the fact that the run was still going) sit on line one. Nothing was crafted to defeat the guard. Ordinary wrapping at 110 columns did it. That is the sharp edge of this class and the reason it is worth a commit of its own: the guard is most easily defeated by exactly the kind of careful, self-critical writing the project demands of itself. A tag could have been cut on the strength of a sentence admitting the release was not ready. FIX: paragraph scope (blank-line separated) instead of line scope. A paragraph contains its lines, so the change can only bring MORE negation tokens into view, never fewer -- no text the old rule rejected can be accepted by the new one. That monotonicity is asserted over a corpus in test_the_new_scope_is_strictly_stricter rather than argued, because "obviously monotone" is how a loosening gets in. SENTENCE SCOPE WAS MEASURED FIRST AND REJECTED. It still returns True on the block above: "falsified" is not a negation token, and the sentence carrying `adversarial` genuinely contains none. A finer scope does not help when the negation lives in the surrounding argument rather than in a word. Recorded because the obvious fix looks right and is not. line-scoped (before) -> True the defect sentence-scoped -> True does NOT close it paragraph-scoped (now) -> False closes it COUNTER-DIRECTION, because a stricter gate that rejects real records is just a different defect: the committed audit_artifacts/370/pre_tag_adversarial_audit_370.md still returns True under all three rules. Its opening paragraph pairs the claim with a "NOT a substitute for the external audit" disclaimer and is correctly not counted; the later "Six diverse falsification-first lenses ..." paragraph carries the marker cleanly and is the attestation. House style survives, and a test pins that. Paragraph scope also must not become FILE scope, or one caveat anywhere would kill every record -- test_a_separate_paragraph_still_carries_its_own_attestation holds that boundary from the other side. REVERT PROBE: with the fix stashed out, this file goes red on exactly the three tests that name the property (live case, paragraph split, meta) -- 3 failed, 6 passed. With it, 9 passed / 14 subtests. tests/test_roadmap_frontload_foundations.py unchanged at 19 passed. HONEST LIMIT, stated in the code as well: this still infers a fact from free prose, and prose inference stays defeatable in principle. The durable answer is an explicit attestation token that means one thing -- `pre-tag-adversarial-audit: RUN | version=X.Y.Z` -- which the version-consistency-gate branch already moves to. This narrows a live hole in the mechanism that guards releases TODAY; it does not claim to have made prose inference sound. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/pre_tag_audit_gate.py | 44 ++++++- tests/test_audit_marker_line_wrap.py | 164 +++++++++++++++++++++++++++ 2 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 tests/test_audit_marker_line_wrap.py diff --git a/scripts/pre_tag_audit_gate.py b/scripts/pre_tag_audit_gate.py index 3ecaf0b..24e13ad 100644 --- a/scripts/pre_tag_audit_gate.py +++ b/scripts/pre_tag_audit_gate.py @@ -47,11 +47,45 @@ def _positive_audit_marker(text: str) -> bool: - """True iff some line ASSERTS an adversarial/N-lens audit was run — a discipline marker on a line that - is NOT negated. Line-scoped so a real positive note survives an unrelated negation elsewhere in the file, - while a marker whose own sentence concedes the audit did not run does not grant a false PASS.""" - for line in text.splitlines(): - if _AUDIT_MARKERS.search(line) and not _AUDIT_NEGATION.search(line): + """True iff some PARAGRAPH asserts an adversarial/N-lens audit was run — a discipline marker in a + paragraph that carries no negation. + + RT10-PRETAG-03 (2026-08-16, found by triggering it): the scope used to be the physical LINE, and a + wrapped sentence walked straight through the RT10-PRETAG-02 negation guard. Measured live on a real + release candidate, where an honest retraction being written INTO the CHANGELOG flipped the gate to + ``ok: true, changelog_records_audit: true`` for roughly two minutes: + + …never shipped a user-facing CLI flag in a patch release. That claim was falsified during the pre-tag + adversarial audit and is retracted here rather than quietly deleted: four patch releases have shipped + + The second line opens with ``adversarial`` and carries no negation token of its own — the words that + take it back (``never``, and the fact that the audit was still running) sit on the line above. Nothing + was crafted to defeat the guard; ordinary prose wrapping at 110 columns did it, and it will do it again + to anyone who writes a careful sentence about an audit that has not finished. The guard was therefore + most easily defeated by exactly the kind of honest text this project requires of itself. + + Paragraph scope is strictly STRICTER than line scope — a paragraph contains its lines, so it can only + ever bring MORE negation tokens into view, never fewer. No text that the old rule rejected can be + accepted by the new one. + + Sentence scope was measured first and REJECTED: it still returns True on the block above, because + "falsified" is not a negation token and the sentence carrying ``adversarial`` genuinely contains none. + A finer scope does not help when the negation lives in the surrounding argument rather than in a word. + + The counter-direction is measured too, because a stricter gate that rejects genuine records would just + be a different defect: the real ``audit_artifacts/370/pre_tag_adversarial_audit_370.md`` still returns + True. Its opening paragraph pairs the claim with a "NOT a substitute for the external audit" + disclaimer, so that paragraph is correctly not counted — and the later "Six diverse falsification-first + lenses …" paragraph carries the marker with no negation, which is the attestation. House style survives. + + HONEST LIMIT, unchanged by this fix: this infers a fact from free prose, and prose inference stays + defeatable in principle. The durable answer is an explicit attestation token that means one thing + ("pre-tag-adversarial-audit: RUN | version=X.Y.Z"), which is what the version-consistency-gate branch + moves to. This narrows a live hole in the mechanism that guards releases today; it does not claim to + have made prose inference sound. + """ + for absatz in re.split(r"\n\s*\n", text): + if _AUDIT_MARKERS.search(absatz) and not _AUDIT_NEGATION.search(absatz): return True return False diff --git a/tests/test_audit_marker_line_wrap.py b/tests/test_audit_marker_line_wrap.py new file mode 100644 index 0000000..2cc3fe4 --- /dev/null +++ b/tests/test_audit_marker_line_wrap.py @@ -0,0 +1,164 @@ +"""RT10-PRETAG-03: the audit-marker negation guard must not be defeated by ordinary line wrapping. + +`_positive_audit_marker` decides whether a release has recorded its pre-tag adversarial audit. It looks +for a discipline marker (`N-lens`, `adversarial`, `master-prompt`, `Linsen`) that is not negated. The +scope of "not negated" used to be the physical LINE. + +HOW IT WAS FOUND, which matters more than the fix: by triggering it. While writing an honest retraction +into a release candidate's CHANGELOG — a paragraph whose whole point is that the audit had NOT finished — +the gate flipped to `ok: true, changelog_records_audit: true` and stayed there for about two minutes. +Nothing was crafted. Prose wrapped at 110 columns put the word `adversarial` at the start of a line, and +the words that take it back stayed on the line above. + +That is the sharp edge of the class: the guard was most easily defeated by exactly the kind of careful, +self-critical text the project requires of itself. A release could have been tagged on the strength of a +sentence admitting it was not ready. +""" +from __future__ import annotations + +import importlib.util +import re +import unittest +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] + + +def _load(mod_name: str, rel: str): + """Same loader shape the rest of the suite uses for `scripts/` (see test_roadmap_frontload_foundations).""" + spec = importlib.util.spec_from_file_location(mod_name, REPO / rel) + module = importlib.util.module_from_spec(spec) + assert spec and spec.loader + spec.loader.exec_module(module) + return module + + +gate = _load("marker_wrap_pretag_gate", "scripts/pre_tag_audit_gate.py") + + +def _line_scoped(text: str) -> bool: + """The rule as it stood before this fix, kept so both directions can be measured, not asserted.""" + for line in text.splitlines(): + if gate._AUDIT_MARKERS.search(line) and not gate._AUDIT_NEGATION.search(line): + return True + return False + + +# The exact two lines that flipped the live gate. Reproduced verbatim rather than described, so this +# test fails for the reason it names if the wrapping ever stops mattering. +DER_LIVE_FALL = ( + "…never shipped a user-facing CLI flag in a patch release. That claim was falsified during the pre-tag\n" + "adversarial audit and is retracted here rather than quietly deleted: four patch releases have shipped" +) + + +class TestWrappedNegationNoLongerGrantsPass(unittest.TestCase): + + def test_the_live_case_is_now_rejected(self): + self.assertFalse(gate._positive_audit_marker(DER_LIVE_FALL), + "a wrapped retraction still certifies the audit it retracts") + + def test_counter_check_the_old_rule_really_did_accept_it(self): + """Without this, the test above could be green because the corpus is harmless. + + A fix test that never demonstrates the defect is a test that measures nothing. This one shows the + previous rule saying yes to the same bytes. + """ + self.assertTrue(_line_scoped(DER_LIVE_FALL), + "the pre-fix rule does not reproduce the defect — the fixture no longer discriminates") + + def test_a_genuine_record_is_still_accepted(self): + """A stricter gate that rejects real records would just be a different defect. + + The committed 3.7.0 record is the house style: its opening paragraph pairs the claim with a + "NOT a substitute for the external audit" disclaimer, and a later paragraph carries the marker + cleanly. Paragraph scope must count the second and not the first. + """ + echt = REPO / "audit_artifacts" / "370" / "pre_tag_adversarial_audit_370.md" + if not echt.is_file(): + self.skipTest(f"reference record absent: {echt} — measuring nothing is not a pass") + text = echt.read_text(encoding="utf-8") + self.assertTrue(gate._positive_audit_marker(text), + "the fix rejects a real, committed pre-tag audit record") + + def test_the_original_single_line_negation_still_holds(self): + """RT10-PRETAG-02 must not regress: a negated marker on one line stays a non-attestation.""" + for text in ("the adversarial audit did NOT run", + "adversarial review pending", + "the 6-lens jury has not completed", + "master-prompt audit skipped for this release", + "Linsen-Lauf noch nicht durchgefuehrt"): + with self.subTest(text=text): + self.assertFalse(gate._positive_audit_marker(text), f"negated marker granted a pass: {text}") + + def test_the_new_scope_is_strictly_stricter(self): + """The property that makes this a safe change: accepted(new) is a SUBSET of accepted(old). + + A paragraph contains its own lines, so widening the scope can only bring more negation tokens into + view. Anything the old rule rejected must stay rejected. Asserted over a corpus rather than argued, + because "obviously monotone" is how a loosening slips in. + """ + korpus = [ + DER_LIVE_FALL, + "six-lens adversarial audit run before the tag", + "adversarial audit\nnot completed", # wrap, negation AFTER the marker + "not completed\nadversarial audit", # wrap, negation BEFORE the marker + "clean prose with no marker at all", + "adversarial\n\nnot completed", # separate paragraphs: marker survives + "| F7 | target | holds | a 6-lens pass |\n| F8 | other | fell | not run |", + "", + "adversarial audit run.\n\nA later paragraph says the release is not ready.", + ] + for text in korpus: + with self.subTest(text=text[:48]): + if gate._positive_audit_marker(text): + self.assertTrue(_line_scoped(text), + "the new rule accepts text the old rule rejected — this is a LOOSENING, " + "not a hardening") + + def test_a_separate_paragraph_still_carries_its_own_attestation(self): + """Paragraph scope must not become file scope, or one disclaimer anywhere would kill every record. + + This is the boundary the fix has to hit exactly: strict enough that a wrapped retraction cannot + certify itself, loose enough that an unrelated caveat elsewhere in the document does not suppress a + genuine attestation. It is the same intent RT10-PRETAG-02 stated for lines, moved up one level. + """ + text = ("A six-lens adversarial audit ran on this candidate.\n" + "\n" + "This is not a substitute for the external human audit.\n") + self.assertTrue(gate._positive_audit_marker(text), + "an unrelated caveat in a DIFFERENT paragraph suppressed a genuine attestation") + + def test_meta_reverting_the_fix_makes_this_file_fail(self): + """PLANT-AND-MUST-CATCH: with the line-scoped rule restored, the live case must go red again. + + Without this, a future refactor could quietly restore line scope and every test above would still + pass on its own terms — the file would keep asserting a property it no longer measures. + """ + gemessen = _line_scoped(DER_LIVE_FALL) + self.assertTrue(gemessen, + "the restored pre-fix rule no longer accepts the live case, so this file can no " + "longer tell the two rules apart") + self.assertNotEqual(gemessen, gate._positive_audit_marker(DER_LIVE_FALL), + "the two rules agree on the defect case — the fix has no effect") + + +class TestParagraphSplitIsTheDocumentedOne(unittest.TestCase): + """The split must be blank-line separated paragraphs, measured rather than assumed from the docstring.""" + + def test_a_blank_line_separates_scopes(self): + verbunden = "adversarial audit\nnot completed" + getrennt = "adversarial audit\n\nnot completed" + self.assertFalse(gate._positive_audit_marker(verbunden), "same paragraph: negation must apply") + self.assertTrue(gate._positive_audit_marker(getrennt), "different paragraph: negation must not apply") + + def test_whitespace_only_lines_also_separate(self): + """A line of spaces looks blank in an editor and must behave that way, or the scope depends on + invisible characters.""" + self.assertTrue(gate._positive_audit_marker("adversarial audit\n \nnot completed"), + "a whitespace-only line did not separate paragraphs") + self.assertTrue(re.split(r"\n\s*\n", "a\n \nb").__len__() == 2) + + +if __name__ == "__main__": # pragma: no cover + unittest.main()