Skip to content

ALERT-4: the falsifier alert's decision as a pure function over sixteen terminal arms - #7935

Merged
briansrls merged 28 commits into
mainfrom
session/calm-fox-748-alert4
Aug 8, 2026
Merged

briansrls merged 28 commits into
mainfrom
session/calm-fox-748-alert4

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

ALERT-4. The whole alert decision as one pure function — no GitHub calls, no clock read, no issue read. Every fact arrives as a parameter, so the arms hardest to reach in production are the cheapest to prove.

alert_decision(admission, receipt, observation, now, escalation, routings) -> AlertDecision, total over sixteen terminal arms, eight of them refusals.

Guard order is the algebra

Trust → escalation recipient → observer availability → receipt decodability → subject match → state readability → verdict.

Observer availability sits before the receipt deliberately: a run that timed out or failed at startup produced no receipt, so requiring one first reported "the floor's writer is broken" for the case where the floor never ran. Trust sits first because this program holds issue-write capability and an unadmitted run's receipt must never be decoded.

The facts each arm carries

  • Every writing arm carries its complete next state. First red sets both clocks; heartbeat preserves first_red; silent refresh moves neither; changed class preserves first_red (restarting it would erase how long the observer has been broken, precisely when the incident worsened).
  • FirstRedStanding — an adopted issue does not fabricate first_red = now. Declared once, in gunbc.falsifier_alert_state, and null on the wire rather than a sentinel instant.
  • Escalation is structural. AlertNotification.escalation_handle: NonEmptyStr; the undeclared case is RefuseEscalationRecipientUndeclared, reached before any notifying arm can be built.
  • Observer incidents are complete instructions — OpenObserverIncident / RefreshObserverIncident carry issue, next state and silent_for, with their own signature so a timeout after a startup failure is a changed class.
  • Trusted controller failures notify. NotifyAlertControllerUnavailable replaces silent refusals for a trusted run with a bad receipt, corrupt marker, or non-joining routings — previously the workflow completed successfully while the alert had done nothing. It never renders receipt content and never overwrites the marker. Untrusted events stay silent, so the wall cannot be used to page an operator on demand.

Identity

IncidentSignature is a sorted set of ComponentIncidentIdentity { index, outcome_tag, failure_mode_tag }. The label is out of the identity — it was the delimiter hazard — and the stored marker is read back structurally, with its index through the same strict_natural_of_lexeme the receipt decoder uses.

Routing

Routings exact-join the failing set by index; missing, surplus and duplicate each refuse with the indices named.

Conclusions

TimedOut/StartupFailure → observer incident. Skipped/Neutral/ActionRequired → RunPreventedByConfiguration, countable and distinct from green. Cancelled → CancellationUnexplained, deliberately unresolved: the workflow_run envelope does not say who cancelled a run or why.

Evidence

52 witnesses, PASS-counted. Discriminating REDs per arm, each with a control that stayed green under the same mutation — receipts in falsifier_alert_decision_red_receipts_note.

Not in scope

The trust wall is not wired (ALERT-6, whose acceptance must prove both enforcement points). The AlertDecision -> AlertEffectPlan projection comes before shadow mode. Item 1's remaining half — typing marker members via the shared closed tag readers — waits on #7928.

Stacked on #7928; rebases onto it when that merges.

gunbc-ci-auto-heal and others added 6 commits August 6, 2026 17:52
@gunbai-bot

gunbai-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in b457d608c86. review 49641 is correct and the cause was mine: introducing AlertIncidentState I renamed the parameter type with a global substitution, and it hit alert_decision itself along with the three inner folds it was meant for. alert_decision now takes FalsifierAlertStateDecoding again — it is the function that performs the peel, so taking the peeled type was backwards as well as wrong.

The part worth more than the fix. The claim in the review that this "will not typecheck" is the reasonable expectation, and it is not what happened — that is the finding. The broken signature typechecked clean and all 18 witnesses passed, both before and after the correction, with an identical roster=18 pass=18. Callers were passing StateAbsent (a FalsifierAlertStateDecoding) into a parameter declared AlertIncidentState, and the body was passing that same parameter to alert_incident_state_of(decoding: FalsifierAlertStateDecoding) — mismatched in both directions at once, and nothing refused.

So my evidence did not catch this and could not have. Every witness in the suite exercises behavior, and the behavior was right the whole time; what was wrong was a declared type that no consumer checked. This is a below-floor gap in the sense DESIGN §4b names — "values inhabit declared types" is the ordinary compiler floor, and a diff that lands a declaration the substrate does not enforce is not compensated by the higher-order properties above it. I am not fixing that here; it is well outside this PR. But recording it plainly matters more than the one-line correction, because the natural reading of "18/18 green" is that the module typechecks in the sense a reader assumes, and on this seam it does not.

Review found it by reading. That is the internal/external division DESIGN names — my witnesses find missing behavior, a reader finds missing checks — and it landed on exactly the class where execution evidence is blind.

— sent from calm-fox-748

…riven witnesses

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Acted on the non-blocking remark in review 49651, because the charitable reading in it does not hold. Landed in 32ae6ed6f30.

The review read the overlap between alert_decision and falsifier_alert_notification_decision as "intentional lane staging ahead of effect-plan wiring, not semantic dilution." I checked rather than accepting it: falsifier_alert_notification_decision had zero production consumers — only its own witnesses — and alert_decision_red recomputes exactly its rule, the same window comparison against last_notification_at, the same signature comparison, the same silent_for. That is not staging, it is a §3 fork: one rule, two implementations, agreeing only by diligence. The narrower one was also the more tempting thing for an effect plan to call later, at which point the alert would have had two answers to the same question with nothing forcing them to match.

Deleted, with the §4b(4) distinction observed: the redundant production machinery goes, the evidence does not. Its six payload-driven witnesses carried coverage the ALERT-4 suite genuinely lacked — they reach the arms through falsifier_alert_state_from_payload, the seam that turns issue-body bytes into the coproduct, where my claims had been constructing CurrentState by hand. So they moved onto alert_decision instead of being deleted alongside the function. Both suites PASS-counted: decision 24/24, migration 46/46.

One of the six failed on the move, and the failure was worth keeping. Written as "an empty payload opens a first red", it red: an empty string is not valid JSON, so it decodes to StateMalformed and the alert refuses. The fixture was wrong, not the code — StateAbsent means the issue body carried no marker at all, which the decoder never produces, because by the time bytes reach it a marker was found. Absence is decided one seam earlier.

I kept it as a claim rather than a corrected mistake because the tempting repair is on the wrong side. Mapping an empty payload to StateAbsent would turn "I read a marker and could not understand it" into "there was no marker" — the empty-observation narrow, with a data-loss consequence: an unreadable incident record would silently start a new incident and overwrite the old one. It now asserts the refusal, under the name an_empty_payload_refuses_rather_than_reading_as_an_absent_marker.

review 49654 raised nothing actionable; no change owed there.

— sent from calm-fox-748

…production caller may use them

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

review 49672 raised nothing blocking, but its reasoning about the Bool helpers rests on a fact that was true and unwritten, so I wrote it down — 2bc74cbbe7e.

The review is right that alert_decision_notifies, notification_escalates, routing_lacks_owner and notification_has_unowned_routing are the shape that normally trips predicate dissolution, and right that they are safe here. I verified the premise rather than taking it: all four are consumed only by witnesses, with no production dispatch site anywhere in the tree.

That is the entire justification, and nothing held it. One future call site — an effect plan branching on alert_decision_notifies instead of matching AlertDecision — would silently make these exactly the collapse this lane already refused twice (review 49400, and the state module's own no-notifies-bool argument), because the arms do not notify the same thing and a Bool answering "does this notify" erases the instruction at the boundary where the caller chooses what to say.

The note states it as a convention and says so plainly, because nothing in the substrate distinguishes a witness caller from a production one today. It names the check that would catch a violation: if the effect plan ever imports this module, the import list is where to look. Not a wall, and not described as one.

— sent from calm-fox-748

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

review 49673 (REQUEST_CHANGES on 32ae6ed6f30) is recorded in the dashboard tally but its artifact has not reached me — no message body delivered, and /api/reviews/49673/artifacts/stdout.log is not reachable from this session. So rather than guess at what it says, I audited the consolidation commit myself for what a reviewer would most plausibly catch, and found real dead surface. Fixed in 72665e9b5c4.

Deleting the forked throttle left eleven import-only symbols in gunbc.falsifier_alert_decision — ReceiptDecoded, ReceiptNotJson, ReceiptMemberAbsent, ReceiptMemberMalformed, ReceiptSchemaUnknown, ReceiptComponentCountMismatch, ReceiptUnsuccessfulComponentIdentityNotUnique, ReceiptSubjectMatches, ReceiptSubjectRunIdMismatch, ReceiptSubjectWorkflowMismatch, ReceiptSubjectHeadShaMismatch — each appearing exactly once in the file, in its own import. The module carries the decoding and subject-match values through as opaque payloads on RefuseReceiptMalformed / RefuseReceiptMismatch and never names a variant, so only the two types are needed. Same class in gunbc.falsifier_alert_state: millisecond_count was imported but its last call site went with the deleted function.

Both suites re-run after the removal: decision 24/24, migration 46/46, PASS-counted against roster.

If review 49673 was pointing at something else, I would like to see it — the artifact link in the tally is the only reference I have and it does not resolve from here. Happy to act on it once the body lands.

Worth flagging that neither the dead imports nor the earlier signature defect (review 49641) produced any compiler complaint. That is the same gap I recorded on this PR: the substrate is not checking these declarations, so unused-import and parameter-type errors both pass silently and review is currently the only thing catching them.

— sent from calm-fox-748

gunbc-ci-auto-heal and others added 5 commits August 7, 2026 00:11
- component index is a Nat read by a strict numeric decoder, not text
- outcome and failure_mode carry the existing typed authorities, unmodelled wire refuses
- unsuccessful rows identity-join the complete population in both directions
- component_count requires a number; the event numeric decoder names its two consumers

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ns policy

- issue observation un-fused from the state marker (duplicate/never-closed)
- notifications carry a handle; undeclared escalation refuses
- incident identity is a sorted typed set, label-free, read back structurally
- every writing arm carries its complete next state
- routings exact-join the failing set by index
- non-success conclusions no longer read as a green corpus

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Operator verdict addressed — all five corrections plus the conclusions ruling, in 2d7775955ae. 44 witnesses, PASS-counted 44/44.

P0-1 — issue observation un-fused from the state marker. AlertIssueObservation = NoOpenAlertIssue | OneOpenAlertIssue { issue, state } | MultipleOpenAlertIssues | AlertIssueLookupRefused is now the input; StateAbsent is a property of a body that exists. The two live defects are witnessed as the corrected behaviour: an existing marker-less issue is adopted on red rather than duplicated, and closed on green rather than reported as GreenWithNoOpenIncident. MultipleOpenAlertIssues refuses rather than picking one — two open alert issues means the alert already lost track of itself, and writing to an arbitrary half compounds it. AlertIssueLookupRefused refuses rather than reading as absence, which would open a duplicate on every red until the lookup recovered.

P0-2 — the mention is now structural, and you were right that it was not. AlertNotification carried an EscalationRecipient, and EscalationUndeclared inhabits that type — so a notification mentioning nobody was constructible and typechecked. The type forced an author to think about the recipient; it did not force the artifact to have one. It is now escalation_handle: NonEmptyStr, with RefuseEscalationRecipientUndeclared reached before any notifying arm can be built. Lane ownership stays absent-able and renderable, because "whose component broke" may honestly have no answer while "who is told the observer is broken" may not.

The old independence claim asserted that such a notification carried no mention — it passed, and it was measuring the wrong thing. It now asserts the refusal.

P0-3 — identity is a typed sorted set. ComponentIncidentIdentity { index, outcome_tag, failure_mode_tag } inside IncidentSignature, sorted by index, compared as a set both directions. The label is out of the identity entirely — it was the delimiter hazard (human-authored text containing : or |), and index already identifies the component uniquely now that #7928's decoder refuses a non-unique population. Rendering exists only for writing the marker, and the marker is read back structurally rather than string-compared, so a rendering change cannot silently become a behavioural change; an unparseable marker refuses rather than reading as "the incident changed".

P0-4 — every writing arm carries its complete next state, with the policy stated once. First red sets both clocks to now; heartbeat preserves first_red and moves last_notification_at; silent refresh moves neither; legacy migration preserves the recovered first_red. Changed class preserves first_red — that was the one needing a decision rather than an inheritance, and restarting it would erase how long the observer has been broken precisely when the incident got worse. One claim per arm.

P1-5 — routings exact-join the failing set by index, the same identity the signature is keyed on. Missing, surplus and duplicate each refuse with the offending indices named. Accept-and-join rather than deriving inside the decision, so the lane roster does not get a second copy inside the alert.

Conclusions policy. TimedOut and StartupFailure → NotifyObserverUnavailable; Skipped/Neutral/ActionRequired → RunPreventedByConfiguration, countable and distinct from a green; Cancelled → CancellationUnexplained. Cancellation is deliberately not resolved into supersession/operator/unexplained: the workflow_run envelope does not say who cancelled a run or why, so the arm records the distinction as unresolved with its dissolve-on named. Guessing "supersession" because it is common would fabricate; paging on every force-push would be noise.

Not in this PR, per your sequencing: the AlertDecision -> AlertEffectPlan projection before shadow mode, and ALERT-6's two-point trust-wall proof.

— sent from calm-fox-748

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

review 49807 is right — fixed in e2a0a585eff. Both blocks folded into one gunbc.floor_component_receipt_document import; the redundant FalsifierAlertState re-import is gone.

Cause was mine and worth naming since it is a pattern rather than a slip: I added the new symbols by appending fresh import blocks while satisfying unresolved-type errors one at a time, and the compiler accepted the duplicates, so nothing pushed back. That is the third time on this stack that a declaration-level rule turned out to be unenforced — after the alert_decision parameter type (review 49641) and the bare cross-module references on #7928 (review 49774). All three were caught by a reader; none by execution, because in every case the behaviour was correct and only the declaration was wrong. Witnesses do not see this class at all, which is exactly why the review pass matters here.

Suite re-run after the change: 44/44, PASS-counted against roster.

— sent from calm-fox-748

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

review 49830 found no defect, but its parenthetical — "11-arm (actually 15, counting all refuse arms)" — was a real catch about my prose, so I checked it. Fixed in 5981ea2cb72.

Counted against the live coproduct: sixteen arms, eight of them refusals. Two of my own notes were stale:

  • falsifier_alert_decision_arm_separation_note still opened "FOUR DISTINCT REFUSE ARMS" and enumerated four investigations. The operator verdict added RefuseEscalationRecipientUndeclared, RefuseMultipleOpenIssues, RefuseIssueLookup and RefuseRoutingJoin; the arms grew and the sentence describing them did not. Now names all eight and what each one sends a human to look at.
  • The witness note said "a decision procedure with eleven terminal arms ... and the four Refuse arms route to four different people."

Both corrected against the coproduct rather than from memory, and I kept the incident in the note rather than silently renumbering, because this is the same class three times on this stack — a name-level claim sitting false beside a mechanism that stayed correct. The previous two were caught by reviewers (the run_id_at decoder note on #7928, the LogicalOr invariant sentence earlier); this one was caught by a reviewer's aside while approving.

The specific hazard is worth stating: a count in prose decays every time the coproduct grows, silently, without anyone touching the sentence. It is DESIGN's positional-citation problem in a different coordinate — a claim about the shape of something the containment tree already knows exactly. The durable fix is not to write counts at all, and where the count IS the point (as it is here — "eight different investigations" is the argument for not collapsing them), it needs re-checking whenever the arms move.

Suite re-run: 44/44, PASS-counted.

— sent from calm-fox-748

gunbc-ci-auto-heal and others added 3 commits August 7, 2026 02:29
…ut, strict natural decode

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tanding, complete writing instructions

- FirstRedStanding on the marker; an adopted issue no longer fabricates first_red = now
- observer-unavailable decided before a receipt is required (a timed-out run has none)
- OpenObserverIncident / RefreshObserverIncident carry issue, next state and throttle
- NotifyAlertControllerUnavailable reports trusted internal failures instead of silence
- review 49873: marker index readback goes through strict_natural_of_lexeme

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Operator live-head items 1–4 plus review 49873, in 98726f9cfa4. 52 witnesses, PASS-counted 52/52.

3 — observer-unavailable now runs BEFORE the receipt is required. This was the sharpest of the four. A run that timed out or failed at startup produced no receipt — that is what those conclusions mean — so demanding a decoded, subject-matched receipt first sent every one of them to RefuseReceiptMalformed. The alert would have reported "the floor's writer is broken" for the common case where the floor never ran at all: a true statement about a missing artifact standing in for the true statement about a missing observer, with the wrong remedy and no notification. Order is now trust → recipient → observer availability → receipt → subject → state → verdict, which reads as escalating questions about what can be known.

2 — an adopted issue no longer claims to know when the incident started. FirstRedStanding = FirstRedObserved { at } | FirstRedUnknownBeforeTracking, and it lives on the marker itself (gunbc.falsifier_alert_state), so the unknown case survives a write instead of being flattened on the way out. On the wire it is JSON null, not a sentinel — 0 is a valid EpochMs meaning 1970, and every consumer would have had to know which impossible instant meant "unknown". Issue creation time is deliberately not substituted: when the issue was filed is not when the falsifier went red, and relabelling one as the other is the same fabrication one layer removed.

That change surfaced a smaller one worth naming. My first version detected the null by string-matching the number reader's diagnostic prose, which would have made a marker's meaning depend on the wording of an error message — reading state out of prose, which is exactly the practice this module deleted when it dropped the First red: regex, crept back in one layer down. StateNumberNull is now a typed arm; first_red reads it as the standing, last_notification_at still refuses.

4 — the observer arms are complete writing instructions, split into OpenObserverIncident / RefreshObserverIncident with issue, next state and silent_for, so the ordinary throttle governs them and a permanently broken observer does not comment every four hours. They carry their own signature (observer-unavailable:<tag>), so a timeout after a startup failure is a changed class and a timeout after a timeout is not.

4 — trusted internal failures now notify. NotifyAlertControllerUnavailable { cause, issue_disposition, notification } replaces the silent refusals for a trusted run whose receipt is missing/malformed/mismatched, whose marker is corrupt, or whose routings do not join. Previously the workflow completed successfully while the thing it exists to do had not happened — the original incident's exact shape, one layer up. It never renders receipt content and never overwrites the marker. Untrusted events stay silent, and the asymmetry is deliberate: making them notify would hand an attacker a way to page an operator on demand.

review 49873 / item 1 (partial) — the marker index readback now goes through strict_natural_of_lexeme, the same cardinal check the receipt decoder applies to the same semantic. The reviewer is right that a stored signature is hand-editable, so -1 and 6.5 are at least as reachable there as on the wire; both refuse, witnessed. The rest of item 1 — typing the marker members as ComponentOutcomeKind / FloorComponentFailureMode via the shared closed tag readers — waits for #7928 to land, as you sequenced.

Also still open by sequence: #7928 item A (gaining ModeStaleKnownRed and the expected-red-evidence-absent mode) needs #7906 merged first; it is still OPEN. I am not guessing at those mappings.

PR body updated separately — it still described the eleven-arm shape.

— sent from calm-fox-748

gunbc-ci-auto-heal and others added 3 commits August 7, 2026 03:49
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot gunbai-bot Bot changed the title ALERT-4: the falsifier alert's decision as a pure function over eleven terminal arms ALERT-4: the falsifier alert's decision as a pure function over sixteen terminal arms Aug 7, 2026
@gunbai-bot

gunbai-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

review 49908 found a real single-authority defect I introduced. Fixed in e4c65710c17.

I declared FirstRedStanding in both gunbc.falsifier_alert_state and gunbc.falsifier_alert_decision, identical shape, while the decision module already imported the state module and pointedly omitted those three names from its import list. Collapsed to the state module's declaration, which is where it belongs — the carrier field it types lives there.

Worth naming rather than fixing quietly: this is the §3 fork on the very carrier FalsifierAlertState.first_red is typed by, and it landed in the same commit whose note argues against exactly that. The reviewer is also right about the mechanism of harm — witnesses reading state.first_red through one declaration's constructors while the state module writes through the other would have stayed in agreement only by diligence, and a variant added to one side would have forked the meaning of the field silently.

Cause was the same one as the duplicate imports earlier on this stack: I added the type where I needed it while satisfying an unresolved-name error, and nothing pushed back. That is now four declaration-level defects on this stack caught by readers and none by execution — parameter type, bare cross-module references, duplicate imports, and now a duplicate type declaration. All four had correct behaviour and a wrong declaration, which is precisely the class witnesses cannot see.

Suite re-run: 52/52, PASS-counted.

PR body and title also updated — they still described the eleven-arm shape from before the operator verdict; it is sixteen arms, eight refusals.

— sent from calm-fox-748

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

review 49927 is correct. Fixed in 88d6726415a — IncidentLegacy.first_red is FirstRedStanding, matching the carrier it is threaded from and into.

The reviewer's reasoning about why it matters is the right one and worth restating: the whole point of FirstRedStanding was to make "unknown before tracking" representable, and re-narrowing to EpochMs on the one arm that carries a legacy incident re-opened exactly the state the carrier exists to hold. That arm is where an unknown first-red is most likely to arrive.

The prediction that "the module will not typecheck" is again the part worth reporting. It did. FirstRedStanding values flowed into a field declared EpochMs, and that same binding then flowed back out into three fields declared FirstRedStanding — a mismatch in both directions at once — and all 52 witnesses passed before and after the correction. Behaviour was right throughout because the value never changed shape at runtime; only the declaration was wrong.

That is the fifth declaration-level defect on this stack found by a reader and the fifth not caught by execution: parameter type (review 49641), bare cross-module references (review 49774), duplicate imports (review 49807), duplicate type declaration (review 49908), and now a field type. Every one had correct behaviour and a wrong declaration. I am not treating the passing suite as evidence about declarations any more, and I have said as much rather than quietly re-running it.

One note on process: the first re-run after this change reported 0/52, which was claim_batch truncating under load rather than a real failure — it printed only its startup line. Re-run after a pause: 52/52, PASS-counted against roster. Reporting it because a 0/52 that is actually a dead command is the same trap as an empty FAIL-grep reading as green, just pointing the other way.

— sent from calm-fox-748

…es match it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

review 49947 is right on all four findings, and they are one defect. Fixed in c86d46db48d.

The defect: two representations of one failure. When the operator verdict moved controller failures from silent refusals to NotifyAlertControllerUnavailable, I added the notify arms and left the refuse arms declared. So RefuseReceiptMismatch, RefuseReceiptMalformed, RefuseRoutingJoin, RefuseMultipleOpenIssues, RefuseIssueLookup and RefuseMalformedState sat on AlertDecision uninhabited, beside inhabited Controller* causes meaning exactly the same things. The reviewer names the consequence precisely: ALERT-5/6 would write exhaustive dispatch handlers for arms that never run.

All six are deleted. AlertControllerCause is the single authority, and ControllerIssueLookupRefused / ControllerMultipleOpenIssues — previously dead — are now the inhabited representation of those two cases. IncidentIndeterminate carries a cause rather than a whole AlertDecision, which also removes a needless recursion through the decision type.

One case moved as part of collapsing rather than being mechanically renamed: an unparseable stored signature was still returning the silent RefuseMalformedState. It is the same class as a corrupt marker — the alert cannot read its own state — so it now reports through ControllerMarkerMalformed with the issue named. Leaving it silent would have kept exactly the defect the operator verdict removed, in the one arm nobody was looking at.

The two stale notes are the same failure in prose, and both are corrected. falsifier_alert_decision_arm_separation_note still described four top-level refuse arms that no longer exist; it now states the two axes that actually govern — whose failure it is decides whether anyone is told (untrusted stays silent so the wall cannot be used to page an operator; ours notifies), and which failure it is decides who investigates, carried as the typed cause. decision_order_note still said a corrupt marker yields RefuseMalformedState on both green and red; the behaviour it describes is unchanged — the marker is still never written over, and a green run still does not close — but who hears about it changed, and the note now says so.

I kept the incidents in both notes rather than silently rewriting. This is now six declaration-or-prose defects on this stack caught by readers: five declaration-level and this pair of stale authority paragraphs. The pattern is consistent — behaviour correct, description wrong — and prose is the half no witness can ever check.

Suite re-run: 52/52, PASS-counted.

— sent from calm-fox-748

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

review 49971 is correct on both. Fixed in df8d4f21c20 — current_state_marker_round_trips and legacy_marker_is_its_own_state_not_malformed now match FirstRedObserved { at: t } instead of comparing a FirstRedStanding to a bare Int.

This one was a verification gap, not just an oversight, and that is the part worth reporting. When FirstRedStanding landed on this branch I re-ran the decision suite (52/52) and the migration suite — but I ran the migration suite on the base branch, where first_red is still EpochMs, so it passed 71/71 and told me nothing about the branch that changed the type. The suite that would have caught this was run, and run against the wrong tree.

It would have redded loudly rather than silently: the interpreter's cross-representation equality guard raises on exactly this comparison, which is how the same class surfaced elsewhere on this branch. So this is a case where the substrate does catch the defect at runtime — unlike the six declaration-level ones — and my process simply never put the two together in one place.

Migration suite on this branch after the fix: 65/65. Decision suite: 52/52. Both PASS-counted against roster.

One thing I checked rather than assumed while doing it: the migration roster is 65 here against 71 on the base, and I did not take that on faith. The six absent claims are exactly the payload-driven ones moved onto alert_decision during the review 49651 consolidation — a_decoded_legacy_marker_notifies_exactly_once_as_a_migration, a_decoded_changed_signature_always_notifies_even_inside_the_window, a_decoded_unchanged_signature_is_suppressed_inside_the_window, a_decoded_unchanged_signature_heartbeats_once_the_window_elapses, a_decoded_malformed_marker_refuses_instead_of_starting_a_new_incident, and an_empty_payload_refuses_rather_than_reading_as_an_absent_marker. Coverage moved, nothing dropped. A shrinking roster across a merge is worth confirming by name every time, because the merge driver on this repo can resolve one side away without a conflict.

— sent from calm-fox-748

@gunbai-bot

gunbai-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Recording a merge conflict, not resolving it — the head stays where the two approvals are until the operator decides to spend the round.

State. #7928 merged as squash d145104eda2. git merge-base --is-ancestor 49a9dd7004cc origin/main says NO: the squash put #7928's content on main under a new lineage while this branch still carries the pre-squash commits. So the two files both PRs touch conflict textually even though the content is substantively the same work:

  • dag/gunbc/falsifier_alert_state.dag
  • dag/test/claim/falsifier_alert_migration_witness_test.dag

Measured read-only with git merge-tree --write-tree origin/main df8d4f21c20e — no worktree write, no head move.

This refutes the prediction the retarget was priced on. The plan held that retargeting after #7928 merged would need no rebase, the base change alone collapsing the diff. That is true for a fast-forward base and false for a squash-merged one. One round was the right call; "no conflict resolution in it" was not.

Why I am not pushing. Both approvals sit on df8d4f21c20e. A merge commit moves the head, discards both, and demands a fresh CI round. That trade may well be correct — this PR cannot merge or run CI conflicted, so the approvals are not redeemable as they stand — but whether to spend it tonight is not my call, and the branch freeze names exactly this case.

Resolution recipe when it is authorized (merge commit, not rebase, per repo merge policy):

git merge origin/main
# resolve the two files above, then re-verify by NAME not by count

The witness file needs care specifically. Its diff against main is 4 insertions, 79 deletions, and that shrink is expected — six witnesses were deliberately moved to falsifier_alert_decision_witness_test.dag — but a count cannot distinguish a deliberate move from the generated-artifact merge driver silently resolving one side away. Diff the test fn name lists and account for every missing name individually.

gunbc-ci-auto-heal and others added 3 commits August 7, 2026 21:27
…alert4

# Conflicts:
#	dag/gunbc/falsifier_alert_state.dag
#	dag/test/claim/falsifier_alert_migration_witness_test.dag
…k refused

Run 31219557841's regen job refused on the pre-plan witness naming walk:
red_receipt_other_class is a plain fn in a *_test.dag unreachable from every
test fn, which is silent de-enrollment.

Deleted rather than promoted. It has zero call sites, and the changed-signature
claim it looks like it should carry is already covered on the correct side --
the receipt stays red_receipt (the current run) while the STORED STATE's
signature varies via signature_of_fixture(component: other_failing_component).
Varying the receipt would exercise the wrong half of the comparison. So this is
a dead scaffold, not a missing witness.

Worth naming because a local run cannot find this class: claim_batch executes
the roster handed to it, built by grepping `test fn`, so a plain fn no test
reaches is outside that denominator by construction. The suite reported 52/52
before and after -- true both times, and silent on exactly the thing that
failed to enroll.

Decision suite 52/52 after the deletion, PASS-counted against roster.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…red path

alert_decision_observer_unavailable's IncidentCurrent arm returned
RefreshObserverIncident unconditionally -- it never compared the heartbeat
window, and never compared the stored signature either. On the four-hourly
falsifier cadence a permanently timed-out observer therefore paged every four
hours, which is the notification spam this module exists to end.

Two notes a few lines apart asserted the guard existed. The code was wrong and
the prose was right, which is the worse orientation: an auditor reading the
policy would have found it documented, coherent and untrue.

The witness was holding it in place. a_repeated_observer_outage_refreshes_the_
same_incident asked the repeat case at last=5000 now=6000 -- a one-second gap
inside a twenty-hour window -- and asserted RefreshObserverIncident, pinning the
spam as the contract. That is why 52/52 was green over it, and why the correct
fix would have read as a regression.

New RefreshObserverIncidentWithoutNotification arm; the observer path now
mirrors the red path. The two notifying cases share one arm because they produce
the same effect -- the red path splits its two only because their MESSAGES
differ, a distinction in what gets said rather than whether to say it.

The suppressed arm carries last_notification_at UNCHANGED. Advancing it to now
looks right (the run did just observe the incident) but restarts the window
every cadence, so the elapsed comparison can never fire and the heartbeat goes
silent permanently -- converting a spam defect into a strictly worse silence
defect that nothing observes. The field records when we last SPOKE, never when
we last LOOKED.

Adding the arm broke alert_decision_notifies and alert_decision_notification as
non-exhaustive, so the closed coproduct made it structurally impossible to add a
suppressing arm while leaving a predicate claiming it notifies.

Roster 52 -> 55, 55/55 PASS-counted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@briansrls
briansrls merged commit 4d8234d into main Aug 8, 2026
5 checks passed
@briansrls
briansrls deleted the session/calm-fox-748-alert4 branch August 8, 2026 00:33
briansrls added a commit that referenced this pull request Aug 8, 2026
… main (#8035)

* WIP: Stop-line: separate direct_rust expected-red standing from long-lane exe

* WIP: Stop-line: separate direct_rust expected-red standing from long-lane exe

* Separate expected-red standing from execution budget: three independent witness axes

Cadence, eval budget and expected verdict were carried on one axis
(WitnessConsumerCadence), so a witness that is both expensive and known-red was
inexpressible. The eval ceiling was armed only for a batch whose entries
intersected the long-lane batch roster, so such a root earned its budget by
ALSO joining that batch: two schedule occurrences, the second on a batch that
expects green, which paged the operator once per heartbeat window for a witness
that is red BY DESIGN (#7737). Expectation itself was batch-wide and matched on
entry PATHS, so file placement and batch homogeneity decided polarity.

std.witness_admission gains WitnessExecutionBudget and WitnessExpectedVerdict as
independent axes; ExplicitWitnessAdmission carries all three per row, and
explicit_witness_admission_is_known_red now reads the EXPECTATION rather than the
cadence. The scheduler ceiling reads witness_long_eval_budget_entries — the union
of the long-lane batch roster and rows declaring their own long budget — so
batch membership no longer confers time. The direct_rust duplicate long-lane row
deletes; its paired schedule-multiplicity admission deletes with it, its
dissolve_on having fired.

claim_executor matches every executed witness against its own row:
still-red is agreement, unexpected green is a typed StaleKnownRedRefusal naming
the rows to un-quarantine. The refusal is derived from roster membership joined
against per-witness outcomes and carried as data, so it reaches the component
receipt as its own StaleKnownRed mode (-> Refused) instead of falling through the
substring classifier's WitnessRed arm, where an un-quarantine and a regression
would share one alert signature with opposite remedies.

The batch-wide predicate survives only for a corpus resolve refuse, which has no
per-witness outcomes, and only in its narrow all-entries form.

* WIP: Stop-line: separate direct_rust expected-red standing from long-lane exe

* Delete the path-grain hermetic known-red field, and refuse an unaccounted failure

Function-grain expectation replaced hermetic_known_red_entry_paths outright, so it
is deleted rather than left beside its successor (-D dead_code caught it as the
CI build error). The wet twin survives because it still scopes a WALL budget, a
different fact from polarity.

The still-red arm now passes only when every failure in the summary is matched by
an agreement, instead of reading an empty unexpected-failure list as 'nothing
failed'. Outcomes and failures are pushed in lockstep today so the counts agree;
if one ever arrives without a per-witness identity the arm refuses rather than
narrowing an unmatchable failure into a pass.

* Receipt-layer witness: StaleKnownRed is a refusal, not a WitnessRed and not Done

Both directions asserted. It must not collapse into WitnessRed (an un-quarantine
and a regression would share one alert class signature with opposite remedies),
and it must not read as success (an expected-red row that silently went green
would accumulate on a roster nobody revisits — the fail-open this mode closes).

Also fixes the two fixture helpers in the admission witness that still called
known_red_probe without the now-required budget.

* Name the lane-granularity consequence of the budget union

One declared-long row raises the ceiling for every row in the batch carrying it,
so the known-red probe batch now runs at the long ceiling. That is the
pre-existing lane granularity rather than a new grain, and it narrows to per-row
on the cost-envelope trigger already declared beside it — stated here rather
than left for a reader to discover.

* Agreement means the assertion RAN and was false; the budget roster is an identity set

TWO DEFECTS FOUND IN REVIEW, both mine.

(1) The classifier computed green = (outcome == Pass) and routed EVERY other
outcome into the expected-red agreement arm. That blessed a budget kill, an
interpreter fault, a non-Bool return and an unobserved row as though the business
assertion had executed and returned false — the empty-observation conflation, in
the one place that decides whether a red is real. It also contradicted a recorded
ruling outright: BudgetExceeded is an interruption plus a measured lower bound on
cost, never a semantic verdict on a witness, so a known-red witness that timed out
reported its quarantine intact while its behaviour was unobserved.

ExpectedRedDisposition now gives each outcome its own row. Only Fail is agreement.
Pass is the stale quarantine. TimedOut is a budget failure and RuntimeError/NotBool
an infrastructure-or-referent failure — both red the batch instead of being blessed.
A rostered expected-red witness with no observation is EvidenceAbsent: counted and
named on every path, never agreement and never a failure, because absent evidence is
not a verdict. Refusal-before-evaluation shares the infrastructure arm today because
run_claim formats the typed InterpError to a String before ClaimOutcome is built;
that is declared on the variant with its trigger, not recovered by sniffing prose.

(2) The ceiling roster concatenated its two inputs, so a witness reaching it through
both would be listed twice. It is now an identity set joined on
schedule_witness_entry_eq.

The dedup control is worth naming, because the first one was a tautology: it read
the live union, and the live inputs are disjoint, so deleting the membership check
left it green — measured, not assumed. The fold is now exposed as
witness_long_eval_budget_union_in and the control drives it with a planted overlap,
which reds the moment the join stops using identity.

* ALERT-3.5: strict input boundary before the decision algebra consumes it

* Point the refused/fault collapse at the existing seam note, not a second ledger entry

The collapse dissolves on the SAME trigger gunbc.witness_row_cost
witness_cost_timed_out_seed_deferral_note already carries, because it is the same
fact: the typed InterpError does not survive the marshalling of ClaimOutcome across
the interpreter boundary. Realizing witness execution from .dag puts the typed error
in hand at the point of judgement and splits the arm. One fact, one home — a new
ledger entry for one cause is the parallel-ledger failure.

Also promotes this PR's own tautology finding to a named tell in the body: a control
that reads a live population can be vacuous whenever that population does not exhibit
the property, and will stay green untouched. Found by mutation, not by reading.

* review 49571: split the component-summary decoder so only index admits a number

* review 49594: emit selection_degradation on every path so encode/decode round-trips

* ALERT-4: the falsifier alert's decision as a pure function over eleven terminal arms

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* carry the guard-order divergence reasoning in the witness carrier, not only the PR

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* review 49641: alert_decision takes the decoding, not the peeled incident state

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* review 49651: delete the forked throttle decision, keep its payload-driven witnesses

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* review 49672: name the Bool helpers as a witness surface, and why no production caller may use them

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* drop import-only symbols left behind by the consolidation

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* operator verdict on #7928: type the decision-grain boundary

- component index is a Nat read by a strict numeric decoder, not text
- outcome and failure_mode carry the existing typed authorities, unmodelled wire refuses
- unsuccessful rows identity-join the complete population in both directions
- component_count requires a number; the event numeric decoder names its two consumers

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* review 49774: import the symbols the new claims call

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* operator verdict on #7935: the four missing facts, plus the conclusions policy

- issue observation un-fused from the state marker (duplicate/never-closed)
- notifications carry a handle; undeclared escalation refuses
- incident identity is a sorted typed set, label-free, read back structurally
- every writing arm carries its complete next state
- routings exact-join the failing set by index
- non-success conclusions no longer read as a green corpus

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* review 49807: fold the duplicated import blocks

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* correct the arm counts in prose against the live coproduct

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* operator live-head B and C: validate the outcome/mode pair, read output, strict natural decode

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* operator live-head 1-4 on #7935: observer-before-receipt, first-red standing, complete writing instructions

- FirstRedStanding on the marker; an adopted issue no longer fabricates first_red = now
- observer-unavailable decided before a receipt is required (a timed-out run has none)
- OpenObserverIncident / RefreshObserverIncident carry issue, next state and throttle
- NotifyAlertControllerUnavailable reports trusted internal failures instead of silence
- review 49873: marker index readback goes through strict_natural_of_lexeme

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* CI: write out the wildcard arms the non-fold-residue lens counts

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* review 49908: FirstRedStanding has one authority, in the state module

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* review 49927: IncidentLegacy carries the standing, not a bare instant

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* review 49947: one representation for controller failures, and the notes match it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* review 49971: the migration witness reads first_red as a standing

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Separate the known-red axes: two budget-homogeneous batches, and three fail-closed repairs

Recut per operator ruling: the known-red lane carries two DERIVED
budget-homogeneous discovery runnables instead of one mixed batch joined
into the long lane for budget. Eval ceilings are per batch, so a mixed
batch widened every sibling's budget; both populations are now derived
from the admission authority by declared budget
(known_red_probe_corpus_fast_budget_roster /
known_red_probe_corpus_long_budget_roster), never a hard-coded singleton
for direct_rust. Component indices are unchanged: components are per
LANE, and LaneGates already carries a multi-runnable batch.

FIX 1 - absent evidence is a COVERAGE REFUSAL, not a log line.
ExpectationRefusal::ExpectedRedEvidenceAbsent refuses the batch on its own
receipt mode (ModeExpectedRedEvidenceAbsent), so a rostered expected-red
witness that stops executing can no longer pass admission. It stays out of
hard_failures deliberately - it is a coverage fact, not a semantic verdict,
and mixing it into the verdict population would corrupt the identity join.

FIX 2 - a pre-verdict refusal needs its own DECLARED expectation.
WitnessExpectedVerdict splits into ExpectWitnessHolds | ExpectAssertionFalse
| ExpectTypedPreVerdictRefusal{phase, cause}. Only rows on
known_red_pre_verdict_refusal_roster may admit a resolve-phase refusal, and
the check is exact-entry-grained membership - no message sniffing. The
observed cause is not yet matched against the declared one because run_claim
formats the typed InterpError into a String before ClaimOutcome exists; the
declaration GATES the inversion without VERIFYING it, disclosed as a bounded
compatibility arm with dissolve-on the existing
witness_cost_timed_out_seed_deferral_note seam.

FIX 3 - completeness is an IDENTITY JOIN, not a count equality (operator
oracle ruling, 2026-08-01). classified_non_pass carries identities;
non_pass_join_is_complete requires every non-Pass outcome to appear in
exactly one bucket by witness identity. The residual summary.failures count
survives only as a guard over rendered diagnostic strings that carry no
identity, explicitly not as admission authority, with its own dissolve-on.

Shared membership helper schedule_witness_entry_roster_contains lifted into
std.realization_schedule rather than forked a second time in ci_layer_roots.

Green by execution: 8/8 executor unit witnesses including the discriminating
pair (equal counts over different identities refuses; absence refuses on its
own mode with an observed positive control), 17/17 exact_witness_admission,
17/17 floor_component_receipt, and the composed ci_floor_plan evaluates.

* Add extdeps.linux.proc_self_cgroup parser substrate (#7887).

Model /proc/self/cgroup unified-membership read with distinct missing,
ambiguous, and malformed refusal states; enroll scope carrier; witness
parser fixtures only (no production workflow or path_intent changes).

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address #7887 substrate review: semantic subject, no inflated service.

Ground ExternalModelScope on ProcPidCgroupMembershipRecord (not the
citation authority), remove LinuxCgroup.CurrentMembership rung inflation,
and require cgroup-v2 unified shape (0::path with empty controller field).
Witness 0:cpu:/path is not accepted as unified membership.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Recut cgroup membership + memory.peak ancestor vertical after #7887 review.

Move bash membership realization into extdeps.linux.proc_self_cgroup (single
parser authority; no grep -c/head -1/awk widen). Add NearestAncestorContaining
search policy in std.path_intent with PathIntent start. Wire ci_floor_peak_emit
through bash_orch_if lowering only. Add compile-time production witnesses plus
executing shell fixtures enrolled in bin_witness_wet_entries; update emit
goldens. Workflow yml drift deferred until merge train clears.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restrict NearestAncestorContaining.start to PathRefStart construction wall.

Replace nearest_ancestor_walk_target's fabricated "d" fallback for
PathLiteral/PathJoin with a PathRefStart field type so non-PathRef walk
carriers are unwritable at construction (review 49936, non-blocking).

Co-authored-by: Cursor <cursoragent@cursor.com>

* Split bash cgroup membership lowering into v2.extdeps.languages.bash_proc_self_cgroup.

extdeps.linux.proc_self_cgroup now owns only language-independent /proc/self/cgroup semantics; the foreign-executor shell spelling lives in a separate realization module per shell->intent Phase 1 layering.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Regenerate the stage0 seed for the roster-membership addition

`regen_verify_gate_passes` returned false on CI run 31153645062: the
`.dag` admission-model changes on this branch emit a new
`schedule_witness_entry_roster_contains` (and its note) into
`std_realization_schedule.rs`, and the regenerated seed was never
committed, so the self-host fixed point diverged.

This is `regen_stage0`'s own output, not a hand edit: 133 generated
files written, exactly one changed, 21 insertions and no deletions.

Run by keen-ibex-349 on behalf of deep-otter-771, whose container could
not complete the `v1-compiler` lib compile: two SIGKILLs with
`memory.events max 0, oom 0, oom_kill 1` at a 2.68 GiB peak against a
31.27 GiB cap — the cgroup's own limit was never reached, so the kill
arrived from an ancestor. A third identical kill reproduced in this
container at 1-min load 34; this run succeeded at load 10.9. The
`--remote` path executes regen but uploads no artifacts, so it cannot
return the bytes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Pin the known-red lane's budget partition in the plan witness, not a singleton count

The floor red on 7992dae is ci_floor_plan_witnesses returning false, and
it is correct: the witness pinned the known-red probe lane to EXACTLY ONE
runnable whose explicit_entries length equals the whole corpus roster. That
is the shape the recut deliberately replaced, so the witness had to move
with it.

What replaces it is strictly stronger than the count it drops. Each runnable
must match EXACTLY one derived budget roster by IDENTITY
(schedule_witness_entry_roster_contains over every entry), not by length —
so a same-sized wrong population refuses, and the two rosters cannot be
swapped between runnables, which is the budget inversion the split could
otherwise hide. The runnable count must equal the number of NONEMPTY
rosters, so an empty side schedules no batch rather than an empty one. Every
runnable independently keeps the lane's SelectionApplied / hermetic /
no-host-compiler profile, which the single-runnable form only ever asserted
once. And a coverage clause requires the two rosters to sum to the corpus
roster, so the split cannot silently drop a row.

Pinning only the total would permit the swap; pinning only the count would
permit either side carrying the wrong rows. Both are asserted.

NOT VERIFIED LOCALLY, and disclosed rather than implied: this container
cannot build the interpreter (five SIGKILLs today compiling the v1-compiler
lib, two of them at load 9-13, with memory.events max 0 / oom_kill 1 at a
2.68 GiB peak against a 31.27 GiB cap — the kill arrives from an ancestor
cgroup, not this one), and CI's own release-bins binary will not run here
(requires GLIBC_2.39, unavailable). CI is the only oracle currently reachable
for this change.

* Make the pre-verdict arm non-green, and make the roster join actually exact

Two defects found in review, both real, both closed here.

BLOCKER 1 — a DECLARATION was flipping the result green. The Err arm
returned ok:true whenever every explicit entry merely DECLARED a typed
pre-verdict refusal. No ClaimOutcome exists on that path and run_claim has
already formatted the typed InterpError into a String, so the observed phase
and cause cannot be matched against the declared ones — a row declaring
PreVerdictResolve/UnboundImportedVariantConstructor was admitted when some
entirely different pre-verdict fault occurred. Disclosing that did not make
it sound: an unobserved fact must not be verdict-bearing. The arm is now
NON-GREEN and the declaration only classifies WHY, via its own mode
ExpectedRedPreVerdictUnverified, which floor_component_receipt maps to
Refused alongside its two siblings. It is distinct from both by design:
StaleKnownRed says an executed row passed (delete the admission),
ExpectedRedEvidenceAbsent says a rostered row never ran (make it run), and
this one says the row stopped as declared while nothing proves it stopped
for the declared reason (carry the typed cause across the boundary).
Dissolve-on: EXPECTED-RED-CAUSE-1.

BLOCKER 2 — the identity join was not exact, and this one is worse than it
looks because it falsified the PR's own thesis. known_red_probe_batch_
matches_roster asserted length equality plus ONE-WAY containment and the
note called that an identity join. It is not: actual [A, A] against expected
[A, B] satisfies both — lengths agree at 2, every actual entry is present —
while B is dropped and A duplicated. A same-sized wrong population passed,
which is exactly what the note claimed would refuse. Replaced with
known_red_roster_set_eq: duplicate-free on BOTH sides plus containment in
BOTH directions, which for identity-set rosters is set equality. covers_lane
had the identical shape one line down (a bare count that two swapped rows or
one duplicate per side both satisfy) and is fixed with the same move rather
than left standing beside the repair.

Why the earlier swap probe missed it, recorded in the note so the calibration
survives: swapping the two rosters changes the ASSIGNMENT, and one-way
containment does catch that, because a fast entry is genuinely absent from
the long roster. Multiplicity is an orthogonal axis the swap never perturbs.
Evidence that a clause is non-vacuous is not evidence that it is exact, and
one discriminating RED establishes only the failure mode it actually drives.

Witnesses: witness_known_red_roster_set_eq_is_exact_not_a_length_agreement
drives [A,A] vs [A,B] in both directions plus the order-independence and
subset cases; declared_pre_verdict_refusal_is_non_green_and_carries_its_own_
mode pins the mode string and its distinctness from both siblings;
fcr_pre_verdict_unverified_is_a_refusal_distinct_from_both_siblings pins the
receipt projection. Rust side 9/9 green by execution; the .dag side is
unverified locally for the reasons the previous commit records.

* Make the non-green regression control real, not a doc comment

Found in review, and it is the same defect class as the identity join it
sits beside: a stated discrimination wider than the one the test drives.

declared_pre_verdict_refusal_is_non_green_and_carries_its_own_mode claimed
"if someone restores the green arm, the first assertion here fails". It did
not. All three assertions compared string constants to each other and to a
literal; none observed ClaimResult.ok. run_discovery_batch_node has two call
sites and neither is a test, so nothing executed the arm — flipping ok:false
back to ok:true left the whole test green. The arm was non-green and the
mode-distinctness half was genuine, but the regression control the comment
promised did not exist.

Repaired by construction rather than by weakening the sentence: the arm's
ClaimResult is extracted to pre_verdict_unverified_claim_result, its single
construction site, and the test now calls it and asserts ok == false. Restore
the green arm and the test fails. It also pins that the typed mode and the
located cause reach detail, that the entry count is carried, and that no
per-witness population is fabricated on a path that observed no witness.

Also worth stating because it is a real new refusal the previous commit did
not advertise: covers_lane moved from a length sum to
set_eq(concat(fast, long), corpus_roster), and duplicate-freedom over the
CONCAT additionally forbids an entry appearing in BOTH budget rosters. The
old form could not see that — 2 + 1 == 3 holds with one row double-counted
and one missing. A row in both rosters would now refuse rather than pass.

9/9 executor unit witnesses green by execution.

* Partition the known-red lane by expectation kind too, not budget alone

cursor review 50216 requested changes and the finding is real. Verified
against the code before acting: known_red_probe_corpus_{fast,long}_budget_
roster were derived from the whole corpus roster, so the three
ExpectTypedPreVerdictRefusal rows sat in the same discovery batch as
ExpectAssertionFalse rows.

Those two kinds cannot share a batch. A pre-verdict row stops the batch at
RESOLVE, so NO per-witness outcome exists for anyone in it, and the Err arm
admits that stop only when EVERY entry declared a pre-verdict refusal. One
pre-verdict row in a mixed batch therefore reds its siblings — witnesses that
were holding quarantine correctly and that the stop says nothing about. That
is the batch-wide coupling this whole PR exists to remove, reintroduced one
axis over by the budget split itself. Splitting on budget alone was not
enough, and the roster that would have caught it
(known_red_pre_verdict_refusal_roster) was imported by the plan and never
used to schedule anything.

The lane now derives up to FOUR batches, one per (expectation kind, declared
budget) pair, each homogeneous in both facts that govern it: the ceiling it
needs and the fact that decides its verdict. Nothing is authored — a new
admission routes itself, and an empty side schedules no batch rather than an
empty one.

Worth stating plainly: isolating the pre-verdict batches does NOT make them
pass. They are non-green by construction until EXPECTED-RED-CAUSE-1 lands,
because a declaration classifies a stop and does not verify it. The point is
that their refusal stays THEIR refusal — located and countable — instead of
being spread over rows it has nothing to say about.

Witnesses moved with it. The partition claim is now four-way with per-row
exactly-one-side membership (not a pairwise XOR, which cannot express four
sides); a new no_batch_mixes_a_pre_verdict_row_with_an_assertion_false_row
asserts the kinds are disjoint in both directions AND that the pre-verdict
side is nonempty, so it cannot pass by the population being empty; budget
homogeneity now covers all four rosters; and the plan witness pins batches
to rosters IN ORDER, each by set equality, plus a union clause so the split
can neither drop a row nor double-count one. The pairwise check is a fold
with an index rather than recursion, to avoid a Loop with no descent
evidence.

Unverified locally: the cold full-lib rebuild still cannot complete here.

* Respell six leading-plus continuations the parser does not accept

4fba338 did not parse. A leading '+' at the start of a continuation line
is a parse error ("expected expression, found Plus"); the six sites were the
new four-way partition arithmetic in
exact_witness_admission_witness_test.dag. Operators moved to the end of the
preceding line. No logic changes.

Proved on minimal three-line fixtures rather than inferred from the file:
  let a = 1 \n + 2 \n + 3              -> parse error
  let a = true \n && false \n && true  -> parses
  let a = 1 + 2 + 3                    -> parses
So '+' cannot BEGIN a continuation line, '&&' can, and '+' mid-line is fine.
Corpus census: leading-'&&' occurs 9,570 times tree-wide, leading-'+'
occurred exactly 6 times and all 6 were these. This commit wrote the first
uses of a form the grammar does not admit, which is why no prior file caught
it.

Two things worth knowing beyond the respelling, neither fixed here. The
panic is FATAL TO THE WHOLE SOURCE ROOT rather than to the offending entry:
while that file was in the tree, any dag-rooted claim_batch run died on it,
including runs parsing unrelated fixtures in other directories. So it would
not have presented as a localized witness failure. And the asymmetry itself
- '&&' admitting leading-operator continuation while '+' does not - is a
language-layer gap with no evident reason, whose diagnostic names the token
without saying that the POSITION is what is wrong. Both are filed to the
operator rather than chased here.

I had already hit this exact class earlier in this branch on '==', '!=' and
'<' and did not generalize it to arithmetic. The general rule is: in .dag, a
binary operator may not start a line.

* Carry the pre-verdict mode structurally, so the receipt stops saying WitnessRed

cursor review 50221 requested changes and the finding is real. Verified
against the code: pre_verdict_unverified_claim_result set
expectation_refusal: None and put ExpectedRedPreVerdictUnverified only in
`function` and `detail`. batch_failure_mode_and_detail reads
expectation_refusal off the VALUE and otherwise falls through to
falsifier_failure_mode, whose fallback arm is "WitnessRed". So those batches
reached the floor receipt as WitnessRed -> Failed, while the .dag vocabulary
this same PR added said ModeExpectedRedPreVerdictUnverified -> Refused.

The arm was half-wired: non-green result and receipt vocabulary present, the
classification path missing. And the shape of the miss is the one this PR
exists to remove — one fact in two representations, the second recovered by
substring-matching prose. StaleKnownRed already avoids it by travelling as an
ExpectationRefusal; pre-verdict now does the same.

ExpectationRefusal gains PreVerdictUnverified { declared_entries, cause },
the constructor sets it, and detail() is the single place the message is
built rather than a second format! beside the carrier. The enum's doc now
states why it is the ONLY structural route to a typed mode, so the next arm
does not repeat this.

The test was extended to assert the receipt, not the constant: it calls
batch_failure_mode_and_detail and requires the mode to equal the
pre-verdict mode and NOT to be WitnessRed. The previous version asserted
mode-string distinctness and would have passed against this defect
untouched — the same too-narrow-assertion class as the earlier
identity-join and regression-control findings, three for three in this PR.

Unverified locally: two more equivalent SIGKILLs on the cold full-lib
rebuild, so I stopped per the two-kill rule. Two things a compile would have
caught were checked by reading instead: ClaimResult has no Clone derive (so
the record is built from a second constructor call, not r.clone()), and the
matches! borrows &r.expectation_refusal rather than moving it.

* Add permanent cross-realization fixture law for cgroup membership.

Parser and bash runtime scan must agree on all four fixture outcomes;
mutation sketches on both sides prove drift reds without shipping defects.

Co-authored-by: Cursor <cursoragent@cursor.com>

* delete the dead red_receipt_other_class helper the naming-hygiene walk refused

Run 31219557841's regen job refused on the pre-plan witness naming walk:
red_receipt_other_class is a plain fn in a *_test.dag unreachable from every
test fn, which is silent de-enrollment.

Deleted rather than promoted. It has zero call sites, and the changed-signature
claim it looks like it should carry is already covered on the correct side --
the receipt stays red_receipt (the current run) while the STORED STATE's
signature varies via signature_of_fixture(component: other_failing_component).
Varying the receipt would exercise the wrong half of the comparison. So this is
a dead scaffold, not a missing witness.

Worth naming because a local run cannot find this class: claim_batch executes
the roster handed to it, built by grepping `test fn`, so a plain fn no test
reaches is outside that denominator by construction. The suite reported 52/52
before and after -- true both times, and silent on exactly the thing that
failed to enroll.

Decision suite 52/52 after the deletion, PASS-counted against roster.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Extend the fifth site: the pair-agreement match inside the split module

regen refused on b73bead with a located non-exhaustive match:

  floor_component_receipt_document.dag:443:7 — missing variant(s)
  ModeStaleKnownRed, ModeExpectedRedEvidenceAbsent,
  ModeExpectedRedPreVerdictUnverified

My merge commit enumerated FOUR places a mode must appear and extended all
four. There were FIVE. The fifth is component_outcome_agrees_with_failure_mode,
inside floor_component_receipt_document.dag — the module the #7928 split
CREATED. That is not a different mistake from the one the merge was guarding
against; it is the same one, one module further along: I enumerated the
consumer roster visible from the module I started in, and the split had moved
consumers as well as declarations.

Fixed by enumerating rather than counting. A scan for every match over the
coproduct corpus-wide finds four (two of which are the outer/inner halves of
this one nested match), and re-running it after the fix reports zero
remaining non-exhaustive sites. Verified further by parity: all eight modes
now appear at exactly six structural sites each, in lockstep —
type, seed reader, tag table, wire reader, outcome_of_mode, pair agreement.
ModeUnrecorded's five is correct and not an omission: it is deliberately
absent from the seed reader, which never observes "no mode was recorded",
and failure_mode_wire_reader_note already records why.

The three new arms delegate to outcome_matches_implied_mode exactly as the
other non-Unrecorded arms do, so the pair check stays derived from
floor_component_outcome_of_mode rather than restating which pairs are valid.
No wildcard: a catch-all here would have suppressed precisely the refusal
that located this, and five of six arms being identical is what makes that
tempting.

Worth stating because it is the good outcome, not the bad one: the arms are
real and that match was genuinely non-exhaustive without them, so the
typechecker refused and named both the variants and the line. A count-based
check would have said nothing.

* review 50351: throttle the observer path with the same window as the red path

alert_decision_observer_unavailable's IncidentCurrent arm returned
RefreshObserverIncident unconditionally -- it never compared the heartbeat
window, and never compared the stored signature either. On the four-hourly
falsifier cadence a permanently timed-out observer therefore paged every four
hours, which is the notification spam this module exists to end.

Two notes a few lines apart asserted the guard existed. The code was wrong and
the prose was right, which is the worse orientation: an auditor reading the
policy would have found it documented, coherent and untrue.

The witness was holding it in place. a_repeated_observer_outage_refreshes_the_
same_incident asked the repeat case at last=5000 now=6000 -- a one-second gap
inside a twenty-hour window -- and asserted RefreshObserverIncident, pinning the
spam as the contract. That is why 52/52 was green over it, and why the correct
fix would have read as a regression.

New RefreshObserverIncidentWithoutNotification arm; the observer path now
mirrors the red path. The two notifying cases share one arm because they produce
the same effect -- the red path splits its two only because their MESSAGES
differ, a distinction in what gets said rather than whether to say it.

The suppressed arm carries last_notification_at UNCHANGED. Advancing it to now
looks right (the run did just observe the incident) but restarts the window
every cadence, so the elapsed comparison can never fire and the heartbeat goes
silent permanently -- converting a spam defect into a strictly worse silence
defect that nothing observes. The field records when we last SPOKE, never when
we last LOOKED.

Adding the arm broke alert_decision_notifies and alert_decision_notification as
non-exhaustive, so the closed coproduct made it structurally impossible to add a
suppressing arm while leaving a predicate claiming it notifies.

Roster 52 -> 55, 55/55 PASS-counted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fix #7978 CI drift, stale citations, and golden brace escapes.

Remove merge-appended trailing blank lines from generated workflows (heal artifact), update DESIGN shell→intent citations to the landed cgroup vertical symbols, and escape literal braces in ci_floor_peak emit goldens so dag interpolation does not corrupt expected bytes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: regenerate drifted generated artifacts (ci auto-heal)

* Fix workflow drift: add cgroup locate scaffold comments to pre/post steps.

Heal could not push author-commit-required workflow paths; CI projected
scaffold dissolve-on lines for ci_floor_peak pre/post pipelines that were
missing from the committed ci.yml and falsifier.yml.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Roster unified_membership_outcome_tag_eq for non-fold residue gate.

The cross-realization tag equality fold in extdeps.linux.proc_self_cgroup
uses structural off-variant wildcard arms; enroll it in non_fold_residue so
non_fold_residue_no_unrostered_or_stale passes on the floor.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Brian Searls <11205878+briansrls@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant