diff --git a/artifacts/release-v0.77/RQ-77-PROSEBLIND.yaml b/artifacts/release-v0.77/RQ-77-PROSEBLIND.yaml index ef156396d..8babfbbcb 100644 --- a/artifacts/release-v0.77/RQ-77-PROSEBLIND.yaml +++ b/artifacts/release-v0.77/RQ-77-PROSEBLIND.yaml @@ -36,6 +36,11 @@ artifacts: req-type: process priority: must issue: "#1319" + # R14 (#1484): the `verified-by:` opens REFUTED beside a claiming status, + # so the ISSUE's fate must be declared rather than inferred from prose. + # `closes` is the truthful value: #1319 is CLOSED because the refutation + # this artifact records had ALREADY SHIPPED in v0.69. Not `outlives`. + issue-scope: closes landed: "#1424" verified-by: >- REFUTED, AND THE REFUTATION ALREADY SHIPPED IN v0.69 — which this lane did not know when it diff --git a/artifacts/release-v0.82/RQ-82-CLOSEARM.yaml b/artifacts/release-v0.82/RQ-82-CLOSEARM.yaml index 73ad9585b..02d3a750a 100644 --- a/artifacts/release-v0.82/RQ-82-CLOSEARM.yaml +++ b/artifacts/release-v0.82/RQ-82-CLOSEARM.yaml @@ -2,7 +2,7 @@ artifacts: - id: RQ-82-CLOSEARM type: system-req release: v0.82 - status: proposed + status: implemented title: "Satisfying one gate ARMED another: a claiming status is both R3's remedy and the closer's permission, and nothing reads the conjunction" description: > v0.81 nearly closed an external reporter's live defect by machine, and neither gate @@ -39,8 +39,13 @@ artifacts: artifacts no longer carry `outlives`, or the authorised set no longer contains an issue whose artifact disclaims progress, the instance is gone — re-derive the shape before building on it. Then - `python3 -c "import sys; sys.path.insert(0,'scripts'); import status_evidence_check as s; print(sorted(s.INCOMPLETE))"` - — if an INCOMPLETE verdict beside a claiming status is ALREADY red, this premise is dead. + `python3 -c "import sys; sys.path.insert(0,'scripts'); import verdict_prose_check as v; print(sorted(v.INCOMPLETE))"` + — CORRECTED at the lane's start: `INCOMPLETE`/`COMPLETE` live in `verdict_prose_check`, + NOT in `status_evidence_check` (which owns `CLOSES_WITHOUT_CLAIMING`); the wrong module + raises AttributeError, which is the GOOD failure. Then derive the population with the + GATE'S OWN classifier and never a hand-written regex: `leading_verdict(vb) in INCOMPLETE + and status in CLAIMING` over `artifacts/**/*.yaml`. If that shape is ALREADY red, this + premise is dead. tags: - process - gate @@ -53,5 +58,35 @@ artifacts: req-type: process priority: must issue: "#1484" + landed: "R14 landed in PR #1486 — the rule, seven unit tests, both potency mutants, and the day-one red (RQ-77-PROSEBLIND gains `issue-scope: closes`)." + verified-by: > + DELIVERED, AND THE RULE JUDGES ITS OWN ARTIFACT. R14 reds an artifact whose + `verified-by` opens with an INCOMPLETE verdict beside a CLAIMING status with no + `issue-scope:` declared. This artifact is claiming, so its own verdict had to be a + COMPLETE one — and it is, truthfully: the rule, seven tests, paired potency mutants + in BOTH directions and the single day-one red are all in the tree. + + PROVEN POTENT WITH PAIRED MUTANTS, because the rule has two failure directions and + each is invisible to the other's tests. Forcing the condition False kills the two + red-first tests; dropping `not _iscope14` so it over-fires kills the two green-path + tests; as shipped 7/7 pass and the live gate is rc=0. On real data R14 reds + RQ-77-PROSEBLIND, and reds BOTH v0.81 artifacts when their `outlives` is stripped — + the near-miss an operator caught by hand, reproduced by a gate. + + THE DESIGN WAS CONSTRAINED BY THE DEFECT IT FIXES. Demanding `outlives` would have + reddened a legitimate artifact: R11 forbids a non-delivery `disposition:` beside a + claiming status and R3 forbids a non-claiming status with done-when evidence, so + claiming + prose-only refutation + no disposition is the FORCED configuration. R14 + therefore demands a DECLARATION in either direction, and R3 and R11 are untouched. + + THE VOCABULARY IS IMPORTED UNDER AN IDENTITY ASSERT: `INCOMPLETE` and + `leading_verdict` come from `verdict_prose_check`, checked with `assertIs` and not + `assertEqual`, because an equal-but-separate set passes today and drifts tomorrow. + Asserted: zero `INCOMPLETE = {` literals in the gate. + + AND IT WAS NEARLY BORN WITH THE DEFECT ITS SIBLING LANE SCOPES: written as + `str(fields.get("issue-scope", ""))` a bare key would read as the truthy string + "None", so a declaration of NOTHING would have SATISFIED R14. It is written `or ""` + before `str()`, and a test pins exactly that case. verification-track: manual - done-when: "manual:2026-12-31 an artifact that claims its outcome holds while its own `verified-by` opens with an INCOMPLETE verdict and declares no `issue-scope: outlives` is RED, proven red-first against v0.81's own two artifacts with the `outlives` field removed, and proven NOT to red either of them as shipped; R3's rule unchanged; and the live population measured before the rule lands so a day-one red count is known rather than discovered" + done-when: "manual:2026-12-31 an artifact that claims its outcome holds while its own `verified-by` opens with an INCOMPLETE verdict and declares NO `issue-scope:` AT ALL is RED — EITHER value satisfies it, because R11 forbids a non-delivery `disposition:` beside a claiming status and R3 forbids a non-claiming status with done-when evidence, so claiming + prose-only refutation + no disposition is the FORCED configuration and demanding `outlives` specifically would red RQ-77-PROSEBLIND, whose issue IS resolved; proven red-first against v0.81's own two artifacts with the `outlives` field removed, proven NOT to red either of them as shipped, proven not to fire on a COMPLETE verdict or a non-claiming status, proven that a VALUELESS `issue-scope:` key still reds, and the verdict vocabulary IMPORTED from `verdict_prose_check` with an identity assert so no mirror can drift; R3's and R11's rules unchanged; and the live population measured before the rule lands so the day-one red count is known rather than discovered" diff --git a/artifacts/release-v0.82/RQ-82-VALUELESSKEY.yaml b/artifacts/release-v0.82/RQ-82-VALUELESSKEY.yaml index d8d14b6b4..be045cbcd 100644 --- a/artifacts/release-v0.82/RQ-82-VALUELESSKEY.yaml +++ b/artifacts/release-v0.82/RQ-82-VALUELESSKEY.yaml @@ -34,6 +34,35 @@ artifacts: truth or membership. Derive that set and fix the class, or state which sites are deliberately tolerant and why. + POPULATION DERIVED AT THE LANE'S START, with `ast` and not a regex — a + `git grep` for the literal `str(fields.get(` misses `str(art.get(`, + `str(a.get(`, `str(fl.get(` and `str((a[4] or {}).get(`, so it UNDERCOUNTS the + very class this artifact scopes. Walking every `scripts/**/*.py` for a + `str(.get(...))` call whose argument is a bare `.get` (an `or ""` guard + makes the argument a BoolOp, so already-fixed sites are excluded by + construction, and that exclusion is controlled): + + 32 total sites of the shape + 14 of them read a YAML ARTIFACT FIELD — the live defect class, because an + author's bare key is what makes the value None + 13 read GitHub API JSON or config, where the None comes from the API + 2 are test fixtures and cannot surprise + 3 other (two reach an artifact field through a tuple) + 11 of the 32 are inside `status_evidence_check.py` itself + + TWO SITES ARE NAMED BECAUSE THEY SIT IN R12's OWN PATH: + `str(fields.get("issue-scope", ""))` and `str(fields.get("issue", ""))`. A bare + `issue-scope:` key there reads as the INVENTED string "none" and then fails R12's + vocabulary check with a value no author wrote — directly beneath R11's own comment + admitting "Round 1 fixed the same trap in `loop_conformance_check` and left R11's + twin — the shape this programme has had to correct repeatedly." + + WHAT THIS LANE MUST NOT DO: patch 32 sites. The classes differ in whether None is + reachable at all, so the deliverable is a DERIVATION plus a tripwire over the + artifact-field class, not a sweep. v0.82's own R14 was written with `or ""` BEFORE + `str()` and is therefore NOT in the population — verified by re-running the census + after it landed. + REFUTING COMMAND: `git grep -c -F 'str(fields.get(' -- scripts/` and `python3 -c "print(repr(str(None).strip()))"`. If the idiom is gone from the gate surface, or if a shared accessor now normalises it, the premise is dead. Then diff --git a/scripts/repro/diagnostic_issue_census_345.py b/scripts/repro/diagnostic_issue_census_345.py index 710740ff6..b858e0a1a 100644 --- a/scripts/repro/diagnostic_issue_census_345.py +++ b/scripts/repro/diagnostic_issue_census_345.py @@ -7,7 +7,7 @@ # make this wrong" named exactly this outcome. What ships instead is this # script plus the convention recorded in CLAUDE.md. There is nothing here for # CI to fail on, which is why it is `manual` and not `wired`. -"""RQ-74-STALEMSG (#345): what does an issue number in a diagnostic MEAN? +r"""RQ-74-STALEMSG (#345): what does an issue number in a diagnostic MEAN? WHY THIS WAS ASKED. v0.73 cost real time twice to one diagnostic: diff --git a/scripts/status_evidence_check.py b/scripts/status_evidence_check.py index 393819549..9c18116f3 100644 --- a/scripts/status_evidence_check.py +++ b/scripts/status_evidence_check.py @@ -353,6 +353,17 @@ class that produced the splice disappears at the source (#1059's import yaml +# R14 (#1484, RQ-82-CLOSEARM) reads the LEADING VERDICT of `verified-by:`. The +# classifier and its vocabulary live in `verdict_prose_check` and are IMPORTED, +# never mirrored: a second copy of COMPLETE/INCOMPLETE is how the model drifts +# from the shipped table (#667's lesson, one tier down). A FAILING import must +# be LOUD — a gate that cannot classify a verdict must not read as "no finding". +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from verdict_prose_check import ( # noqa: E402 + INCOMPLETE as VERDICT_INCOMPLETE, + leading_verdict as _leading_verdict, +) + REPO_ROOT = Path(__file__).resolve().parent.parent # Statuses that CLAIM the artifact's stated outcome holds. Everything else @@ -1788,6 +1799,47 @@ def check(root: Path, release_glob: str, subjects: list[str], f"`proposed`, #1250); flip to a claiming status, or " f"set `disposition: partial|refuted|deferred`" ) + # R14 (#1484, RQ-82-CLOSEARM). SATISFYING ONE GATE CAN ARM ANOTHER. + # `status: implemented` is simultaneously R3's ONLY remedy for "the + # status under-reports shipped work" AND `authorised_close_set`'s + # permission to retire the issue. v0.81's JESSDIVERGE3 and WIDEFILE4 + # each delivered a REFUTATION, said so in their own `verified-by`, and + # were set claiming for R3's sake — which silently placed an externally + # reported live blocker in the authorised set. An operator caught it by + # running the closure gate before tagging; no gate did. + # + # WHY THIS DOES NOT DEMAND `outlives`, AND WHY IT CANNOT DEMAND A + # `disposition:`. R11 reds a non-delivery `disposition:` beside a + # claiming status, and R3 reds a non-claiming status whose done-when + # evidence exists. So for a lane that did real work and refuted its own + # premise, claiming + prose-only refutation + NO disposition is the + # FORCED configuration — there is no legal third option. Demanding + # `outlives` would therefore red a legitimate artifact whose issue IS + # resolved (RQ-77-PROSEBLIND: `verified-by` opens REFUTED, and #1319 is + # closed because the refutation shipped in v0.69). + # + # What this demands instead is a DECLARATION, in either direction: say + # `closes` or say `outlives`, in the field the closure gate already + # reads. The prose already states the outcome; this makes the ISSUE's + # fate structural rather than inferable only by reading English. + # R3 and R11 are untouched. + if version >= DISPOSITION_SINCE: + # `or ""` BEFORE `str()` — `str(None)` is the four-character string + # "None" and a bare key would read as a verdict. (#1458.) + _vb = str(fields.get("verified-by") or "") + _lead = _leading_verdict(_vb) if _vb.strip() else None + _iscope14 = str(fields.get("issue-scope") or "").strip().lower() + if _lead in VERDICT_INCOMPLETE and status in CLAIMING and not _iscope14: + failures.append( + f"R14 {art_id}: `verified-by:` opens `{_lead}` while status " + f"`{status}` claims the outcome holds, and no `issue-scope:` " + f"says what that means for the ISSUE. A claiming status is " + f"both R3's only remedy and the closure gate's permission, " + f"so this shape silently authorises retiring an issue the " + f"artifact itself says is unresolved. Declare " + f"`issue-scope: outlives` (the issue survives this release) " + f"or `issue-scope: closes` (the verdict resolves it)." + ) # R12 (#1250, RQ-71-ISSUESCOPE): an artifact may declare that its # ISSUE outlives its own delivery. Separate key from `disposition:` # by design — see ISSUE_SCOPES above — so this never trips R11. diff --git a/scripts/test_oracle_run.py b/scripts/test_oracle_run.py index 46d70f435..d56e400ed 100644 --- a/scripts/test_oracle_run.py +++ b/scripts/test_oracle_run.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Unit tests for scripts/oracle_run.py. +r"""Unit tests for scripts/oracle_run.py. RQ-77-STDERR (#1419). This driver runs **206 oracles** in CI and had NO tests at all, and was not wired into `ci.yml`. The specific defect it shipped with: an diff --git a/scripts/test_status_evidence_check.py b/scripts/test_status_evidence_check.py index bc1640d65..982668e57 100644 --- a/scripts/test_status_evidence_check.py +++ b/scripts/test_status_evidence_check.py @@ -2076,6 +2076,84 @@ def test_r4_plan_and_chore_subjects_are_not_matched(self): +class R14ClosearmPoseVerdict1484(unittest.TestCase): + """RQ-82-CLOSEARM (#1484). A claiming status is simultaneously R3's ONLY + remedy for "the status under-reports shipped work" AND the closure gate's + permission to retire the issue, and nothing read their conjunction. v0.81's + JESSDIVERGE3 and WIDEFILE4 each delivered a REFUTATION, said so in their own + `verified-by`, and were set claiming for R3's sake -- which silently placed + an externally reported LIVE blocker in the authorised close set. An operator + caught it by running the closure gate before tagging; no gate did. + + WHY R14 DEMANDS A DECLARATION RATHER THAN `outlives`. R11 reds a + non-delivery `disposition:` beside a claiming status, and R3 reds a + non-claiming status whose done-when evidence exists, so for a lane that did + real work and refuted its own premise, claiming + prose-only refutation + + NO disposition is the FORCED configuration. Demanding `outlives` would red + RQ-77-PROSEBLIND, whose `verified-by` opens REFUTED and whose issue (#1319) + IS closed because the refutation shipped in v0.69. So R14 demands that the + ISSUE's fate be STATED -- either value -- in the field the closure gate + already reads.""" + + MANUAL = "manual: the stated outcome holds" + REFUTED = "REFUTED. The premise did not survive its own measurement." + DELIVERED = "DELIVERED. Both directions, measured at the cut." + + def _fx(self, fields): + fx = Fixture() + fx.release("release-v0.82/RQ-82-SUBJECT.yaml", + [art("RQ-82-SUBJECT", fields.pop("status"), fields)]) + return fx + + def test_incomplete_verdict_beside_claiming_without_a_scope_is_red(self): + fx = self._fx({"status": "implemented", "done-when": self.MANUAL, + "verified-by": self.REFUTED}) + self.assertTrue(has(fx.run([]), "R14 RQ-82-SUBJECT"), fails(fx.run([]))) + + def test_outlives_satisfies_it(self): + fx = self._fx({"status": "implemented", "done-when": self.MANUAL, + "verified-by": self.REFUTED, "issue-scope": "outlives"}) + self.assertEqual(fails(fx.run([])), []) + + def test_closes_satisfies_it_too(self): + # The PROSEBLIND shape: the verdict refutes the premise AND resolves the + # issue. Demanding `outlives` here would be false. + fx = self._fx({"status": "implemented", "done-when": self.MANUAL, + "verified-by": self.REFUTED, "issue-scope": "closes"}) + self.assertEqual(fails(fx.run([])), []) + + def test_a_COMPLETE_verdict_is_untouched(self): + # CONTROL: the rule must fire on the INCOMPLETE set only, or it would + # demand a scope declaration from every delivered artifact in the tree. + fx = self._fx({"status": "implemented", "done-when": self.MANUAL, + "verified-by": self.DELIVERED}) + self.assertEqual(fails(fx.run([])), []) + + def test_a_non_claiming_status_is_untouched(self): + # CONTROL: R3/R11 own that case. R14 must not double-report it. + fx = self._fx({"status": "proposed", "done-when": self.MANUAL, + "verified-by": self.REFUTED}) + self.assertFalse(has(fx.run([]), "R14 "), fails(fx.run([]))) + + def test_a_VALUELESS_issue_scope_key_declares_nothing_and_still_reds(self): + # #1458's shape: a bare `issue-scope:` parses to None, and `str(None)` + # is the four-character string "None" -- truthy. Read that way a + # valueless key would SATISFY R14 while declaring nothing at all. + fx = self._fx({"status": "implemented", "done-when": self.MANUAL, + "verified-by": self.REFUTED, "issue-scope": None}) + self.assertTrue(has(fx.run([]), "R14 RQ-82-SUBJECT"), fails(fx.run([]))) + + def test_the_vocabulary_is_IMPORTED_not_mirrored(self): + # DERIVE WHAT YOU CHECK AGAINST. If R14 ever grows its own copy of the + # verdict sets, the two drift silently -- the #667 lesson one tier down. + # Identity, not equality: an equal-but-separate set would still drift. + import verdict_prose_check as vpc + self.assertIs(sec.VERDICT_INCOMPLETE, vpc.INCOMPLETE, + "R14 must READ verdict_prose_check's set, not copy it") + self.assertIs(sec._leading_verdict, vpc.leading_verdict, + "R14 must READ verdict_prose_check's classifier") + + class ClosesWithoutClaimingIsScopedToRefuted(unittest.TestCase): """v0.81 round-1 cold review, finding 9.