From ebc85b92b8f61baad1fb2951d009ab7e4e789911 Mon Sep 17 00:00:00 2001 From: Bryant Date: Thu, 13 Aug 2026 17:11:58 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=93=9D=20(core):=20Point=20the=20?= =?UTF-8?q?=C2=A76=20Planned=20referent=20at=20the=20capability=20ticket?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR 0033 §6 scopes `Planned` to "decided but not implemented — a ticket reference". All three sites referenced AAASM-5731, which measured the absence of an SDK-side audit sink but never intended to build one. That ticket is now closed, so the term reads as a live commitment while the reference points at finished work. AAASM-5750 owns the capability. The term itself is unchanged; references citing AAASM-5731 as the ticket that *measured* the drop are left alone, because those are still true. --- agent_assembly/adapters/_shared/tool_governance.py | 2 +- agent_assembly/core/audit_sink.py | 2 +- test/unit/test_quickstart_negative_control.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/agent_assembly/adapters/_shared/tool_governance.py b/agent_assembly/adapters/_shared/tool_governance.py index 66d91412..0bc9f1f2 100644 --- a/agent_assembly/adapters/_shared/tool_governance.py +++ b/agent_assembly/adapters/_shared/tool_governance.py @@ -185,7 +185,7 @@ async def _record_async_tool_result( HTTP boundaries, this function therefore finds no hook and emits nothing on the shipped path — for allowed calls as much as denied ones. - Under ADR 0033 §6 that makes SDK-side recording **Planned** (AAASM-5731), + Under ADR 0033 §6 that makes SDK-side recording **Planned** (AAASM-5750), not *Unmeasured*: §6 reserves ``Unmeasured`` for an action no control inspected, where nothing is known, and here exactly where the record stops has been measured. It is certainly not *Observed*, which needs a durable diff --git a/agent_assembly/core/audit_sink.py b/agent_assembly/core/audit_sink.py index c25ce8d5..c079cd0f 100644 --- a/agent_assembly/core/audit_sink.py +++ b/agent_assembly/core/audit_sink.py @@ -19,7 +19,7 @@ declares its disposition; :func:`resolve_audit_sink` reads it; ``init_assembly`` warns about it and reports it on the returned context. -Under ADR 0033 §6 this makes SDK-side recording **Planned** (AAASM-5731), not +Under ADR 0033 §6 this makes SDK-side recording **Planned** (AAASM-5750), not *Observed* — *Observed* requires a durable event attributed to the action, and there is none. It is deliberately not *Unmeasured*: §6 reserves that for an action no control inspected, where nothing is known, and here exactly where the diff --git a/test/unit/test_quickstart_negative_control.py b/test/unit/test_quickstart_negative_control.py index 0a3cafe2..90793b94 100644 --- a/test/unit/test_quickstart_negative_control.py +++ b/test/unit/test_quickstart_negative_control.py @@ -300,7 +300,7 @@ def test_a_denied_call_emits_an_audit_record_carrying_the_agent_and_tool( # (RuntimeQueryInterceptor + GatewayClient expose neither # record_result nor on_tool_end), so tool outcomes produce no audit # evidence on the shipped path — Planned under ADR 0033 §6 - # (AAASM-5731), not Unmeasured, since where the record stops has been + # (AAASM-5750), not Unmeasured, since where the record stops has been # measured. What this pins is the governance flow's call — the part # fixable without wiring a sink. assert len(quickstart.interceptor.records) == 1 From a3815b0c43a69ecc5185b9e2bf31e41962327d4e Mon Sep 17 00:00:00 2001 From: Bryant Date: Thu, 13 Aug 2026 17:12:19 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=85=20(core):=20Gate=20the=20=C2=A76?= =?UTF-8?q?=20Planned=20referent=20against=20a=20source=20scan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The referent lives in a docstring or a comment, and those are the artifacts in a source tree with no mechanical check on them. These three were corrected by hand once and would stay correct only until the next edit. The scan pairs each §6 `Planned` with a ticket reference on the same line and asserts the referent. Lowercase `planned` is ordinary English and is not the term; a `Planned` with no ticket on the line is prose continuation, not a referent — `docs/examples/framework-support.md`'s maturity label is a different axis and is deliberately out of scope. A separate floor test guards the scan itself: an empty walk and a clean tree otherwise report the same result. --- test/unit/core/test_planned_referent.py | 104 ++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 test/unit/core/test_planned_referent.py diff --git a/test/unit/core/test_planned_referent.py b/test/unit/core/test_planned_referent.py new file mode 100644 index 00000000..35ab534c --- /dev/null +++ b/test/unit/core/test_planned_referent.py @@ -0,0 +1,104 @@ +"""AAASM-5750 — the ADR 0033 §6 ``Planned`` term must reference the ticket that +will build the capability, not the one that measured its absence. + +§6 scopes ``Planned`` to "decided but not implemented — a ticket reference; no +capability claim." A reference to a ticket that never intended to deliver the +capability goes stale the moment that ticket closes: the term still reads as a +live commitment while the reference points at finished work. Nothing mechanical +catches that, because the referent lives in a comment or a docstring, and those +are the artifacts in a source tree with no check on them at all. + +This is that check. It is deliberately a source scan rather than a review +convention — the previous referent was corrected by hand in three places here +and would have stayed correct only until the next edit. + +The floor is a ratchet, not a transcription. It was measured from the tree when +this was written, and exists because an empty scan and a clean scan otherwise +report the same result. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +# The ticket that owns building the SDK-side audit sink. +CAPABILITY_REFERENT = "AAASM-5750" + +# §6 Planned sites carrying a ticket reference when this gate was written. +# Fewer means sites were removed without revisiting this gate, which would leave +# it passing over nothing. +PLANNED_REFERENT_FLOOR = 3 + +_PLANNED_TERM = re.compile(r"\bPlanned\b") +_TICKET_REF = re.compile(r"AAASM-\d+") +_SCANNED_SUFFIXES = frozenset({".py", ".md"}) +_SKIPPED_DIRS = frozenset({".git", ".venv", "node_modules", "__pycache__", "build", "dist"}) + + +def _repo_root() -> Path: + """Walk up to the directory holding ``pyproject.toml``. + + Resolving the root rather than assuming a relative path keeps the scan + repository-wide regardless of the directory pytest is invoked from — a + package-relative path silently narrows the scan to whatever happens to be + below the caller. + """ + for candidate in [Path(__file__).resolve(), *Path(__file__).resolve().parents]: + if (candidate / "pyproject.toml").is_file(): + return candidate + raise AssertionError( + "no pyproject.toml found above this test; the scan would cover nothing and pass for the wrong reason" + ) + + +def _planned_sites() -> list[tuple[str, int, str, str]]: + """Every line where the §6 term and a ticket reference are co-located. + + A ``Planned`` with no ticket on the line is prose continuation, not a + referent. Lowercase ``planned`` is ordinary English and is not the §6 term. + """ + root = _repo_root() + sites: list[tuple[str, int, str, str]] = [] + + for path in sorted(root.rglob("*")): + if not path.is_file() or path.suffix not in _SCANNED_SUFFIXES: + continue + if _SKIPPED_DIRS.intersection(path.relative_to(root).parts): + continue + + for lineno, line in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), start=1): + if not _PLANNED_TERM.search(line): + continue + ticket = _TICKET_REF.search(line) + if ticket is None: + continue + sites.append((str(path.relative_to(root)), lineno, ticket.group(0), line.strip())) + + return sites + + +def test_scan_reaches_the_planned_referent_sites() -> None: + """Positive control on the scan itself. + + Without it, a walk that reached no files — a broken root, a changed suffix + set, an over-broad skip list — reports the same clean result as a tree with + every referent correct. + """ + sites = _planned_sites() + assert len(sites) >= PLANNED_REFERENT_FLOOR, ( + f"scan found {len(sites)} §6 Planned referent sites under {_repo_root()}, " + f"floor is {PLANNED_REFERENT_FLOOR}; either sites were removed without " + "revisiting this gate, or the scan stopped reaching them and is passing " + "over nothing" + ) + + +def test_planned_references_the_capability_ticket() -> None: + wrong = [site for site in _planned_sites() if site[2] != CAPABILITY_REFERENT] + assert not wrong, "\n".join( + f"{path}:{lineno} references {ticket} as the §6 Planned referent, want " + f"{CAPABILITY_REFERENT} (the ticket that builds the sink, not one that " + f"measured its absence): {text}" + for path, lineno, ticket, text in wrong + ) From 54e33735da1029f129e91b331446943e5f297400 Mon Sep 17 00:00:00 2001 From: Bryant Date: Thu, 13 Aug 2026 17:35:31 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9C=85=20(core):=20Narrow=20the=20refere?= =?UTF-8?q?nt=20gate=20to=20the=20invariant=20it=20can=20defend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Independent review broke the first version three ways, and all three came from one design mistake: the gate scanned its own file. Its header names AAASM-5750, so the floor was partly satisfied by the gate quoting itself — in node it was satisfied ENTIRELY that way, and deleting the only real site left the suite green. Four changes: * **Exclude the gate file from its own scan.** Verified with a positive control: the same stale-referent line fails when placed in any other file. * **Assert the expected SET of guarded paths, not a count.** A count can be held up by an unrelated site appearing as a real one is deleted; naming the paths makes that substitution visible. * **Pair the claim with a ticket on its line OR the next.** Coverage was decided by where a comment happened to wrap — python's quickstart control split `Planned` from its ticket across two lines and was silently unchecked while the PR claimed it was covered. Reflowing a comment now cannot hide a stale referent. * **Stop asserting that every `Planned` names AAASM-5750.** §6 scopes the term to any decided-but-unbuilt capability with any ticket, so the first version would have failed CI on the next unrelated roadmap row. The invariant is now the narrow one that is actually true: AAASM-5731 and AAASM-5681 measured the absence and will not fix it, so neither may ever be a forward referent. The rule is also now attributed correctly. It comes from AAASM-5750's own description, not from ADR 0033 §6 — §6 requires *a* ticket reference and says nothing about which. A failure message citing an ADR for a rule the ADR does not contain sends the next reader to the wrong document. --- test/unit/core/test_planned_referent.py | 144 +++++++++++++++--------- 1 file changed, 88 insertions(+), 56 deletions(-) diff --git a/test/unit/core/test_planned_referent.py b/test/unit/core/test_planned_referent.py index 35ab534c..57caf0e9 100644 --- a/test/unit/core/test_planned_referent.py +++ b/test/unit/core/test_planned_referent.py @@ -1,20 +1,24 @@ -"""AAASM-5750 — the ADR 0033 §6 ``Planned`` term must reference the ticket that -will build the capability, not the one that measured its absence. - -§6 scopes ``Planned`` to "decided but not implemented — a ticket reference; no -capability claim." A reference to a ticket that never intended to deliver the -capability goes stale the moment that ticket closes: the term still reads as a -live commitment while the reference points at finished work. Nothing mechanical -catches that, because the referent lives in a comment or a docstring, and those -are the artifacts in a source tree with no check on them at all. - -This is that check. It is deliberately a source scan rather than a review -convention — the previous referent was corrected by hand in three places here -and would have stayed correct only until the next edit. - -The floor is a ratchet, not a transcription. It was measured from the tree when -this was written, and exists because an empty scan and a clean scan otherwise -report the same result. +"""AAASM-5750 — AAASM-5731 must not be the referent of a forward-looking claim. + +The rule this gate enforces comes from **AAASM-5750's own description**, not from +ADR 0033 §6. §6 requires that ``Planned`` carry *a* ticket reference and says +nothing about which ticket; naming the right one is 5750's decision. Stating the +source precisely matters, because a failure message citing an ADR for a rule the +ADR does not contain sends the next reader to the wrong document. + +The defect: AAASM-5731 measured that no interceptor this SDK ships resolves an +audit hook. It never intended to build a sink, and it is closed. A +forward-looking claim pointing at it reads as a live commitment while resolving +to finished work. So the invariant is narrow and permanent — **AAASM-5731 may be +cited as the ticket that measured the gap, never as the ticket that will fix +it.** + +Deliberately NOT asserted: that every ``Planned`` in this repository names +AAASM-5750. §6 scopes ``Planned`` to any decided-but-unbuilt capability with any +ticket, so an unrelated roadmap row — including +``docs/examples/framework-support.md``'s docs-area maturity label, a different +axis entirely — is legitimate and must not fail this gate. The first version of +this test made exactly that over-broad assertion. """ from __future__ import annotations @@ -22,27 +26,41 @@ import re from pathlib import Path -# The ticket that owns building the SDK-side audit sink. -CAPABILITY_REFERENT = "AAASM-5750" +#: Tickets that *measured* the absence of an SDK-side audit sink. Backward +#: citations to them are correct and are left alone; what this gate forbids is +#: either one appearing as the ticket a forward-looking claim defers to. +STALE_REFERENTS = frozenset({"AAASM-5731", "AAASM-5681"}) -# §6 Planned sites carrying a ticket reference when this gate was written. -# Fewer means sites were removed without revisiting this gate, which would leave -# it passing over nothing. -PLANNED_REFERENT_FLOOR = 3 - -_PLANNED_TERM = re.compile(r"\bPlanned\b") +#: The two shapes a deferral takes: the ADR 0033 §6 term, and the plain +#: "tracked as" pointer used where no term is stated. +_FORWARD_CLAIM = re.compile(r"\bPlanned\b|tracked as") _TICKET_REF = re.compile(r"AAASM-\d+") _SCANNED_SUFFIXES = frozenset({".py", ".md"}) _SKIPPED_DIRS = frozenset({".git", ".venv", "node_modules", "__pycache__", "build", "dist"}) +#: Excluded from its own scan. Including it was the first version's defect: this +#: file names AAASM-5750 in its own docstring, which padded the site count and +#: let a floor be satisfied by the gate quoting itself. +_GATE_FILE = "test/unit/core/test_planned_referent.py" + +#: Audit-sink deferrals that must remain reachable by the scan. A fixture +#: compared against a walk of the tree, not a constant compared against another +#: constant: if a site is deleted, renamed, or reflowed out of the scan's reach, +#: the walk stops finding it and this fails. +EXPECTED_SITES = ( + "agent_assembly/core/audit_sink.py", + "agent_assembly/adapters/_shared/tool_governance.py", + "test/unit/test_quickstart_negative_control.py", +) + def _repo_root() -> Path: """Walk up to the directory holding ``pyproject.toml``. Resolving the root rather than assuming a relative path keeps the scan repository-wide regardless of the directory pytest is invoked from — a - package-relative path silently narrows the scan to whatever happens to be - below the caller. + package-relative path silently narrows the scan to whatever is below the + caller. """ for candidate in [Path(__file__).resolve(), *Path(__file__).resolve().parents]: if (candidate / "pyproject.toml").is_file(): @@ -52,11 +70,15 @@ def _repo_root() -> Path: ) -def _planned_sites() -> list[tuple[str, int, str, str]]: - """Every line where the §6 term and a ticket reference are co-located. +def _deferral_sites() -> list[tuple[str, int, str, str]]: + """Every forward-looking claim paired with a ticket. - A ``Planned`` with no ticket on the line is prose continuation, not a - referent. Lowercase ``planned`` is ordinary English and is not the §6 term. + The ticket is looked for on the claim's own line **and the line after it**. + One line is not enough: ``test/unit/test_quickstart_negative_control.py`` + wraps ``Planned`` and its ticket onto separate lines, and the first version + of this scan silently skipped it while the PR claimed it was covered. + Whether a site is checked must not depend on where a comment happens to + wrap. """ root = _repo_root() sites: list[tuple[str, int, str, str]] = [] @@ -64,41 +86,51 @@ def _planned_sites() -> list[tuple[str, int, str, str]]: for path in sorted(root.rglob("*")): if not path.is_file() or path.suffix not in _SCANNED_SUFFIXES: continue - if _SKIPPED_DIRS.intersection(path.relative_to(root).parts): + + rel = path.relative_to(root) + if _SKIPPED_DIRS.intersection(rel.parts) or str(rel) == _GATE_FILE: continue - for lineno, line in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), start=1): - if not _PLANNED_TERM.search(line): + lines = path.read_text(encoding="utf-8", errors="replace").splitlines() + for index, line in enumerate(lines): + if not _FORWARD_CLAIM.search(line): continue - ticket = _TICKET_REF.search(line) + + window = line + if index + 1 < len(lines): + window = f"{line}\n{lines[index + 1]}" + + ticket = _TICKET_REF.search(window) if ticket is None: continue - sites.append((str(path.relative_to(root)), lineno, ticket.group(0), line.strip())) - return sites + sites.append((str(rel), index + 1, ticket.group(0), line.strip())) + return sites -def test_scan_reaches_the_planned_referent_sites() -> None: - """Positive control on the scan itself. - Without it, a walk that reached no files — a broken root, a changed suffix - set, an over-broad skip list — reports the same clean result as a tree with - every referent correct. - """ - sites = _planned_sites() - assert len(sites) >= PLANNED_REFERENT_FLOOR, ( - f"scan found {len(sites)} §6 Planned referent sites under {_repo_root()}, " - f"floor is {PLANNED_REFERENT_FLOOR}; either sites were removed without " - "revisiting this gate, or the scan stopped reaching them and is passing " - "over nothing" +def test_no_forward_claim_defers_to_a_closed_measurement_ticket() -> None: + wrong = [site for site in _deferral_sites() if site[2] in STALE_REFERENTS] + assert not wrong, "\n".join( + f"{path}:{lineno} defers to {ticket}, which measured the gap and will " + f"not fix it — use the ticket that builds the sink (AAASM-5750, per its " + f"own description): {text}" + for path, lineno, ticket, text in wrong ) -def test_planned_references_the_capability_ticket() -> None: - wrong = [site for site in _planned_sites() if site[2] != CAPABILITY_REFERENT] - assert not wrong, "\n".join( - f"{path}:{lineno} references {ticket} as the §6 Planned referent, want " - f"{CAPABILITY_REFERENT} (the ticket that builds the sink, not one that " - f"measured its absence): {text}" - for path, lineno, ticket, text in wrong +def test_every_expected_site_is_still_reachable() -> None: + """Anti-vacuity, and the reason it names paths rather than counting. + + A count can be held up by an unrelated site appearing as a real one is + deleted. Naming them makes that substitution visible. + """ + seen = {site[0] for site in _deferral_sites()} + missing = [path for path in EXPECTED_SITES if path not in seen] + assert not missing, "\n".join( + f"{path} carries no forward claim the scan can pair with a ticket; it " + f"was deleted, renamed, or reflowed so the term and the ticket are more " + f"than one line apart — in which case a stale referent there would no " + f"longer be checked" + for path in missing ) From 1632ec9cad8353cd6a164148bbf89dfe263682dc Mon Sep 17 00:00:00 2001 From: Bryant Date: Thu, 13 Aug 2026 18:05:07 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E2=9C=85=20(core):=20Hold=20guarded=20site?= =?UTF-8?q?s=20to=20the=20capability=20ticket,=20not=20just=20off=20the=20?= =?UTF-8?q?stale=20ones?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-2 review: narrowing the invariant to a two-element denylist stopped the gate asserting the thing the change actually made true. Repointing a guarded site to ANY non-stale ticket passed green — including a real sibling ticket: ffi_governance_client.go:169 AAASM-5750 -> AAASM-5749 -> ok The likeliest drift — AAASM-5750 gets split or superseded and someone updates the referent — was undetected. Round 1 was too broad, round 2 too narrow. Two tiers, so neither failure mode returns: * a **guarded** site (one of the named audit-sink deferrals) must name AAASM-5750 exactly; * **any other** site must merely not name a stale referent, so an unrelated roadmap row is still legitimate. The positive assertion is scoped to a named set, so it carries none of round 1's repo-wide over-breadth. Also fixed: the one-line lookahead had no directionality, and created a new false-positive class the same-line rule could not. A line of forward-looking prose inserted above a correct BACKWARD citation was blamed for it: 153: // Streaming record delivery is Planned for a later release. 154: // RecordResult discards the record and reports success (AAASM-5731). FAIL: ...:153 defers to AAASM-5731 The window now extends only when the claim line carries no ticket of its own AND does not end a sentence. There are 81 backward citations across the three SDKs — go 30, python 33, node 18 — each a landmine for any adjacent claim prose. Zero are adjacent today, so this was green by luck of layout. Two residual limits are disclosed in the header rather than papered over: the reachability check is per file rather than per site, and the excluded gate file is a hiding place. Both measured as currently unreachable. --- test/unit/core/test_planned_referent.py | 81 ++++++++++++++++++++----- 1 file changed, 66 insertions(+), 15 deletions(-) diff --git a/test/unit/core/test_planned_referent.py b/test/unit/core/test_planned_referent.py index 57caf0e9..44ccb780 100644 --- a/test/unit/core/test_planned_referent.py +++ b/test/unit/core/test_planned_referent.py @@ -13,12 +13,30 @@ cited as the ticket that measured the gap, never as the ticket that will fix it.** -Deliberately NOT asserted: that every ``Planned`` in this repository names -AAASM-5750. §6 scopes ``Planned`` to any decided-but-unbuilt capability with any -ticket, so an unrelated roadmap row — including -``docs/examples/framework-support.md``'s docs-area maturity label, a different -axis entirely — is legitimate and must not fail this gate. The first version of -this test made exactly that over-broad assertion. +The assertion is two-tier, because one tier alone fails in one direction or the +other and review caught both: + +* a **guarded** site (one of :data:`EXPECTED_SITES`) must name AAASM-5750 + exactly. Without this the gate stops asserting the thing the change made true + — repointing a guarded site to any other live ticket passed green, which is + precisely the drift the gate exists to catch. +* **any other** site must merely not name a stale referent. Asserting + AAASM-5750 repository-wide was the first version's defect: §6 scopes + ``Planned`` to any decided-but-unbuilt capability with any ticket, so an + unrelated roadmap row — including + ``docs/examples/framework-support.md``'s docs-area maturity label, a + different axis entirely — is legitimate and must not fail this gate. + +Two limits are disclosed rather than fixed, both measured as currently +unreachable: + +* the reachability check is per **file**, not per site. A guarded file that + reflowed its real site out of the scan's reach *and* gained a second, correct + claim would keep its entry. Requires two coordinated edits; today no file in + this repository carries more than one site. +* the gate file is excluded from its own scan, so it is a hiding place for a + stale referent. It is a test file that documents no SDK behaviour, and the + exclusion matches one exact path rather than a prefix. """ from __future__ import annotations @@ -26,6 +44,10 @@ import re from pathlib import Path +#: The ticket that owns building the SDK-side audit sink. Guarded sites must +#: name it exactly. +CAPABILITY_REFERENT = "AAASM-5750" + #: Tickets that *measured* the absence of an SDK-side audit sink. Backward #: citations to them are correct and are left alone; what this gate forbids is #: either one appearing as the ticket a forward-looking claim defers to. @@ -70,6 +92,16 @@ def _repo_root() -> Path: ) +def _ends_sentence(line: str) -> bool: + """Whether a comment line closes a sentence. + + A wrapped sentence (``… Planned under ADR 0033 §6`` / ``(AAASM-5750) …``) + does not; a complete one does. + """ + trimmed = line.strip().rstrip("#* ") + return bool(trimmed) and trimmed[-1] in ".!?" + + def _deferral_sites() -> list[tuple[str, int, str, str]]: """Every forward-looking claim paired with a ticket. @@ -96,8 +128,14 @@ def _deferral_sites() -> list[tuple[str, int, str, str]]: if not _FORWARD_CLAIM.search(line): continue + # Extend to the next line only when this line carries no ticket of + # its own AND does not end a sentence. Without the sentence guard + # the window pairs a claim with a ticket belonging to the *next* + # sentence — review produced a real case where an inserted line of + # forward-looking prose was blamed for a correct backward citation + # beneath it. There are 33 such backward citations in this repo. window = line - if index + 1 < len(lines): + if not _TICKET_REF.search(line) and not _ends_sentence(line) and index + 1 < len(lines): window = f"{line}\n{lines[index + 1]}" ticket = _TICKET_REF.search(window) @@ -109,14 +147,27 @@ def _deferral_sites() -> list[tuple[str, int, str, str]]: return sites -def test_no_forward_claim_defers_to_a_closed_measurement_ticket() -> None: - wrong = [site for site in _deferral_sites() if site[2] in STALE_REFERENTS] - assert not wrong, "\n".join( - f"{path}:{lineno} defers to {ticket}, which measured the gap and will " - f"not fix it — use the ticket that builds the sink (AAASM-5750, per its " - f"own description): {text}" - for path, lineno, ticket, text in wrong - ) +def test_forward_claims_name_the_right_ticket() -> None: + guarded = set(EXPECTED_SITES) + problems = [] + + for path, lineno, ticket, text in _deferral_sites(): + if path in guarded: + if ticket != CAPABILITY_REFERENT: + problems.append( + f"{path}:{lineno} is a guarded audit-sink deferral and must " + f"name {CAPABILITY_REFERENT}, not {ticket} — this is the site " + f"the referent change corrected, and letting it drift to any " + f"other ticket is what this gate exists to prevent: {text}" + ) + elif ticket in STALE_REFERENTS: + problems.append( + f"{path}:{lineno} defers to {ticket}, which measured the gap and " + f"will not fix it — use the ticket that builds the sink " + f"(AAASM-5750, per its own description): {text}" + ) + + assert not problems, "\n".join(problems) def test_every_expected_site_is_still_reachable() -> None: