Repository navigation
Denominate the enrolment margin in eval steps: the plan, and Phase 1's model - #11366
Conversation
… filed stall Opens the follow-up gunbc#11195 named: floor_enrolment_margin still derives a 302ms budget from required_floor_per_subject_cpu_line_ms and compares an observed CPU reading against it, so a per-subject budget still consumes a run-level figure. That is clause (iv) of the §4b(3) drop floor_cost_cpu_regression_at_constant_eval_steps and the only clause #11195 left standing. THE FIRST PHASE IS NOT THE ENROLMENT MARGIN, and that is the plan's main finding. floor_enrolment_margin consumes ClaimCostReading directly, every arm of which is CPU-shaped, so the gate cannot SEE a step count until the carrier carries one. The step count exists today only as FloorCostRow.eval_steps: a bare Int beside the coproduct. THE PHASE IS NOT INVENTED HERE. gunbc.guarantee_stall eval_steps_outside_the_reading_coproduct_stall already declares it: ceiling StructurallyImpossible, blocker ClimbableButUnbuilt, and a next_rung_trigger that reads "eval_steps moves INSIDE the ClaimCostReading arms, so an observed step count and an observed-so-far-before-censor count are reached through different constructors". This plan executes a trigger the corpus filed before this lane existed rather than minting a roadmap beside it. WHY INSIDE THE ARMS AND NOT A SIBLING COPRODUCT is argued rather than asserted: a sibling satisfies the trigger's letter while leaving an incoherent row constructible -- an OBSERVED cpu reading beside a BEFORE-CENSOR step count. Whether the claim reached its end is one fact about one occurrence and it discriminates both readings, so carrying the steps inside the arm is what reaches the stall's declared ceiling instead of stopping one rung below it. It is not clock fusion: no arm compares a step count to a millisecond. THE BLAST RADIUS IS MEASURED, NOT ESTIMATED -- 41 + 21 + 11 reading constructors and 41 FloorCostRow sites across 7 .dag files plus three seed files, on origin/main at 3a0f2d4. The type change admits no partial state, which is why this PR opens with the plan rather than a half-migration that would sit red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D8Hk99iz2Q1H32CydKF6YS
Executes the next_rung_trigger of gunbc.guarantee_stall
eval_steps_outside_the_reading_coproduct_stall verbatim: a performed count and an
observed-so-far-before-censor count are now reached through DIFFERENT
CONSTRUCTORS, so no fold can compare them without first matching which it holds.
The stall is retired with this commit -- roster entry and file deleted -- because
a stall is a standing state and this class has moved.
ObservedCpuReading { cpu_ms, eval_steps }
RightCensoredCpuReading { cpu_lower_bound_ms, censoring_ceiling_ms, eval_steps_before_censor }
CpuLowerBoundWithoutCeiling { cpu_lower_bound_ms, eval_steps_before_censor }
with the pair that mirrors the CPU accessors the file already carried:
observed_eval_steps (Present on the observed arm only -- the POINT question) and
eval_steps_at_least (total -- the one question a stopped row answers soundly).
FloorCostRow.eval_steps: Int is DELETED, not deprecated: DESIGN §3 cuts a
replacement over at the root, and a surviving bare Int is the attractor that rule
exists to prevent.
INSIDE THE ARMS RATHER THAN A SIBLING COPRODUCT, argued in the carrier itself. A
sibling satisfies the trigger's letter while leaving the incoherent row
constructible -- an OBSERVED cpu reading beside a before-censor step count.
Whether the claim reached its end is ONE fact about ONE occurrence and it
discriminates both readings. Not clock fusion: no arm compares a step count to a
millisecond.
WHAT THE CARRIER MADE VISIBLE, which is the point of the climb rather than a side
effect. Three sites were silently treating a bound as work performed:
- the equal-work inflation cohort paired rows on step equality, so a censored
row whose before-censor count happened to equal a completed row's performed
count JOINED the cohort and the whole difference was attributed to inflation.
performed_steps_agree now refuses it, INCLUDING two equal bounds: "at least N"
twice says nothing about whether the two claims did the same work.
- the determinism comparison gained a fourth arm,
CompletedIdentityStepsNotPerformed, because verdict_reached and the cost
reading are INDEPENDENT AXES, so reaching a verdict does not entitle a fold to
a performed count.
- the envelope scan now extracts the performed count at the same boundary that
extracts the cpu, so one case split admits or excludes a row.
- the instrument's row_step_count_is_complete is DELETED: a Bool beside the
value is the case split the accessor now performs, and its own annotation
already said "the same arm that censors the cost censors the steps".
FIXTURE STEP COUNTS ARE AUTHORED, NOT SED-ABLE, and that is why this is one
commit rather than a script. The 37 relocations in the distribution witness test
MOVED THE NUMBER THE FIXTURE ALREADY DECLARED beside its reading, so each fixture
asserts exactly what it asserted before. Where a fixture's subject is the CPU half
and no assertion reads the count -- the enrolment-margin and debt-admission suites
-- it takes fixture_incidental_steps() and the annotation says the value is
incidental, so a later reader cannot mistake an arbitrary figure for a measured
one. Phase 2 gives those suites a real step parameter.
NO RUST CHANGES, AND I OWE A CORRECTION ON THAT. My own file census said three
seed files, which counted files MENTIONING eval_steps rather than files consuming
the carrier. The seed does not mirror gunbc.floor_cost_distribution at all: its
eval_steps sites are the runner's own measurement and TSV emission, and this phase
does not change a column. The artifact format is untouched.
EVIDENCE, EXECUTING: two_equal_bounds_do_not_agree_on_performed_work is the
discriminating RED -- it is red only because the count moved, and no re-spelling
recovers the old answer since a bound is not reachable through observed_eval_steps
at all. Its positive control asserts the point question answers on exactly one arm
and the bound question on all three.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8Hk99iz2Q1H32CydKF6YS
The build lane was green and the FLOOR lane refused, which is the acceptance-path disagreement this module's own prose already records: the whole-corpus strict path checks joins the entry-scoped path accepts. Both reds are mine. NON-EXHAUSTIVE MATCH over EvalStepDeterminismViolation. Phase 1 added CompletedIdentityStepsNotPerformed and the instrument's renderer still eliminated four arms. It now renders the fifth, and says in the operator line what the arm means: the attempt reached a verdict but its step count is a bound, so the two attempts cannot be compared. Nothing else in the corpus matches this coproduct without a wildcard, checked rather than assumed. Nat/Int JOIN in the specimen extrema. eval_step_count_value returns Nat, as millisecond_count does, so a fold seeded `init: 0` put an Int against a Nat in the lambda's if-branches. Nat widens to Int in ARGUMENT position -- which is why the field assignments and specimen_steps_grounding keep working -- but not across a branch join, which is exactly where it refused. THE REPAIR IS THE CORPUS'S OWN IDIOM RATHER THAN A CAST. gunbc.roadmap_forecast takes extrema by sorting and reading the ends -- sort_by then first / first(reverse) -- and never seeds a fold with a literal at all. Using that here removes the join instead of working around it, and it deleted max_of, whose only callers were the two folds it replaced (§3c: a declaration with no consumer does not stay). I did NOT reach for a Nat-to-Int conversion. There isn't one in std, and minting one to satisfy a checker would have been the authoring-time workaround DESIGN §5 names -- the obstacle was my fold shape, not a missing primitive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D8Hk99iz2Q1H32CydKF6YS
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 574602505f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ObservedCpuReading { cpu_ms: _, eval_steps: s } => s | ||
| RightCensoredCpuReading { cpu_lower_bound_ms: _, censoring_ceiling_ms: _, eval_steps_before_censor: b } => b | ||
| CpuLowerBoundWithoutCeiling { cpu_lower_bound_ms: _, eval_steps_before_censor: b } => b |
There was a problem hiding this comment.
Keep lower-bound steps structurally distinct
When an equal-work consumer uses this new total accessor, the constructor distinction is erased: both an observed count and a before-censor lower bound become the same EvalStepCount, so eval_step_count_value(eval_steps_at_least(...)) can compare them without matching either reading. That is exactly the invalid cohort the deleted guarantee stall was meant to make unwritable; the safe implementation of performed_steps_agree only avoids the path in the current consumer. Preserve bound provenance in the return type (or keep the stall open) so future callers cannot silently reintroduce this comparison.
Useful? React with 👍 / 👎.
DRAFT — Phase 1 is landed and green; Phases 2–3 are not started. This body is the durable statement of what is in and what is pending, written for someone picking the lane up cold.
Plan document:
docs/plans/enrolment-margin-eval-step-denomination.mdStatus
574602505fb, run 34853617920, all four jobs.Why this lane exists
gunbc#11195 moved the required floor's claim ceiling onto eval steps and made CPU observed-only for every claim. It did not move every per-subject budget.
v2.workflow.floor_enrolment_marginstill derives a 302ms budget fromrequired_floor_per_subject_cpu_line_msand compares an observed CPU reading against it, so a newly enrolled witness can still be refused on a figure that is a property of the machine rather than of the claim.That is clause (iv) of the §4b(3) drop
floor_cost_cpu_regression_at_constant_eval_steps, in the trigger's own words:and it is the only clause #11195 left standing. A drop is retired by its trigger and by nothing else, so the drop stands until Phase 3 completes.
Phase 1 — IN, green at
574602505fbThe step count moved inside the
ClaimCostReadingarms:with
observed_eval_steps(Presenton the observed arm only — the point question) andeval_steps_at_least(total over all three — the one question a stopped row answers soundly).FloorCostRow.eval_steps: Intis deleted, not deprecated.This executed the
next_rung_triggerofgunbc.guarantee_stall.eval_steps_outside_the_reading_coproduct_stallverbatim, so that stall is retired — roster entry and file removed, because a stall is a standing state and this class moved.Inside the arms rather than a sibling coproduct, because a sibling satisfies the trigger's letter while leaving the incoherent row constructible: an observed CPU reading beside a before-censor step count. Whether the claim reached its end is one fact about one occurrence and it discriminates both readings. Not clock fusion — no arm compares a step count to a millisecond.
What Phase 1 surfaced
Three sites were silently treating a bound as work performed. This is the return on cutting at the root rather than adding a field:
performed_steps_agreenow refuses it — including two equal bounds, since "at least N" twice says nothing about whether the two claims did the same work.CompletedIdentityStepsNotPerformed, becauseverdict_reachedand the cost reading are independent axes: reaching a verdict does not entitle a fold to a performed count.row_step_count_is_completeis deleted — a Bool beside the value, where the accessor now performs the case split. Its own annotation already said "the same arm that censors the cost censors the steps".Evidence, executed
Read from the green run's own
required_floor_claim_costartifact (executed=3840), not inferred from a green lane — the floor log prints only non-PASS lines, so green alone does not distinguish passed from never ran:two_equal_bounds_do_not_agree_on_performed_workthe_point_question_answers_on_one_arm_and_the_bound_question_on_every_arma_parsed_row_takes_eval_steps_from_the_steps_columnThe RED is red only because the count moved: a bound is not reachable through
observed_eval_stepsat all, so no re-spelling of the comparison recovers the old answer.Two corrections recorded, so they are not re-derived
eval_stepsrather than files consuming the carrier. The seed does not mirrorgunbc.floor_cost_distributionat all — its sites are the runner's own measurement and TSV emission — and Phase 1 changes no column. The artifact format is untouched.buildwas green. That is the acceptance-path disagreementfloor_cost_distributionalready documents: the whole-corpus strict path checks joins the entry-scoped path accepts. Both reds were mine — a non-exhaustive match on the arm Phase 1 added, and aNat/Intbranch join (eval_step_count_valuereturnsNat, asmillisecond_countdoes;Natwidens toIntin argument position but not across anif-join, which is exactly where it refused). Repaired with the corpus's own extrema idiom —sort_bythenfirst/first(reverse), asgunbc.roadmap_forecastdoes — rather than minting aNat→Intconversion, which would have been the authoring-time workaround §5 names.Phase 2 — PENDING: the enrolment margin consumes it
floor_enrolment_margin_budget_msbecomes anEvalStepCountderived through the same pinned calibration the claim ceiling uses (v2.workflow.floor_eval_step_calibrationeval_step_budget_for), from a declared policy in milliseconds of work. Not a second calibration and not a literal: §5's oracle rule admits the budget only because it is grounded in a controlled fixture, and a second rate would fork that authority.EnrolmentMarginStandingkeeps its arms and changes their denomination —EnrolmentWithinMargin/EnrolmentOverMargincarryEvalStepCount, and the CPU reading is carried beside the verdict as an observation rather than deciding it, the shape #11195 gave the claim ceiling.EnrolmentBoundWithoutCeilingsurvives unchanged in meaning.Evidence to author. RED: a claim whose eval steps exceed the enrolment budget refuses. CONTROL: a claim far over the 302ms CPU line with in-budget steps is admitted, with the CPU observation recorded. These must be re-authored here, not cited from #11195 — a different budget is a different subject.
Fixture note for whoever takes this. The enrolment-margin and debt-admission suites currently pass
fixture_incidental_steps(), annotated as incidental because those fixtures exercise the CPU half and no assertion reads the count. Phase 2 gives them a real step parameter, at which point the value stops being incidental.Phase 3 — PENDING: retire the CPU line, or say why it stays
required_floor_per_subject_cpu_line_msexists only because two per-subject gates still read a CPU clock. After Phase 2 the enrolment margin is not one of them, and the remaining consumer isv2.workflow.floor_cost_debt_admission. Phase 3 decides that one: denominate it too and delete the symbol with its dissolution condition discharged, or keep it with a population of exactly one, stated.Only when no per-subject budget reads a run-level figure does clause (iv) retire.
Owed before undraft
The plan document is not yet linked from the authority that governs it. Its home is the drop row it discharges (
gunbc.rung_drop.floor_cost_cpu_regression_at_constant_eval_steps), and Phase 2 edits that row anyway. Note for whoever does it: touching arung_droprow driftsdocs/design-rung-drops.md, so runtools.docs_projection_gateregenbefore pushing. That regen refuses on BuildBuddy withHostBudgetUnreadable(no bound cgroup) but runs in a session container, which hasmemory.maxset. Letting CI's heal do it instead costs a full CI cycle and the heal-pushed commit then stalls ataction_requiredwith zero check-runs until someone approves the run.A separate lane, not part of this PR
fierce-lark ruled (2026-09-14) that a blocking floor red carries a typed attribution arm. Recorded here because it is a ruling, not a derivation, and it would otherwise be lost:
ChangedByThisDiff— blocks, author owns.UnchangedIdentityRegressed— blocks, lane/roster owns; the author is told plainly it is not theirs.FirstMeasurementOfAnUnchangedIdentity— routes to roster/debt, blocks nobody.Unattributed— blocks. If any classification input is unavailable (no cut census, no selection receipt), this is the arm. Never default into the routing arm.Attribution decides who owns a red, never whether it blocks — collapsing those two questions is what makes a two-arm shape look sufficient. A routing arm may never carry a regression. Define
FirstMeasurementby fact: no prior accepted measurement of this identity exists in the floor's measured population (the frozen cut plus every later accepted measurement), and it was not selected as changed — not by roster absence, because after #11195 the roster is the cut minus declared removals, so a removed identity is absent without ever being new. Its RED: an on-roster, unselected identity over budget must classify asUnchangedIdentityRegressed, must block, and must not be classifiable as either other arm.Deliberately not done
🤖 Generated with Claude Code
https://claude.ai/code/session_01D8Hk99iz2Q1H32CydKF6YS