decideSpecRoute function
The SPEC-route matrix (pure — zero I/O; the whole decision, unit-testable).
In order:
- the GATING lane at
F(a structurally broken spec; a MISSING gating grade fail-closes here too) ⇒ SpecEscalate — a deterministic structural block is not an "actionable critic grade with a rationale"; a human rules. Its reason NAMES the gating lane (ADR-0000 A13(5)'s invariant, preserved in the fork). - any NON-gating lane at
F⇒ SpecEscalate. AnFis the scope/decompose-class judgement (ADR-0000 A13(4): the station has no separate decompose verdict — its semantics ride this gate), or a verdict-transport miss whose fail-closedFcarries no usable rationale. Neither is respec-fixable. - the FIXABLE set = every non-gating lane at
DorE. Empty ⇒ SpecAdvance — INCLUDING whenpriorRoundhas already reachedmaxRounds. A CONVERGED join advances no matter how many rounds were consumed: the cap bounds the LOOP, it never condemns the spec (beadpow-p8w). - a fixable lane with an EMPTY rationale ⇒ SpecEscalate (
no-rationale) — there is nothing to feed the re-specify agent, and a respec that re-runsspecifywith no guidance would re-write the same spec and re-park. LOUD. - a fixable join containing ANY author-owed lane ⇒ SpecEscalate
(
author-owed) IMMEDIATELY, with the correction bound unspent and every author-owed rationale quoted verbatim for the governor. A blank or unrecognised owner reads as architect-owed — today's behaviour; the LOUD guard against a missing owner is the DECODER (requireOwner), which refuses a live D/E naming no owner at all. - TWO OR MORE fixable lanes ⇒ SpecEscalate (
multi-action-grade) — the round did not converge on a single carriable finding, so a human rules (beadpow-bhm, policy Nico-ratified 2026-07-18: "hard-gate ONLY on F or on two-plus D lanes"). The auto-respec bound is NOT spent here. - a SINGLE architect-owed
DWITH a rationale ⇒ SpecAdvance carrying it as SpecAdvance.fixInFlight — the finding rides the build brief VERBATIM as a BINDING fix-in-flight item and the code committee re-checks it at review, so the catch keeps its value without spending a respec cycle (beadpow-bhm). A singleEfalls through to arm 8:Eis the band aboveD, and the ratified rule namesDalone. - a fixable join UNDER the bound (
priorRound<maxRounds) ⇒ SpecRespec carrying the current circuit round. - else — a join that is fixable RIGHT NOW, at the bound ⇒ SpecEscalate
(
respec-cap). This arm is the FALL-THROUGH of arm 8, so the cap is the conjunction "the rounds are spent AND the CURRENT join still fails" by STRUCTURE rather than by a second read, and its reason quotes the same fresh vector the matrix just decided on — never a ledger-recorded last failure. (decideDiscovery's regather arm carries the identical shape.)
The spread rule is GONE from the spec route (and Step 4's pow-bhm cut
removed it from CodeRouteCapability as well, for the identical reason). A
spread ≥ 3 across A..F necessarily
puts
some lane at D or worse, so arms 2/3 already cover every case it caught —
and what it did with them ("human ultimatum") is exactly what this bead exists
to stop doing by default. The spread still rides SpecAdvance as provenance.
Recorded as
power_station#a14-bead-pow-7nm-the-spec-route-auto-respecs-a-fixable-spec
clause (4), extended to the code route by the pow-bhm decision entry
committee-gate-single-finding-advance-and-the-refinement-flag.
Arm numbering is a CITED contract. Bead pow-s2t (rework can DECOMPOSE)
will hang its breakdown disposition off the BOUND arms — today arms 8
(SpecRespec) and 9 (respec-cap), renumbered from 6 and 7 by bead
pow-bhm. The threshold itself (kMaxRespecRounds) is unmoved and the cap
arm's rule string is still respec-cap; only the ordinals shifted.
Implementation
SpecRouteVerdict decideSpecRoute({
required List<SpecLane> lanes,
required String gating,
required String sessionRoot,
required int priorRound,
int maxRounds = kMaxRespecRounds,
}) {
String gradeOf(SpecLane lane) =>
(lane.grade == null || lane.grade!.trim().isEmpty)
? 'F'
: lane.grade!.trim().toUpperCase();
String ownerOf(SpecLane lane) => lane.owner.trim().toLowerCase();
// The FRESH grade vector — the ONE verdict source this matrix decides on, in
// `critics` order. Every arm that REPORTS a grade quotes this binding, so a
// flare can never cite a grade the decision did not read (bead `pow-p8w`: the
// cap flare and the respec decision must share one channel).
final gradesCsv = lanes.map((l) => '${l.id}=${gradeOf(l)}').join(',');
// 1. the deterministic structural lane — a hard block (fail-closed on missing).
final gate = lanes.where((l) => l.id == gating);
final gatingGrade = gate.isEmpty ? 'F' : gradeOf(gate.first);
if (gatingGrade == 'F') {
final why = gate.isEmpty || gate.first.rationale.trim().isEmpty
? ''
: ' — ${gate.first.rationale.trim()}';
return SpecEscalate(
rule: 'gating-hard-block',
reason: '$gating failed: hard block$why',
);
}
final judged = [
for (final l in lanes)
if (l.id != gating) l,
];
// 2. any judgement lane at F — a scope/decompose-class ruling, or a transport
// miss. A human decides; never an auto-respec.
final hard = [
for (final l in judged)
if (gradeOf(l) == 'F') l,
];
if (hard.isNotEmpty) {
return SpecEscalate(
rule: 'critic-F',
reason:
'a spec critic returned F (${hard.map((l) => l.id).join(', ')}) — an F '
'is not a fixable nit (a wrong scope, a bead that needs decomposing, '
'or a missing verdict that fail-closed). A human rules; auto-respec is '
'withheld.',
);
}
// 3. the FIXABLE set — an actionable critic grade.
final fixable = [
for (final l in judged)
if (gradeOf(l) == 'D' || gradeOf(l) == 'E') l,
];
// The spread is PROVENANCE only (A14(4)) — no arm decides on it. Hoisted out
// of the converged arm so the single-finding advance (arm 7) reports it too.
final indices = [
for (final l in lanes)
if (l.grade != null && l.grade!.trim().isNotEmpty)
_gradeIndex(gradeOf(l)),
];
final spread = indices.isEmpty
? 0
: indices.reduce((a, b) => a > b ? a : b) -
indices.reduce((a, b) => a < b ? a : b);
if (fixable.isEmpty) {
return SpecAdvance(gradesCsv: gradesCsv, spread: spread);
}
// 4. a fixable grade with NO rationale — nothing to correct against. LOUD.
final mute = [
for (final l in fixable)
if (l.rationale.trim().isEmpty) l,
];
if (mute.isNotEmpty) {
return SpecEscalate(
rule: 'no-rationale',
reason:
'a spec critic graded '
'${mute.map((l) => '${l.id}=${gradeOf(l)}').join(', ')} but returned '
'NO rationale — there is no correction guidance to respec with, so an '
'auto-respec would rewrite the same spec blind. A human rules.',
);
}
// 5. AUTHOR-OWED — the fix is a decision only the bead's AUTHOR can make, so
// an auto-respec round is UNWINNABLE by construction: `specify` re-runs
// with the same bead text and re-draws the same grade (the receipt:
// pow-n6n.1 burned both rounds on a coherence D that asked the architect
// to choose between its own names and pow-n6n.2's). Park IMMEDIATELY,
// spending NO round of the bound, and hand the governor the fork verbatim
// so the bead BODY is cured once. ANY author-owed lane decides the join:
// the human is already in the loop, and the architect-owed lanes ride the
// same rework wave for free. Bead `pow-hxme`'s ADR-0000 A37 records
// this refinement of ratified A14(3).
final authorOwed = [
for (final l in fixable)
if (ownerOf(l) == kOwnerAuthor) l,
];
if (authorOwed.isNotEmpty) {
final b = StringBuffer()
..write(
'author-owed: '
'${authorOwed.map((l) => '${l.id}=${gradeOf(l)}').join(', ')} — the '
'critic marked the fix AUTHOR-owed: it needs a decision the BEAD TEXT '
'does not make (which names win, whether the scope splits, which of '
'two designs), not a spec correction the architect can derive from the '
'tree. Auto-respec is WITHHELD with the bound UNSPENT ($priorRound of '
'$maxRounds round(s) used): re-running `specify` cannot decide it. '
'Cure the bead BODY once, then resolve this gate. The current join is '
'$gradesCsv.',
);
for (final l in authorOwed) {
b.write('\n\n### ${l.id} — grade ${gradeOf(l)}\n${l.rationale.trim()}');
}
return SpecEscalate(rule: 'author-owed', reason: b.toString());
}
// 6. TWO OR MORE action grades — the round did not converge on a single
// finding, so it GATES (bead `pow-bhm`, policy Nico-ratified 2026-07-18:
// "hard-gate ONLY on F or on two-plus D lanes"). The auto-respec loop is
// NOT spent here: a multi-lane join is the shape a human reads once, and
// the governor's rework is one command. A29(2): the flare quotes the FRESH
// vector this matrix decided on.
if (fixable.length >= 2) {
final b = StringBuffer()
..write(
'multi-action-grade: '
'${fixable.map((l) => '${l.id}=${gradeOf(l)}').join(', ')} — two or '
'more lanes returned an action grade, so the round did not converge on '
'a single carriable finding. The current join is $gradesCsv.',
);
for (final l in fixable) {
b.write('\n\n### ${l.id} — grade ${gradeOf(l)}\n${l.rationale.trim()}');
}
return SpecEscalate(rule: 'multi-action-grade', reason: b.toString());
}
// 7. a SINGLE architect-owed D WITH a rationale — ADVANCE, carrying the
// finding VERBATIM to the build brief as a BINDING fix-in-flight item.
// The receipt (bead `pow-bhm`): `tg-h4u` burned two substantive spec
// rounds on a single sub-C lane, one of them a decision-alignment D whose
// load-bearing defect was a citation misattribution. Every catch keeps its
// value — the finding rides the build and the code committee re-checks it
// — and the respec cycle is not spent. A single E still respecs: E is the
// band above D, and the ratified rule names D alone.
final only = fixable.single;
if (gradeOf(only) == 'D') {
return SpecAdvance(
gradesCsv: gradesCsv,
spread: spread,
fixInFlight: RespecLane(
rubric: only.id,
grade: gradeOf(only),
rationale: only.rationale.trim(),
),
);
}
// 8. RESPEC — a fixable join UNDER the bound auto-loops with the failing
// lanes' rationales as the correction guidance.
if (priorRound < maxRounds) {
return SpecRespec(
RespecLedger(
sessionRoot: sessionRoot,
round: priorRound,
lanes: [
for (final l in fixable)
RespecLane(
rubric: l.id,
grade: gradeOf(l),
rationale: l.rationale.trim(),
),
],
),
);
}
// 9. the BOUND — reachable ONLY as arm 8's fall-through, i.e. over a join that
// is fixable RIGHT NOW (arm 3 already advanced a converged one). So the cap
// IS "rounds spent AND the current join still fails", and the reason quotes
// `fixable` plus the whole fresh `gradesCsv` the matrix decided on — a human
// reading the parked gate can check the cited grade against the critique on
// disk without trusting a second channel.
return SpecEscalate(
rule: 'respec-cap',
reason:
'respec-cap: $priorRound auto-respec round(s) already ran (cap '
'$maxRounds) and the spec STILL fails '
'(${fixable.map((l) => '${l.id}=${gradeOf(l)}').join(', ')}) — the '
'current join is $gradesCsv. Flaring to a human: the committee and the '
'specify agent are not converging.',
);
}