Skip to content
82 changes: 82 additions & 0 deletions scripts/coord/dispatch_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,48 @@
# Below this, the ledger did not parse and no verdict from this gate is evidence.
MIN_ITEMS = 50

# VERDICTS THAT MEAN DO NOT JUST BUILD IT, and what lifts each (BACKLOG #1334).
#
# This is DISPATCH POLICY -- what a verdict says about STARTING work -- not ledger parsing, so it
# lives here rather than in the shared parser. The closed verdict vocabulary itself is ``_VERDICTS``
# in ``scripts/docs/verdict_divergence_check.py``; these keys must stay a SUBSET of it, and
# ``test_the_gated_verdicts_are_a_subset_of_the_closed_vocabulary`` enforces that rather than
# trusting this sentence.
#
# WHY THIS EXISTS -- and read the second paragraph, because the obvious story is WRONG.
#
# This gate green-lit BACKLOG #1336 to a dispatcher on 2026-08-24. #1336 was not startable: an owner
# ruling in its body put a shell tokeniser out of scope with no fifth candidate, and that ruling sat
# ~105 lines BELOW the banner block this gate reads. The item was dispatched, and a builder lost a
# slot to it.
#
# THIS CHANGE WOULD NOT HAVE CAUGHT IT, and saying otherwise would be a false justification on a true
# observation. Measured at 883f7734^, #1336's banner read `Verdict: build, Closing-act: code` -- so
# this branch, which keys on the VERDICT FIELD, returns ``ok`` on it exactly as the old code did. The
# banner was WRONG, and a reader of the banner cannot detect a wrong banner. What fixed #1336 was a
# person reading the body and correcting the row (PR 578). The two are COMPLEMENTARY: that corrected
# the DATA, this corrects the READER, and neither substitutes for the other.
#
# WHAT THIS DOES BUY, stated at its real size. 31 items on the ledger at 883f7734 declare a gated
# verdict. Every one of them is ALREADY ``advise`` on its closing act, so on today's corpus this
# changes NO LEVEL -- it changes the REASON for 31 items, from one naming only who closes them to one
# naming what gates them and who lifts it. The level arm is real and unexercised: it fires the moment
# a gated-verdict item carries a ``code`` closing act, which today's 31 do not (29 close by
# owner-ruling, 2 by blocked). The self-test drives that case directly rather than waiting for the
# ledger to produce one.
GATED_VERDICTS = {
"demand-gate": (
"the DEMAND is unproven, not the design -- nobody has ruled that this should exist. "
"Scoping and research are legitimate. SHIPPING THE CODE IS NOT A COMPLETE OUTCOME here, "
"because the gate is lifted by an owner ruling via the LIAISON, never by a merge."
),
"owner-ruling": (
"the SCOPE question belongs to the owner and is routed as one. Do not build a candidate "
"before it is answered; the ruling comes via the LIAISON, and the Dispatcher or Lander "
"records it."
),
}


def load_items(root: Path) -> dict[int, Item]:
"""Every item across the ledger namespace, keyed by number."""
Expand All @@ -101,6 +143,11 @@ def judge(item: Item) -> tuple[str, str]:

So the rule is NAME THE CLOSING ACT, NEVER REFUSE IT. The only ``refuse`` left is an item whose
state nobody has declared, because there the dispatch cannot name anything at all.

**The gated verdicts ADVISE, they do not refuse** -- the same correction governs them. A
``demand-gate`` item can legitimately be scoped or researched; what it cannot be is silently
treated as ordinary build work. So the note leads with what gates it and who lifts it, and the
seat still decides.
"""
act = item.fields.get("closing-act", "").strip().lower()
verdict = item.fields.get("verdict", "").strip().lower()
Expand All @@ -114,6 +161,20 @@ def judge(item: Item) -> tuple[str, str]:
)

notes: list[str] = []

# THIS BRANCH GOES FIRST, and the order is load-bearing rather than cosmetic. The closing-act
# note below ends "That is a complete outcome, not a failure." Left to lead, it tells the reader
# of a demand-gate item that shipping the code finishes the job -- the exact opposite of what a
# demand gate means. test_the_gated_note_leads pins the ordering, because a comment cannot.
#
# AND IT IS UNCONDITIONAL ON `Research:`, deliberately NOT mirroring the research branch's
# `research in ("", "none")` guard below. A demand gate is not lifted by finishing research; it
# is lifted by a ruling. Copying that guard is the plausible wrong fix and it re-greens the item
# the moment someone records a completed pass -- which is why two tests here drive these
# verdicts WITH research done.
if verdict in GATED_VERDICTS:
notes.append(f"Verdict is {verdict!r} -- DO NOT JUST BUILD IT: {GATED_VERDICTS[verdict]}")

if act not in BUILDER_CLOSABLE_ACTS:
who = CLOSING_SEAT.get(act, "a seat this tool does not know")
notes.append(
Expand Down Expand Up @@ -166,6 +227,27 @@ def mk(fields: dict[str, str]) -> Item:
"ok",
"research done, closing act is code",
),
(
{"closing-act": "code", "verdict": "demand-gate", "research": "none"},
"advise",
"a demand gate means the DEMAND is unproven -- shipping the code does not close it",
),
(
{"closing-act": "code", "verdict": "demand-gate", "research": "done 2026-08-20"},
"advise",
"THE DISCRIMINATOR: a demand gate is lifted by a RULING, never by finished research, so "
"mirroring the research branch's guard here would wrongly re-green this",
),
(
{"closing-act": "code", "verdict": "owner-ruling", "research": "none"},
"advise",
"an owner-ruling verdict routes the scope question to the owner before anyone builds",
),
(
{"closing-act": "code", "verdict": "owner-ruling", "research": "done 2026-08-20"},
"advise",
"DISCRIMINATOR TWIN: research done does not answer a question routed to the owner",
),
]
for fields, want, why in cases:
got, reason = judge(mk(fields))
Expand Down
112 changes: 112 additions & 0 deletions tests/test_coord_dispatch_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,115 @@ def test_an_item_absent_from_the_ledger_is_reported(
assert gate.main(["999999", "--root", str(_ROOT)]) == 0
assert "NOT IN THE LEDGER" in capsys.readouterr().out
assert gate.main(["999999", "--root", str(_ROOT), "--refuse"]) == 1


# ------------------------------------------------------------------ gated verdicts (BACKLOG #1334)
#
# `judge()` tested exactly ONE verdict value. `demand-gate` and `owner-ruling` -- the two that mean
# DO NOT JUST BUILD IT -- fell through and were green-lit as ordinary build work.
#
# WHAT THESE TESTS ARE AND ARE NOT ABOUT. They pin the READER, not the data. A banner that declares
# the wrong verdict is invisible to any of this, which is why the fix could not have caught #1336:
# that row read `Verdict: build` while an owner ruling 105 lines below said otherwise.


def test_a_demand_gate_verdict_is_advised_not_green(gate: ModuleType) -> None:
level, reason = gate.judge(
_item(gate, **{"closing-act": "code", "verdict": "demand-gate", "research": "none"})
)
assert level == "advise"
# The LEVEL alone is a weak assertion: an over-broad fix that advises every non-build verdict
# would also produce it. The reason text is the only thing that tells a seat what gates the item.
assert "DO NOT JUST BUILD IT" in reason
assert "LIAISON" in reason


def test_a_demand_gate_verdict_stays_advised_when_research_is_done(gate: ModuleType) -> None:
"""THE DISCRIMINATOR. A demand gate is lifted by a RULING, never by finished research.

The plausible wrong fix mirrors the research branch's ``research in ("", "none")`` guard. That
passes the test above and fails this one, re-greening the item the moment somebody records a
completed pass.
"""
level, reason = gate.judge(
_item(
gate,
**{"closing-act": "code", "verdict": "demand-gate", "research": "done 2026-08-20"},
)
)
assert level == "advise"
assert "DO NOT JUST BUILD IT" in reason


def test_an_owner_ruling_verdict_is_advised_not_green(gate: ModuleType) -> None:
level, reason = gate.judge(
_item(gate, **{"closing-act": "code", "verdict": "owner-ruling", "research": "none"})
)
assert level == "advise"
assert "DO NOT JUST BUILD IT" in reason
assert "owner" in reason.lower()
assert "LIAISON" in reason


def test_an_owner_ruling_verdict_stays_advised_when_research_is_done(gate: ModuleType) -> None:
"""Discriminator twin. Research done does not answer a question routed to the owner."""
level, _ = gate.judge(
_item(
gate,
**{"closing-act": "code", "verdict": "owner-ruling", "research": "done 2026-08-20"},
)
)
assert level == "advise"


def test_a_plain_build_verdict_is_still_green(gate: ModuleType) -> None:
"""The opposite direction: this must NOT become a blanket advisory.

A NOTE ON WHAT THIS DOES AND DOES NOT CATCH, because the obvious claim is wrong. It does catch a
blanket advise. It does NOT catch ``if verdict != "build"`` -- that variant leaves build items
untouched, so this test passes over it. The guard against THAT shape is
``test_completed_research_with_a_code_closing_act_passes``, which goes red under it. Do not trim
that test as redundant, and see the mutation note in the module docstring above.
"""
level, reason = gate.judge(
_item(gate, **{"closing-act": "code", "verdict": "build", "research": "none"})
)
assert level == "ok"
assert "DO NOT JUST BUILD IT" not in reason


def test_the_gated_note_leads(gate: ModuleType) -> None:
"""Ordering is load-bearing, so it is asserted rather than left to a comment.

The closing-act note ends "That is a complete outcome, not a failure." Left to lead, it tells the
reader of a gated item that shipping the code finishes the job -- the opposite of what a demand
gate means.
"""
_, reason = gate.judge(
_item(
gate,
**{"closing-act": "scorecard-rescore", "verdict": "demand-gate", "research": "none"},
)
)
assert "DO NOT JUST BUILD IT" in reason
assert "complete outcome" in reason, "precondition: both notes must be present to order them"
assert reason.index("DO NOT JUST BUILD IT") < reason.index("complete outcome")


def test_the_gated_verdicts_are_a_subset_of_the_closed_vocabulary(gate: ModuleType) -> None:
"""Makes the constant's own comment executable instead of merely true when written.

``GATED_VERDICTS`` is dispatch policy and lives here; the closed verdict vocabulary lives in
``verdict_divergence_check.py``. A typo here would silently gate nothing, and every test above
would still pass because they all drive ``judge()`` with the same spelling this module defines.
Reaching for the private ``_VERDICTS`` is deliberate: a second copy of the vocabulary is the
defect, not the fix.
"""
checker = _load(
Path(__file__).resolve().parents[1] / "scripts" / "docs" / "verdict_divergence_check.py",
"verdict_divergence_check_for_gate_test",
)
assert set(gate.GATED_VERDICTS) <= set(checker._VERDICTS), (
f"GATED_VERDICTS has a value the ledger vocabulary does not know: "
f"{set(gate.GATED_VERDICTS) - set(checker._VERDICTS)}"
)
Loading