7676# Below this, the ledger did not parse and no verdict from this gate is evidence.
7777MIN_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
80122def 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 ))
0 commit comments