Skip to content

Commit 488ef2d

Browse files
authored
Merge branch 'main' into claude/builder-1-1337-citation-screen
2 parents e969be4 + 0ab3070 commit 488ef2d

2 files changed

Lines changed: 194 additions & 0 deletions

File tree

scripts/coord/dispatch_gate.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,48 @@
7676
# Below this, the ledger did not parse and no verdict from this gate is evidence.
7777
MIN_ITEMS = 50
7878

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

80122
def load_items(root: Path) -> dict[int, Item]:
81123
"""Every item across the ledger namespace, keyed by number."""
@@ -101,6 +143,11 @@ def judge(item: Item) -> tuple[str, str]:
101143
102144
So the rule is NAME THE CLOSING ACT, NEVER REFUSE IT. The only ``refuse`` left is an item whose
103145
state nobody has declared, because there the dispatch cannot name anything at all.
146+
147+
**The gated verdicts ADVISE, they do not refuse** -- the same correction governs them. A
148+
``demand-gate`` item can legitimately be scoped or researched; what it cannot be is silently
149+
treated as ordinary build work. So the note leads with what gates it and who lifts it, and the
150+
seat still decides.
104151
"""
105152
act = item.fields.get("closing-act", "").strip().lower()
106153
verdict = item.fields.get("verdict", "").strip().lower()
@@ -114,6 +161,20 @@ def judge(item: Item) -> tuple[str, str]:
114161
)
115162

116163
notes: list[str] = []
164+
165+
# THIS BRANCH GOES FIRST, and the order is load-bearing rather than cosmetic. The closing-act
166+
# note below ends "That is a complete outcome, not a failure." Left to lead, it tells the reader
167+
# of a demand-gate item that shipping the code finishes the job -- the exact opposite of what a
168+
# demand gate means. test_the_gated_note_leads pins the ordering, because a comment cannot.
169+
#
170+
# AND IT IS UNCONDITIONAL ON `Research:`, deliberately NOT mirroring the research branch's
171+
# `research in ("", "none")` guard below. A demand gate is not lifted by finishing research; it
172+
# is lifted by a ruling. Copying that guard is the plausible wrong fix and it re-greens the item
173+
# the moment someone records a completed pass -- which is why two tests here drive these
174+
# verdicts WITH research done.
175+
if verdict in GATED_VERDICTS:
176+
notes.append(f"Verdict is {verdict!r} -- DO NOT JUST BUILD IT: {GATED_VERDICTS[verdict]}")
177+
117178
if act not in BUILDER_CLOSABLE_ACTS:
118179
who = CLOSING_SEAT.get(act, "a seat this tool does not know")
119180
notes.append(
@@ -166,6 +227,27 @@ def mk(fields: dict[str, str]) -> Item:
166227
"ok",
167228
"research done, closing act is code",
168229
),
230+
(
231+
{"closing-act": "code", "verdict": "demand-gate", "research": "none"},
232+
"advise",
233+
"a demand gate means the DEMAND is unproven -- shipping the code does not close it",
234+
),
235+
(
236+
{"closing-act": "code", "verdict": "demand-gate", "research": "done 2026-08-20"},
237+
"advise",
238+
"THE DISCRIMINATOR: a demand gate is lifted by a RULING, never by finished research, so "
239+
"mirroring the research branch's guard here would wrongly re-green this",
240+
),
241+
(
242+
{"closing-act": "code", "verdict": "owner-ruling", "research": "none"},
243+
"advise",
244+
"an owner-ruling verdict routes the scope question to the owner before anyone builds",
245+
),
246+
(
247+
{"closing-act": "code", "verdict": "owner-ruling", "research": "done 2026-08-20"},
248+
"advise",
249+
"DISCRIMINATOR TWIN: research done does not answer a question routed to the owner",
250+
),
169251
]
170252
for fields, want, why in cases:
171253
got, reason = judge(mk(fields))

tests/test_coord_dispatch_gate.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,115 @@ def test_an_item_absent_from_the_ledger_is_reported(
222222
assert gate.main(["999999", "--root", str(_ROOT)]) == 0
223223
assert "NOT IN THE LEDGER" in capsys.readouterr().out
224224
assert gate.main(["999999", "--root", str(_ROOT), "--refuse"]) == 1
225+
226+
227+
# ------------------------------------------------------------------ gated verdicts (BACKLOG #1334)
228+
#
229+
# `judge()` tested exactly ONE verdict value. `demand-gate` and `owner-ruling` -- the two that mean
230+
# DO NOT JUST BUILD IT -- fell through and were green-lit as ordinary build work.
231+
#
232+
# WHAT THESE TESTS ARE AND ARE NOT ABOUT. They pin the READER, not the data. A banner that declares
233+
# the wrong verdict is invisible to any of this, which is why the fix could not have caught #1336:
234+
# that row read `Verdict: build` while an owner ruling 105 lines below said otherwise.
235+
236+
237+
def test_a_demand_gate_verdict_is_advised_not_green(gate: ModuleType) -> None:
238+
level, reason = gate.judge(
239+
_item(gate, **{"closing-act": "code", "verdict": "demand-gate", "research": "none"})
240+
)
241+
assert level == "advise"
242+
# The LEVEL alone is a weak assertion: an over-broad fix that advises every non-build verdict
243+
# would also produce it. The reason text is the only thing that tells a seat what gates the item.
244+
assert "DO NOT JUST BUILD IT" in reason
245+
assert "LIAISON" in reason
246+
247+
248+
def test_a_demand_gate_verdict_stays_advised_when_research_is_done(gate: ModuleType) -> None:
249+
"""THE DISCRIMINATOR. A demand gate is lifted by a RULING, never by finished research.
250+
251+
The plausible wrong fix mirrors the research branch's ``research in ("", "none")`` guard. That
252+
passes the test above and fails this one, re-greening the item the moment somebody records a
253+
completed pass.
254+
"""
255+
level, reason = gate.judge(
256+
_item(
257+
gate,
258+
**{"closing-act": "code", "verdict": "demand-gate", "research": "done 2026-08-20"},
259+
)
260+
)
261+
assert level == "advise"
262+
assert "DO NOT JUST BUILD IT" in reason
263+
264+
265+
def test_an_owner_ruling_verdict_is_advised_not_green(gate: ModuleType) -> None:
266+
level, reason = gate.judge(
267+
_item(gate, **{"closing-act": "code", "verdict": "owner-ruling", "research": "none"})
268+
)
269+
assert level == "advise"
270+
assert "DO NOT JUST BUILD IT" in reason
271+
assert "owner" in reason.lower()
272+
assert "LIAISON" in reason
273+
274+
275+
def test_an_owner_ruling_verdict_stays_advised_when_research_is_done(gate: ModuleType) -> None:
276+
"""Discriminator twin. Research done does not answer a question routed to the owner."""
277+
level, _ = gate.judge(
278+
_item(
279+
gate,
280+
**{"closing-act": "code", "verdict": "owner-ruling", "research": "done 2026-08-20"},
281+
)
282+
)
283+
assert level == "advise"
284+
285+
286+
def test_a_plain_build_verdict_is_still_green(gate: ModuleType) -> None:
287+
"""The opposite direction: this must NOT become a blanket advisory.
288+
289+
A NOTE ON WHAT THIS DOES AND DOES NOT CATCH, because the obvious claim is wrong. It does catch a
290+
blanket advise. It does NOT catch ``if verdict != "build"`` -- that variant leaves build items
291+
untouched, so this test passes over it. The guard against THAT shape is
292+
``test_completed_research_with_a_code_closing_act_passes``, which goes red under it. Do not trim
293+
that test as redundant, and see the mutation note in the module docstring above.
294+
"""
295+
level, reason = gate.judge(
296+
_item(gate, **{"closing-act": "code", "verdict": "build", "research": "none"})
297+
)
298+
assert level == "ok"
299+
assert "DO NOT JUST BUILD IT" not in reason
300+
301+
302+
def test_the_gated_note_leads(gate: ModuleType) -> None:
303+
"""Ordering is load-bearing, so it is asserted rather than left to a comment.
304+
305+
The closing-act note ends "That is a complete outcome, not a failure." Left to lead, it tells the
306+
reader of a gated item that shipping the code finishes the job -- the opposite of what a demand
307+
gate means.
308+
"""
309+
_, reason = gate.judge(
310+
_item(
311+
gate,
312+
**{"closing-act": "scorecard-rescore", "verdict": "demand-gate", "research": "none"},
313+
)
314+
)
315+
assert "DO NOT JUST BUILD IT" in reason
316+
assert "complete outcome" in reason, "precondition: both notes must be present to order them"
317+
assert reason.index("DO NOT JUST BUILD IT") < reason.index("complete outcome")
318+
319+
320+
def test_the_gated_verdicts_are_a_subset_of_the_closed_vocabulary(gate: ModuleType) -> None:
321+
"""Makes the constant's own comment executable instead of merely true when written.
322+
323+
``GATED_VERDICTS`` is dispatch policy and lives here; the closed verdict vocabulary lives in
324+
``verdict_divergence_check.py``. A typo here would silently gate nothing, and every test above
325+
would still pass because they all drive ``judge()`` with the same spelling this module defines.
326+
Reaching for the private ``_VERDICTS`` is deliberate: a second copy of the vocabulary is the
327+
defect, not the fix.
328+
"""
329+
checker = _load(
330+
Path(__file__).resolve().parents[1] / "scripts" / "docs" / "verdict_divergence_check.py",
331+
"verdict_divergence_check_for_gate_test",
332+
)
333+
assert set(gate.GATED_VERDICTS) <= set(checker._VERDICTS), (
334+
f"GATED_VERDICTS has a value the ledger vocabulary does not know: "
335+
f"{set(gate.GATED_VERDICTS) - set(checker._VERDICTS)}"
336+
)

0 commit comments

Comments
 (0)