Skip to content

Adjudicate Publication against the exact head, not by branch lookup - #7498

Merged
briansrls merged 18 commits into
mainfrom
session/proud-swift-104-u1
Aug 1, 2026
Merged

briansrls merged 18 commits into
mainfrom
session/proud-swift-104-u1

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Reworked to the ratified design (operator, 2026-07-31). Still draft — the adjudicator has no production consumer yet, and the finished vertical slice needs more than this.

What changed and why

The previous cut answered "which pull request belongs to this branch." That's a lookup, not a judgment — it reported whatever head the PR happened to carry and left the caller to compare. Every downstream obligation is about one exact commit, so the question Publication must answer is whether this commit is the one on offer.

The arm that motivates the rewrite:

PublicationHeadDiverged { branch, pr_number, pr_head, candidate_head, url }

A PR exists, its branch matches, and its head is a different commit — the ordinary state after a worker pushes a revision while a review is in flight. Under a branch lookup that reads as a successful publication, and every later receipt binds to a commit nobody assessed. It's the outcome a lookup structurally cannot express and the one most likely to occur.

The refusal arm is now constructible — and the old note said it wasn't

The previous cut carried a long note explaining that PublicationRefused could not exist: v1_interpreter turns every non-2xx and transport error into an untyped TypeError, so a failed observation raises rather than returning a value. It named a dissolve-on trigger pointing at an interpreter change.

That was true of a function that called the service itself. Taking observations instead of performing them moves the read to the caller, who reports what happened — so the refusal is an ordinary value and the arm is reachable today, with no interpreter change. The substrate limit was real; the model was standing in the wrong place relative to it.

Arms

arm meaning
PublicationBound a PR names this branch and its head is the commit under judgment
PublicationHeadDiverged PR exists, branch matches, head is a different commit
BranchPublishedWithoutPullRequest branch is on the remote, no open PR names it
BranchNotPublished branch is not on the remote
PublicationObservationRefused a read failed; nothing was learned

Both observation inputs are coproducts (PullRequestsRead | PullRequestsUnreadable, RemoteBranchesRead | RemoteBranchesUnreadable) because an unreadable list must not be indistinguishable from an empty one. Both are negative, and collapsing them points the operator at the wrong action — pushing a branch that may already be pushed.

Draft is carried, never judged. A draft PR is a fine publication surface for the roadmap's own review; readiness is decided by the roadmap's Run Review action, not the forge's flag.

Evidence — 11 witnesses, green by execution

Three could not be stated under the old shape:

  • divergence naming both commits
  • a refusal that is not absence (both are negative, so asserting only "not bound" passes against the collapse)
  • a sibling branch sharing a prefix — origin/dispatch/x-old must not satisfy dispatch/x, which a contains-style match would accept

What remains before this is the finished slice

Nothing calls adjudicate_publication. Still needed: a persisted per-candidate publication receipt, the workflow projection, the UI state and reason surface, and a live positive plus discriminating negative observation. Live observation additionally depends on #7501, without which github.Pulls is uncallable.

🤖 Generated with Claude Code

gunbc-ci-auto-heal and others added 3 commits July 31, 2026 15:29
… endpoint resolve

Two things, and the second was found by executing the first.

THE PRODUCER. roadmap_attempt_workflow_stage_note declares seven
obligations and says Verification, Publication, Review and Goal Audit
"remain desired obligations until independent producers ground them".
roadmap_verify.dag grounded the first. gunbc.roadmap_publish grounds the
next two, together, because they are one observation: a review verdict is
meaningless without the pull request it is about, and the pull request is
only reachable from the attempt's branch.

It is instantiation, not invention — extdeps.github.pulls already modeled
PullRequest/PullReview/ReviewState against the cited upstream. What was
missing was the join: no function anywhere took an attempt's branch and
returned the pull request it became.

The belt OBSERVES; it does not publish (operator ruling, 2026-07-31).
Nothing here pushes a branch or opens a pull request.

ReviewStale is the arm that earns its place. PullReview carries commit_id,
so "approved" always meant "approved THIS commit" upstream; nothing in the
roadmap read it. GitHub only dismisses stale approvals when a branch is
configured to, so an approval predating the current head is a live fact
about an OLDER candidate — which is how a reviewed-then-rewritten branch
passes review without anyone reviewing what shipped. Absent/Refused split
for the same reason roadmap_verify splits Failed from Refused.

THE DEFECT IT SURFACED, which is the larger half. Running the producer
against a live pull request failed with RelativeUrlWithoutBase, and
github.Pulls turned out to have never executed successfully — modeled,
cited, mock-covered, with production callers in gunbc.tools.review, and
zero successful live requests.

find_service_config_string returned a config value only for a string
LITERAL; anything else fell through to authored_name_at, which reads the
SOURCE TEXT at the identifier's span. So `endpoint: default_api_base`
resolved to the string "default_api_base" — the identifier's own spelling —
and that became the base URL.

This is not one service. THIRTEEN service configs in dag/extdeps declare
their endpoint as a data reference (production_api_base x6,
default_api_base x4, docker_default_endpoint x2, edgar_data_api_base), and
every one of them was unrunnable in exactly this way. The nine that spell a
literal worked, which is why the class stayed invisible.

The fix is a single authority rather than a repair: a service-config value
is EVALUATED, exactly like the `path` template two lines below its only
caller. The authored_name_at fallback is DELETED rather than fixed, because
it was the thing that hid the defect — "the configured literal" and "the
name of something I could not resolve" both returned Some(String), so the
failure could only surface downstream as a malformed URL instead of as a
located refusal at the config read. unwrap_or_default() went with it for
the same reason: an empty base produces the identical downstream error.

An unresolvable endpoint now raises the typed, located
InterpError::ServiceConfigUnresolved carrying the spelling that failed, so
the next occurrence names itself instead of arriving as a URL parse error.

GREEN BY EXECUTION against live GitHub, with a discriminating negative:
  observe_publication("session/proud-swift-104-u1")
    -> draft pull request #7498 at c923697
  observe_publication("no/such/branch/exists")
    -> no open pull request (9 open pull requests read)   <- Absent, not Refused
  observe_review(#7498, c923697...)
    -> no review at c923697...                        <- correct; draft PR

The absent case reporting the count it read is what distinguishes a real
negative observation from a failed one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review July 31, 2026 16:30
@gunbai-bot gunbai-bot Bot changed the title Roadmap Ground Publication and Review on real observation, and make a service endpoint resolve Jul 31, 2026
gunbc-ci-auto-heal and others added 2 commits July 31, 2026 16:50
…othing builds, witness the classification

Three findings, all correct. Verified each against the tree before changing
anything.

1. THE STATE PROJECTION WAS A NICKNAME. review_state_key was byte-identical
to gunbc.digest_render's render_review_state — a second authority for one
concept, minted while the first sat one import away. Deleted; reviews_in_state
now compares variants directly (r.state == want), which the corpus already
does elsewhere, so nothing stringifies either side to answer a question about
identity.

2. THE REFUSAL ARMS WERE SPEC WITHOUT BEHAVIOUR, and the reviewer was right
to reject them. PublicationRefused and ReviewRefused were declared, the notes
argued at length that absent and refused must not collapse, and NOTHING
CONSTRUCTED EITHER. That is the shape this repository keeps finding, written
by the same hand that spent the day removing it.

The root is below this module and is worth naming rather than papering over:
v1_interpreter turns every non-2xx response AND every transport error into an
untyped InterpError::TypeError (v1_interpreter.rs, the dispatch_rest response
match), so the `401 => GitHubErrorShape` arms extdeps.github.pulls declares
are dead at runtime. There is no value for a .dag caller to match on, so the
arm was unreachable by the substrate rather than by omission.

Deleted rather than documented. A declared-but-unreachable refusal is exactly
what lets a reader believe a failure is handled when it is not, and a note
saying "this cannot be built yet" beside a type that says it can is the
documented-leak shape. The module now says what it can do: a failed read
RAISES, a caller that needs the distinction makes it at its own boundary, and
the arms return when the interpreter surfaces a declared error-response arm as
a value. That trigger is stated in publication_arms_note.

3. NO EXECUTING CONSUMER. The producer landed beside a manual probe that was
deleted, so its green-by-execution claim rested on evidence nobody could
re-run. Fixed by splitting the call from the judgment: publication_from_pulls
and review_from_reviews are pure and total over values the caller holds, the
observe_* wrappers are one line each and carry no decisions. Every arm is now
reachable from constructed inputs with no network and no token — the same
shape roadmap_verify_witness_test already uses.

Eight claims, green by execution, WITH A DISCRIMINATING RED PROVEN rather
than asserted: perturbing the exact-head filter to ignore commit_id
(r => true) turns three of them red — stale-not-approval, the at-head counts,
and the stale detail — and restoring it returns all three to green. Two
approvals on an older revision must not read as two approvals on the
candidate, and that is now checked rather than described.

The bound is stated in the witness itself: nothing here executes github.Pulls,
so a break in the REST call with the classification intact would keep these
green. That gap belongs to the effect-frontier enrollment, not to this file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The service-endpoint resolution repair in v1_interpreter.rs is not roadmap
work. It was found while trying to run the publication producer — github.Pulls
declares `endpoint: default_api_base`, and the interpreter was sending the
identifier's own source text as the URL — but it is an independent §5 defect
affecting 13 service declarations across 7 upstreams, and it belongs on its own
receipt rather than inside a change about roadmap segments.

Moved to #7501 unchanged, together with the two witness cases that prove it.

This also keeps #7498 a .dag-only diff. Touching v1_interpreter.rs invalidates
the whole v1-compiler-tests compile, which does not fit the build job's
five-minute step cap from a cold registry — that timeout, not any code defect,
is what failed CI on the previous two heads here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot gunbai-bot Bot changed the title Ground Publication and Review on real observation, and make a service endpoint resolve Ground the Publication segment on real observation Jul 31, 2026
@gunbai-bot

gunbai-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Both findings in review 45542 were right, and both are addressed by removing what should not have been here rather than by arguing.

Finding 1 — producers with no consumer, while the projection still says pending. Correct. Two different resolutions, because the two producers were wrong in different ways.

The Review half is deleted, not wired. It was the wrong concept: the roadmap's Review stage is a review the roadmap triggers against the item's candidate, with findings routed back to the worker — a control loop — not a verdict read back from a forge (operator ruling, 2026-07-31). The evidence that this was a real error and not a naming quibble: every review in this repository is produced by a system that never writes to GitHub, so github.Pulls.ListReviews returns an empty list for every pull request here. The classifier would have answered "no review" forever and never once looked wrong.

The Publication half stays, and your finding stands against it — it has no production consumer. I have narrowed the PR's claim to match rather than leaving the overclaim standing: the body now says the classifier is produced and that workflow_segment_from_evidence still renders Pending. Wiring follows the receipt pattern Verify already established, and is the next commit.

Finding 2 — hand-Rust without a receipt. The right call, and the cleanest answer was that the Rust did not belong in this PR at all. It is now #7501, and this diff is two .dag files.

Two corrections to the finding's premises, for the record. auth_declared_but_unwired_witness.rs is not new witness code — it is a pre-existing harness that gained two cases. And v1_interpreter.rs is already a declared seed-retained row, so the question was never whether hand-Rust may grow there, but whether the change earns it. The receipt it carries in #7501: a service config written as endpoint: default_api_base resolved to the identifier's own source text, so the request went to a URL spelled default_api_base — silent wrongness, which §5 places outside the guarantee ladder rather than low on it. Census: 13 service declarations across 7 upstreams, including dag/extdeps/github/pulls.dag:60, so every REST operation this repo declares against the GitHub pulls API was already unrunnable. The fix deletes the fallback rather than repairing it and refuses with ServiceConfigUnresolved { key, spelled }. Inlining the literal was the available workaround and is refused on §3 grounds — it duplicates the value the data row is the single authority for, and leaves the other 12 sites silently wrong.

Splitting it also removes the CI failure on this branch, which was never a code defect: touching v1_interpreter.rs invalidates the whole v1-compiler-tests compile, and that does not fit the build job's 5-minute step cap from a cold registry.

— sent from proud-swift-104

@gunbai-bot
gunbai-bot Bot marked this pull request as draft July 31, 2026 17:36
@gunbai-bot

gunbai-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Back to draft at two approvals, deliberately — the design this belongs to changed underneath it (operator ruling, 2026-07-31).

The substantive reason, not process: publication_from_pulls joins on branch. The ratified design adjudicates publication on candidate SHA == PR head SHA, and makes "a PR exists but its head differs from the candidate" a distinct outcome that has to be actionable. This classifier cannot express that — it selects the branch's pull request and reports whatever head it has, without adjudicating it against anything. The join is at the wrong grain, so the core function gets rewritten rather than extended.

Underneath that is a finding I verified in the tree today rather than taking on faith. Verify is split across two receipt authorities:

  • roadmap-verification-receipt/v1 (gunbc.roadmap_verify) — carries node_id, attempt_key, host, worktree, revision, command_wire, exit, evidence
  • roadmap-validation-receipt/v1 (gunbc.roadmap_validation_oracle) — schema, verdict, detail, oracle observations

The belt reads /validation-receipt.json — the second one, which carries no node, attempt, worktree, or candidate revision. So the receipt actually in the loop cannot bind a verdict to the thing it judged. Widening WorkflowAttemptEvidence with publication_receipt beside it would have cemented that grain instead of fixing it.

What lands first is candidate identity — one immutable candidate per terminal worker turn, minted only from an observed clean committed HEAD — and unifying the Verify authority onto it. Publication then returns as a persisted vertical slice bound to a candidate, which is a bigger and more useful unit than this PR.

The four witnesses and the pure/impure split survive the redesign; publication_from_pulls likely survives as an inner function under a candidate-bound adjudicator. Nothing here is being thrown away, and no Review code returns to this PR.

Thanks to all three reviewers — the approvals were correct about what this diff is. It's the requirement that moved, which is exactly the case the working agreement describes: approvals mean "no blocking defect found," not "this is what was asked for."

The interpreter repair that was bundled here is #7501 and continues independently; it is what makes live publication observation executable at all.

— sent from proud-swift-104

@gunbai-bot gunbai-bot Bot changed the title Ground the Publication segment on real observation Adjudicate Publication against the exact head, not by branch lookup Jul 31, 2026
gunbc-ci-auto-heal and others added 4 commits July 31, 2026 19:10
Two exact-head holes the first rework left, both found in review.

REMOTE REF NAME IS NOT A PUBLICATION. Remote branches were observed as bare
strings, so the branch-without-pull-request arm established only that a ref
with the right NAME existed — abandoning the exact-head thesis on the one path
where no pull request head would reveal the mismatch. A branch left from an
earlier attempt satisfied it. RemoteBranch now carries remote, branch and
head_sha, and the arm splits: RemoteBranchAtExpectedHeadWithoutPullRequest, or
RemoteBranchHeadDiverged naming both shas.

NO SILENT PICK. The binding selected the FIRST pull request whose head ref
matched. Nothing in the model establishes that the match is unique — a branch
can carry pull requests against different bases — so first() picked one by list
order and reported it as THE publication. That is the same silent-pick defect
the namespace lane is eliminating, in a module whose entire thesis is that the
subject must be exact. The match is now keyed by head branch AND expected base,
folded 0/1/many, with PublicationBindingAmbiguous carrying every matching
number.

candidate_head is renamed expected_head throughout. The first trip has no
candidate entity — the pushed sha IS the subject — so carrying that word in the
central Publication API would invite the abstraction back by vocabulary alone,
which is how a rejected thesis returns without anyone deciding to reinstate it.

Twelve witnesses green by execution. Three are new and each kills a specific
wrong implementation: two pull requests on one target must be ambiguous rather
than picked (a first() implementation passes every other claim in the file); a
pull request against another base must not bind; a stale remote branch must
diverge rather than count as published.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-review. The previous commit fixed the pull-request binding and left
adjudicate_remote_branch handling zero and then taking first() of the rest --
so no_silent_pick_note declared a rule the module did not uniformly keep, one
screen below where it was written.

A well-formed ref listing cannot carry two refs of one name, so no live
observation reaches it. That is exactly the argument the note already rejects
for pull requests: the observation is a caller-supplied list and nothing in the
MODEL establishes the uniqueness the code relied on.

The old singleton arm was worse than a silent pick on top of that. count was
already known to be nonzero, so the Absent branch was unreachable -- and if
reached it answered BranchNotPublished, a confident wrong answer to a question
it had not decided. It now answers RemoteBranchBindingAmbiguous: an unreachable
arm still has to name what it would mean, and the honest meaning is that the
binding was not established, never that the branch is absent.

RemoteBranchBindingAmbiguous carries the remote and the match count. Both
projections are exhaustive with no catch-all, so the new arm forced
publication_detail and publication_offers_the_expected_head to be updated
rather than silently defaulting.

The witness states the claim in BOTH orders. With the expected head first the
old code answered RemoteBranchAtExpectedHeadWithoutPullRequest, and with the
stale ref first it answered RemoteBranchHeadDiverged, so a single-order claim
would have been satisfied by list position rather than by a decision. Proven
discriminating by execution: with the ambiguity arm disabled the claim goes red
while both neighbouring remote-ref witnesses stay green, so it rejects
pick-first and pick-last alike.

14 claims green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls pushed a commit that referenced this pull request Jul 31, 2026
…me as the URL (#7501)

* Refuse an unresolvable service endpoint instead of sending its own name as the URL

A service config written as `endpoint: default_api_base` — a reference to a
`data` row rather than a string literal — did not resolve to that row's value.
`find_service_config_string` matched only a literal, then fell through to
`authored_name_at`, which returns the identifier's SOURCE TEXT. So the request
went to a URL spelled `default_api_base`, and `dispatch_rest` finished the job
with `unwrap_or_default()`, turning a missing endpoint into an empty one.

That is silent wrongness, which DESIGN §5 places outside the guarantee ladder
rather than low on it: the caller receives a plausible-looking failure from a
plausible-looking URL, and nothing anywhere says the endpoint was never read.

The fix evaluates the expression and refuses when it cannot:
`InterpError::ServiceConfigUnresolved { key, spelled }` names both the config
key and what the author wrote. The `authored_name_at` fallback is deleted
rather than repaired — it had no correct use, since a config value that is not
a resolvable expression is not a value — and `unwrap_or_default()` becomes the
same typed refusal.

CENSUS, which is why this is a repair and not a nicety: 13 service declarations
across 7 upstreams write `endpoint:` as a bare data reference and were
therefore unrunnable — docker (2), ebay (2), github (4), sec (1), tcgplayer
(4). `dag/extdeps/github/pulls.dag:60` is one of them, so every REST operation
this repo declares against the GitHub pulls API was already broken. The class
stayed invisible because the 9 services with literal endpoints worked fine.

Inlining the literal at each site was the available workaround and is refused
on §3 grounds: it duplicates the value the `data` row exists to be the single
authority for, and leaves the other 12 sites silently wrong.

Witnessed by execution in the existing `auth_declared_but_unwired_witness`
harness, which gains two cases: `endpoint_by_reference_resolves_to_its_value`
asserts the service names the RESOLVED host and not the identifier (the
discriminating positive — it fails against the old fallback), and
`endpoint_resolving_empty_refuses` is the control for an endpoint that resolves
to the empty string. Both were confirmed to execute by perturbing them red and
restoring them green; the harness prints nothing per case, so registration
alone would not have established that they run.

Split out of #7498, where it was bundled with unrelated roadmap work.

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

* Close the two remaining slip paths in the endpoint read (review 45550, review 45552)

Both findings were the same defect this PR exists to fix, surviving in arms the
first cut did not cover.

NON-STRING (review 45550). The emptiness check ran on `format!("{}", v)`, and
Display is total over Value: Null renders as the non-empty "null", an Int as
its digits, a List as its brackets. Any of those would have cleared the check
and been sent as a base URL. The deleted branch narrowed to LitStr; the
replacement lost that narrowing, so the original defect reappeared one layer
down — the old code returned the identifier's source text, the new code
returned the value's rendering. Narrowed to `Value::Str`, which is safe
because there is no branded-string variant: a `NonEmptyStr` data row evaluates
to `Value::Str` exactly as a literal does.

ABSENT (review 45552). A config with no `endpoint` key at all still reached
`String::new()` and sent the bare path as a relative URL. It gets its own
error, `ServiceConfigMissing { key, service }`, rather than sharing
`ServiceConfigUnresolved`: "declared nothing" and "declared something
unreadable" are different authoring mistakes whose fixes name different edits,
and collapsing them is the state-space conflation DESIGN lists as a recurring
failure mode.

Checked before refusing, because refusing is only correct if nothing legitimate
relies on the old arm: 48 of 89 services declare no endpoint, but they are
shell / git / filesystem / systemd transports that never reach dispatch_rest.
The two that looked like counterexamples — extdeps.http.client and
extdeps.bmc.http — carry `transport shell` with curl argv, not `transport rest`.
So no live service loses a working path.

Both new witnesses were proven discriminating by execution, not by
registration. Reverting each fix independently makes its witness fail with its
own message (`EXIT=1`), and restoring it returns the harness to `EXIT=0`. The
absent-key control additionally shows what the old arm produced: an untyped
`TypeError` from downstream URL parsing, which is the unlocated failure the
typed refusal replaces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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>
gunbc-ci-auto-heal and others added 4 commits July 31, 2026 21:08
first() survives in the pull-request fold and sole_remote_branch does the same
job in the remote-ref fold, and both are correct: they run only after count has
proven the match set is a singleton, so they EXTRACT the one element rather than
SELECT among several. The banned move is reaching for the head of a list whose
length was never established, which is where list order silently becomes the
answer.

Found by grepping my own previous commit and being briefly confused by it, which
is exactly what a reviewer would hit. Reading the rule as banning the CALL
rather than the SELECTION would push someone to replace a correct guarded
extractor with something less clear.

14 claims green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Slice 1 of the Publication wiring. gunbc.roadmap_publish's RemoteBranch carries
head_sha -- the field that makes a publication judgment exact rather than
name-shaped -- and NO operation in the corpus could populate it. git.Core
RemoteBranches runs `git branch -r`, whose output is ref names only, and it
reads local remote-tracking refs rather than asking the remote. The model had
gotten ahead of the transport, so the live observation could not have been
written whatever the adjudicator looked like.

git.Core LsRemoteHeads is the ref-advertisement reading: it contacts the remote
and returns object id plus full ref name per line. Kept beside RemoteBranches
rather than widening it, because they answer different questions with different
freshness -- `branch -r` is local and offline, ls-remote is a network read with
its own failure modes, and collapsing them would make every name listing pay a
round trip.

scan_advertised_refs REFUSES on a line it cannot read and carries that line with
its index. Skipping instead would still answer AdvertisedRefsRead with the good
rows, so a consumer could not tell a complete advertisement from a truncated
one -- and a partial ref list is exactly the input that makes an exact-head
judgment answer `not published` about a branch that IS published. That is the
absorbing fallback, arriving as an ordinary negative result.

Proven discriminating by execution: with the fold changed to skip, the two
refusal claims and the located-line claim go red while every parsing claim stays
green.

8 claims green. Not yet wired: converting AdvertisedRef rows into RemoteBranch
and calling the adjudicator is slice 2, which is where roadmap_publish stops
being a witnessed island.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review July 31, 2026 23:37
@gunbai-bot
gunbai-bot Bot marked this pull request as draft August 1, 2026 00:00
@briansrls
briansrls marked this pull request as ready for review August 1, 2026 01:00
@briansrls
briansrls merged commit daec54a into main Aug 1, 2026
10 checks passed
@briansrls
briansrls deleted the session/proud-swift-104-u1 branch August 1, 2026 01:00
briansrls pushed a commit that referenced this pull request Aug 1, 2026
…gv out of the refusal (#7529)

* Type the remote-read failure, name the offending refs, and get the argv out of the refusal

Three defects from #7498 that merged to main, fixed together because they are one
defect seen three ways.

RemoteBranchesUnreadable carried `detail: String`, and three genuinely different
failures were flattened into it at construction: the advertisement was refused by
the transport, an advertised line did not parse, or an advertised ref projected to
an empty branch name. Different remedies -- the first says nothing about the
repository, the second means the remote spoke a format this parser rejects, the
third means a ref survived advertisement but not projection. A caller holding a
sentence could only recover which by matching substrings, which is the
classify-by-prose move the transport-anemia plan exists to remove.

The argv went with it. Each sentence concatenated the literal `git ls-remote
--heads` onto the remote, so the command spelling was load-bearing in a domain
refusal three times. The stable identity is the operation, git.Core.LsRemoteHeads,
and the spelling is one realization of it that changes when the invocation is
derived. render_remote_branch_read_failure is now the only function producing a
sentence and it names the operation; a claim asserts the argv spelling is absent.

advertised_refs_projecting_empty_branch returned an Int, so the identity of the
offending ref was in hand at the moment of the test and discarded before the
refusal was built. It now returns the refs. That is also strictly less work: the
filter already built the set and count() collapsed it. It returns every offender
rather than the first, per this module's own no_silent_pick_note.

Evidence strengthened rather than merely kept. The witnesses asserted substrings
of the flattened String, so they could not distinguish a renderer that lost a
field from a model that never had it. They now assert typed fields, with the
renderer claimed separately -- a rendering regression and a modeling regression
now fail different claims. 24 claims green by execution across both files.

NOT fixed, and the seam is named in-code: RefAdvertisementRefused still carries
exit_code and stderr as Int and String, because the typed process observation
that replaces them is Lane C and in flight separately. The other two arms have no
such dependency. The `as NonEmptyStr` cast keeps its pre-cast guard: refinement
brands are not construction-enforced, the compiler says so out loud at that site,
and checking before the cast is the attainable ceiling until they are.

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

* WIP: Roadmap

* Fix the stale note this PR itself made false

ls_remote_carries_no_exit_block_note said a caller learns whether the read
succeeded from success/exit_code/stderr. This PR removed the success output, so
that sentence became false inside the same diff that falsified it -- the stale
citation class, committed by the change that created it.

Corrected to name exit_code/stderr and to point at the note directly above it,
which is the authority for why the Bool went. Caught in the portfolio review.

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

* Carry the operation identity as an OperationRef, not a free string

review 45876: this PR deleted the argv spelling from three domain refusals and
then re-minted the operation NAME as a free String in the renderer. That is a
smaller nickname in the same place, not the removal of one.

The identity is now the corpus's modeled carrier, and the renderer projects it
through operation_ref_label, added beside OperationRef in v2.std.operation_argv
so the next module naming an operation has one place to reach for.

Why this reduces drift instead of relocating it: shell_transport_operation_rows
enumerates every declared shell-transport operation carrying its own OperationRef,
and ArgvRefusalCause already has OperationNotFound for a ref resolving to nothing.
A stale ref is reachable from an enumeration that exists. A sentence fragment
inside a join([..]) is not reachable from it at all.

Why the ref is WRITTEN here rather than derived, which is the sharper question and
is refused deliberately: deriving it means selecting the row out of
shell_transport_operation_rows, and that builtin reads the live source tree. This
module's whole property, stated in belt_observes_note, is that it adjudicates over
values a caller already holds, so every arm including the refusals is reachable
with no network, no token, and no tree. Trading that for a staleness check would
move the module to ReadsLiveTree. Written down as ls_remote_operation_ref_note
rather than left implied; the check belongs outside, over the corpus's refs at
once.

Evidence, by execution: 10/10 roadmap_publish_observe witnesses PASS, including
the two that assert the rendered text contains git.Core.LsRemoteHeads -- so the
bytes are unchanged and those assertions are the discriminating check on the
refactor. Compile 0 blocking on roadmap_publish, its witness, the operation-argv
corpus witness, and effect_plan_bash_materialize.

Only change to v2.std.operation_argv is an added pure function and its note.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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>
briansrls pushed a commit that referenced this pull request Aug 2, 2026
…#7552)

* Type the remote-read failure, name the offending refs, and get the argv out of the refusal

Three defects from #7498 that merged to main, fixed together because they are one
defect seen three ways.

RemoteBranchesUnreadable carried `detail: String`, and three genuinely different
failures were flattened into it at construction: the advertisement was refused by
the transport, an advertised line did not parse, or an advertised ref projected to
an empty branch name. Different remedies -- the first says nothing about the
repository, the second means the remote spoke a format this parser rejects, the
third means a ref survived advertisement but not projection. A caller holding a
sentence could only recover which by matching substrings, which is the
classify-by-prose move the transport-anemia plan exists to remove.

The argv went with it. Each sentence concatenated the literal `git ls-remote
--heads` onto the remote, so the command spelling was load-bearing in a domain
refusal three times. The stable identity is the operation, git.Core.LsRemoteHeads,
and the spelling is one realization of it that changes when the invocation is
derived. render_remote_branch_read_failure is now the only function producing a
sentence and it names the operation; a claim asserts the argv spelling is absent.

advertised_refs_projecting_empty_branch returned an Int, so the identity of the
offending ref was in hand at the moment of the test and discarded before the
refusal was built. It now returns the refs. That is also strictly less work: the
filter already built the set and count() collapsed it. It returns every offender
rather than the first, per this module's own no_silent_pick_note.

Evidence strengthened rather than merely kept. The witnesses asserted substrings
of the flattened String, so they could not distinguish a renderer that lost a
field from a model that never had it. They now assert typed fields, with the
renderer claimed separately -- a rendering regression and a modeling regression
now fail different claims. 24 claims green by execution across both files.

NOT fixed, and the seam is named in-code: RefAdvertisementRefused still carries
exit_code and stderr as Int and String, because the typed process observation
that replaces them is Lane C and in flight separately. The other two arms have no
such dependency. The `as NonEmptyStr` cast keeps its pre-cast guard: refinement
brands are not construction-enforced, the compiler says so out loud at that site,
and checking before the cast is the attainable ceiling until they are.

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

* WIP: Roadmap

* Fix the stale note this PR itself made false

ls_remote_carries_no_exit_block_note said a caller learns whether the read
succeeded from success/exit_code/stderr. This PR removed the success output, so
that sentence became false inside the same diff that falsified it -- the stale
citation class, committed by the change that created it.

Corrected to name exit_code/stderr and to point at the note directly above it,
which is the authority for why the Bool went. Caught in the portfolio review.

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

* Carry the operation identity as an OperationRef, not a free string

review 45876: this PR deleted the argv spelling from three domain refusals and
then re-minted the operation NAME as a free String in the renderer. That is a
smaller nickname in the same place, not the removal of one.

The identity is now the corpus's modeled carrier, and the renderer projects it
through operation_ref_label, added beside OperationRef in v2.std.operation_argv
so the next module naming an operation has one place to reach for.

Why this reduces drift instead of relocating it: shell_transport_operation_rows
enumerates every declared shell-transport operation carrying its own OperationRef,
and ArgvRefusalCause already has OperationNotFound for a ref resolving to nothing.
A stale ref is reachable from an enumeration that exists. A sentence fragment
inside a join([..]) is not reachable from it at all.

Why the ref is WRITTEN here rather than derived, which is the sharper question and
is refused deliberately: deriving it means selecting the row out of
shell_transport_operation_rows, and that builtin reads the live source tree. This
module's whole property, stated in belt_observes_note, is that it adjudicates over
values a caller already holds, so every arm including the refusals is reachable
with no network, no token, and no tree. Trading that for a staleness check would
move the module to ReadsLiveTree. Written down as ls_remote_operation_ref_note
rather than left implied; the check belongs outside, over the corpus's refs at
once.

Evidence, by execution: 10/10 roadmap_publish_observe witnesses PASS, including
the two that assert the rendered text contains git.Core.LsRemoteHeads -- so the
bytes are unchanged and those assertions are the discriminating check on the
refactor. Compile 0 blocking on roadmap_publish, its witness, the operation-argv
corpus witness, and effect_plan_bash_materialize.

Only change to v2.std.operation_argv is an added pure function and its note.

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

* Repository-bound publication observation: one identity, both reads

Phase 1 slice, part 1 of the vertical.

extdeps.github.github gains github_https_clone_url, projecting GitHub's
documented HTTPS clone URL from a Repository's owner and name rather than storing
it -- a stored URL would be a second representation of a fact those two fields
already fix, free to disagree with them.

WHY THE PROJECTION EXISTS when git accepts the local alias origin equally well:
an alias is a fact about one checkout's configuration, not about a repository.
Two checkouts can point origin at different repositories. A caller reading refs
from origin and pull requests from an owner/name pair has performed two reads that
are only COINCIDENTALLY about the same repository, and nothing in either result
would reveal it if they were not.

observe_publication_for_repository derives the remote from the SAME Repository
that supplies owner and name to the pull-request read, so there is no arrangement
of arguments in which the two observations describe different repositories. That
is the construction answer rather than a convention to remember, and it needs no
signature change to adjudicate_publication -- the caller simply stops passing an
alias.

STATED, NOT PAPERED OVER: github.Pulls.List declares its outputs for the 200 case
so a successful read builds PullRequestsRead here, but a non-2xx does NOT arrive
as a value -- the REST dispatch raises, so PullRequestsUnreadable is not
constructible from a failed call. Deliberately NOT dressed up by wrapping the call
in a fabricated refusal no real failure would produce. The arm stays in the type
because a caller can hold a refusal from elsewhere, and deleting it would push the
ignorance-as-answer conflation into every other producer.

Compile 0 blocking on both modules.

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

* Publication receipt: emit and decode, subject carried on both sides

Phase 1 slice, part 2. The persisted-receipt half of the vertical.

THE KEY IS THE VARIANT, NOT A GRADE. Eight outcomes, eight keys.
publication_offers_the_expected_head folds seven of them to false, and a receipt
storing only that fold could not distinguish a branch nobody pushed from a pull
request whose head moved under a review - the two states with the most different
remedies. The boolean would have been cheaper and would have destroyed exactly the
information the arms exist to carry.

THE RECEIPT NAMES ITS SUBJECT, which is what makes it a receipt rather than a
status: repository, branch and expected head beside the outcome. A reader finding
only an outcome key would have to trust that whoever wrote it was looking at the
same head the reader cares about - and PublicationHeadDiverged, the arm this module
exists for, is precisely the case where a stale receipt and a fresh one differ
while both say something plausible. The expected head is recorded because it is the
QUESTION ASKED, not the answer given.

The decode returns the subject with the outcome for the same reason, in the other
direction: a consumer holding only outcome_key can tell WHAT was judged and not
WHAT ABOUT, so it could not detect a receipt answering a question about a head that
has since moved. Dropping it on the read side would reintroduce the defect one
layer down, in the artifact instead of the judgment.

An unrecognized outcome key REFUSES rather than passing through as an opaque
string. The keys are exactly what publication_outcome_key writes, so one outside
that set means the document came from another emitter or a future version; carrying
it would let a consumer match a value no arm corresponds to and fall into its
default branch - a silent wrong answer at a boundary whose whole job is deciding
whether a commit is published.

Shape follows the validation receipt precedent exactly (schema constant, _json
emitter, string-member reader, _decode over parse_json). Compile 0 blocking;
execution receipt lands with the witnesses.

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

* Prove the receipt round trip by execution, subject and all

22/22 roadmap_publish claims PASS, seven of them new. The three that carry weight
are discriminating rather than confirming:

a_receipt_records_the_head_that_was_asked_about - two receipts differing ONLY in
expected_head must decode to different subjects. A receipt format that dropped the
subject passes every other positive claim in this file and fails exactly this one,
which is why it is a witness and not an inspection.

a_receipt_missing_its_subject_refuses - a document carrying schema, outcome and
detail but no repository is rejected, not decoded with blanks.

an_outcome_key_this_emitter_never_writes_refuses - "published-ok" is the shape of
key a reasonable OTHER emitter would produce, so it probes the boundary rather than
a nonsense string.

the_clone_url_names_the_repository_not_a_local_alias asserts both halves: the URL is
what GitHub documents AND is not "origin". That file's existing fixture_remote is
literally "origin", harmless where the adjudicator treats the remote as a label, and
exactly the value the new binding must never produce - so the claim names it.

Why this run matters beyond the compiles already reported on this branch: --entry
compiles do not run body analyses, so those proved the modules typecheck, not that
the receipt round-trips. This is the evidence.

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

* WIP: Roadmap

* Publication reads a receipt: delete the constant, prove the replacement

Phase 1 slice, part 3. The hardcoded arm is gone.

It returned WorkflowSegmentPending with a fixed sentence whatever had happened,
so a row published at exactly the head under judgment looked identical to one
nobody had pushed. A constant cannot be wrong about a particular attempt because
it is not about any attempt - which also means it can never become right.

Two distinctions it was hiding:

PENDING WAS TWO STATES. No receipt means publication has not been adjudicated -
an ABSENT OBSERVATION. A receipt decoding to branch-not-published is a JUDGMENT
that nobody pushed. Same lamp before, different remedies; the details now say
which.

AN UNDECODABLE RECEIPT REFUSES, it does not fall back to pending. That arm was
the tempting one and it is the absorbing fallback in miniature - the failure
would be indistinguishable from ordinary progress, its frequency zero by
construction, and nothing would ever count it.

Wiring: publication-receipt.json paths at attempt and current-attempt grain
(mirroring the validation receipt), two WorkflowAttemptEvidence fields, the belt
Filesystem.Read, and every one of the 14 construction sites corpus-wide.

23/23 progress claims PASS.

TWO DEFECTS THIS RUN CAUGHT THAT THE COMPILES COULD NOT:

1. The segment key is "publish", not "publication". All five new claims compared
   "" against their expectations and failed. --entry compiles do not run body
   analyses and never reach the test corpus, so 0-blocking said nothing here.
   I also first checked field coverage with a within-30-lines proximity grep,
   which missed four construction sites; replaced with brace-depth matching from
   each site to its actual closing brace.

2. forged_admission_receipt_cannot_complete_environment was VACUOUS, and it is
   not mine. It asserted !(segment_state(.., "env") == "complete") - but "env" is
   not a key either, so segment_state returned "" and the negation was true
   unconditionally. It would have passed if the forged receipt DID complete
   Environment, which is the one thing it exists to forbid. Now asserts the
   segment is found AND not complete; the != "" clause is what stops a future
   key rename from silently re-vacuuming it.

Same green before and after for that one, entirely different meaning.

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

* WIP: Roadmap

* wip: publication producer + subject-checked projection

* wip: publish route + serve wiring

* wip: located decoder refusals, full repository round trip

* WIP: Roadmap

* Remove the throwaway live probe; keep the explicit Open state at the pulls call site

* Delete the second head a bound receipt could carry (review 46116)

ReceiptBound carried its own pr_head, and the decoder read it
independently of the receipt's subject. A document naming this subject
while binding a different pull-request head therefore decoded cleanly,
matched the subject on all six subject fields, and completed Publication
for a head no pull request offered -- the exact class this lane exists to
catch, walked in through the carrier meant to record it.

Fixed by construction rather than by a check (DESIGN section 5). The
variant means "a pull request is open at the subject's expected_head",
so the head it offers IS subject.expected_head -- already in the same
document and already the receipt's filename. The field is deleted, so
the contradictory document is unrepresentable rather than rejected.
ReceiptHeadDiverged keeps its pr_head: there the head is genuinely new
information that appears nowhere else.

The projection reads the same single copy: publication_outcome_segment_
detail now takes expected_head and renders it, so the completed sentence
cannot disagree with the judgment it describes.

Evidence, green by execution:
- a_bound_receipt_binds_the_subject_head_and_no_other and
  a_stray_head_cannot_make_a_receipt_evidence_for_the_head_it_names feed
  the decoder the doctored document the old encoder would have produced
  (a stray pr_head naming the other fixture) and assert it answers with
  the subject's head, never the stray one. Both go red if any reading of
  an outcome-side head is restored.
- the_completed_sentence_names_the_head_that_was_judged asserts both
  directions: the judged head appears, the other fixture sha does not.
- publish 37/37, progress 26/26, belt 56 PASS (9 pre-existing
  no-mock_response failures, unrelated).

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

* WIP: Roadmap

* Establish receipt absence by listing, never by a failed read (review 46148)

Filesystem.Read answers success=false both for a receipt that does not
exist and for one that exists and cannot be read. The producer inferred
absence from that bit, EvidenceAbsent mapped to PublishProceed, and an
I/O fault on an existing receipt would therefore have overwritten
evidence the belt could not read -- a fail-open in the one place this
lane promises not to be.

The repository had already paid for this lesson at the Verify seam:
roadmap_validation_oracle's validation_not_run_note records the same
defect (empty text arriving from three worlds, then the validation
re-run and the evidence overwritten) and the same resolution -- make
absence a state somebody ESTABLISHED, not a value inferred from failure.

Pure half: publication_evidence_for now takes PublicationReceiptSource
(Absent | Present{text} | Unreadable{detail}) instead of (Bool, String),
so there is no longer a spelling for "I could not read it, treat it as
gone". A present-but-empty file reads as unreadable, not absent -- a
truncated write is a fault, and concluding "nothing published" from it
is the same fabrication one step down.

Effect half: belt_publication_receipt_source walks a ladder whose every
absence conclusion is positive. It starts at the attempt state
directory, which dispatch creates and which this producer only reaches
for an attempt whose worktree head it already read, so a failed listing
there is unambiguously a fault. Then: publication dir absent from a
successful listing, or the head's file absent from a successful listing.
Only a listed file is read, so a failed read is unambiguously a fault.
Every failure arm refuses with the path and the host error; none widens.

Line-exact listing membership, so a head sha is not found inside a
longer one nor a receipt inside its own .tmp sibling.

Evidence, green by execution:
- an_unreadable_receipt_halts_rather_than_being_overwritten and
  an_absent_receipt_lets_the_publish_producer_proceed are the
  discriminating PAIR -- the old code proceeded on both, so either
  alone would pass under the defect.
- a_present_but_empty_receipt_is_a_fault_not_an_absence.
- an_absent_receipt_is_absent_and_present_but_unusable_ones_refuse
  separates all four sources at the pure layer.
- an_unreadable_receipt_refuses_rather_than_reading_as_not_yet_published
  asserts the lamp differs from pending.
- listing_membership_is_line_exact_not_substring covers prefix, suffix
  and empty-listing cases.
- publish 37/37, progress 27/27, oracle 36/36, serve 15/15, belt 59 PASS
  (9 pre-existing no-mock_response failures, byte-identical set).

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

* Make the repository identity one authority the five nicknames consume

review 46301 rejected the added gunbc_repository as a fourth spelling
carrying a promise to consolidate later, and it is right to: DESIGN's
recurring-failure list says an honestly-marked scaffold duplicating a
canonical fact is still a violation. A sixth nickname with a note is
not an answer to five nicknames.

It was also worse than the note claimed. There were FIVE existing
spellings, not three: ci_heal_dispatch's owner/repo String pair,
runner_host_deploy's org, review's parameter defaults, review_codex's
own owner/repo pair, and bmc_token_federation's gunb-ai/gunbc slug --
three different shapes for one entity, none of them a Repository.

So the value moves to gunbc.repository as the single typed authority
and every one of the five now projects from it. No bare gunb-ai literal
remains anywhere outside that row.

Repository rather than a String pair because extdeps.github.github
models what the API returns, so owner, name, full_name, private and
default_branch travel as one fact: a consumer needing the default
branch stops guessing main, and one needing the slug stops building it
by concatenation. The instance lives in gunbc rather than extdeps
because WHICH repository this project is, is a fact about the project;
putting it in extdeps would make the dependency model know its dependent.

One hazard is recorded at review's parameter defaults rather than left
implicit: a default argument is evaluated in the CALLER's scope, so a
future caller in a module without the import would die naming a symbol
it never wrote. Safe today only because review_cycle has no in-corpus
caller -- unreachable rather than proven, which is what the note says.

Verified: 0 blocking errors on roadmap_belt_actuate, bmc_token_federation
and runner_host_deploy; ci_heal_dispatch typechecks. review and
review_codex fail on a pre-existing unresolved upsert_tagged_cron_tab
that reproduces identically on the unmodified parent, so it is inherited
rather than introduced -- baselined before attributing it.

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

* List the belt's publication symbols in its import blocks

review 46342 is right that seven symbols the new receipt reader uses
are absent from roadmap_belt_actuate's selective import lists:
PublicationReceiptSource with its three constructors, plus
dispatch_attempt_state_path_for_instance,
publication_receipt_directory_segment and
dispatch_attempt_publication_dir_for_instance.

The review's stated consequence does not hold -- the module compiled
with zero diagnostics on any of those names, before and after, so they
were resolving. But resolving is not the same as being declared, and
HOW they resolved is the problem: by pool membership, because some
other module in the closure already dragged the definer in. That is
the failure class DESIGN records under the import-strip cascade, where
a bare cross-module reference works only while an unrelated import
elsewhere happens to keep its target in the pool, and stops working
when that unrelated file changes. Coverage by coincidence.

So the imports are listed. Nothing about the behaviour changes; what
changes is that the dependency is now stated where a reader and the
graph can both see it, and roadmap_belt_actuate stops disagreeing with
roadmap_workflow_progress, which imports the same symbols explicitly.

Verified with a freshly built binary, because a stale one had already
produced one false green today: 0 diagnostics on every symbol named in
the review; publish 37/37, progress 27/27, belt 60 PASS with the 9
standing no-mock_response failures unchanged. The only remaining
diagnostics are the four inherited filter call-shape errors in
roadmap_presentation, which PR #7592 fixes on its own branch.

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

* WIP: Roadmap

* Model a REST response that is not 2xx as an answer, not an absence

Service operations already declare what each status yields — github.Pulls.List
declares 200/401/404/422/5xx, and twenty-two other extdeps modules declare 204
such arms between them. The v1 seed parses every one of those declarations onto
the operation node as a response_<status> property and then never reads it:
dispatch_rest raises InterpError::TypeError with a rendered "HTTP {status}:
{body}" string for anything at or above 400. So a caller cannot reach the
status, cannot reach the body, and cannot persist either without parsing a
diagnostic written for a human.

This lands the type the realization will project into. Four states rather than
three: a transported request that was answered with a non-success status keeps
both the status and the body it arrived with, because the body is usually the
only place the remote says why; a request that never transported has no status
at all, so none is invented; and a status that arrived over an unreadable
payload is its own arm, so an unreadable body is never reported as an empty one.

status is std.types HttpStatus rather than a fresh Int — the range already has
one authority — and RestTransportRefused deliberately carries no status, since a
sentinel zero would be a plausible-looking value standing where the honest
answer is that the question does not apply.

No realization consumes this yet, and the scope is stated on the carrier so it
cannot be misread as complete: the outcome is opt-in per operation, and a
refusal body stays a String rather than being decoded into the shape the
response block already names. Both dissolve when the response block becomes the
single authority for a result and output derives from its 2xx arm.

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

* WIP: Roadmap

* roster: import list_map

* Move the ANSI roster work to its own branch

An auto-commit captured an in-progress edit to extdeps.render.ansi on this
branch. That work is the terminal control roster decomposition — a separate
repair with its own reviewable argument — and it now lives on
session/proud-swift-104-ansiroster. This branch carries only the REST outcome
model, restoring ansi.dag to main.

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

* Escape the braces in the note that .dag reads as interpolation

The carrier note quoted the diagnostic the realization used to raise, 'HTTP
{status}: {body}'. In .dag a brace followed by an identifier opens string
interpolation, so those were parsed as references to variables named status and
body, and the compile-clean gate refused with two undefined-variable errors.

The escape is \{ and \}. Verified through a CONSUMER entry rather than the
module itself: gunbc compile --entry on a module skips body analyses, so the
clean result I took as verification earlier could not have caught this.

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

* WIP: Roadmap

* Surface opted-in REST failures as data (#7602)

* WIP: REST non-2xx as data in dispatch_rest

* Witness REST outcomes through .dag callers

* Name REST witness scaffold dissolution

---------

Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com>

* WIP: Roadmap

* Revert the auto-commit belt-timer fragment off the Publication branch

An auto-commit (49d377b, "WIP: Roadmap") captured a two-line mid-edit
fragment of unrelated belt-timer work onto this branch: BeltTimerUnit
added to OwnedArtifactKind and its teardown arm. The fragment is
self-consistent and compiles -- a variant with a teardown answer and no
construction site is inert -- so it was not the cause of this branch's
CI red, which is inherited from main (two witnesses in
v1_interpreter_primitive_surface_witness_test.dag, keen-swift-704 lane).

It is reverted anyway because it does not belong here. This branch is
the Publication producer; the belt tick driver is separate work and
lands as its own PR against main.

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

* WIP: Roadmap

* F1 REST transport replay seam (#7610)

* WIP: F1 REST transport replay seam

* Bind REST replay fixtures to realized query targets

---------

Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com>

* Read back which repository the attempt worktree is actually a checkout of

The publication path read a head SHA off a worktree on disk and then
reported it as evidence about a repository, without ever establishing
that the worktree was a checkout of that repository. Both REMOTE reads
were already bound by construction - publication_subject_remote derives
its URL from github_https_clone_url(subject.repository), so they cannot
disagree about which repository they concern - but the LOCAL side had no
such binding, and it is the local side the head comes from.

WorktreeRepositoryBinding has three arms because the failure has three
shapes and only one of them is a mismatch. WorktreeBoundElsewhere carries
BOTH urls, since a mismatch whose message names only one of them cannot
be acted on. WorktreeRemoteUnreadable carries the exit code and stderr,
because "we could not ask" is a different fact from "we asked and the
answer was wrong". An empty stdout resolves to unreadable rather than to
a mismatch against the empty string, following the module's existing
empty-stdout-is-unobserved precedent.

The outcome is PublishEvidenceUnreadable, not PublishDeferred. Deferral
means the belt should look again on the next tick, and neither a wrong
checkout nor an unreadable remote fixes itself by waiting - a deferral
would spend a tick per attempt forever while reporting nothing.

git.Core.RemoteUrlIn existed on this branch with zero consumers, which is
the specification-without-execution shape this lane keeps producing. It
now has one.

Also fixes the parse error the auto-committed mid-edit snapshot (0b5ce9c)
pushed: the nested match arm was short two closing braces, so the parser
ran past the function into the next declaration and reported the Colon it
found there.

Witnesses, green by execution (claim_batch, not compile):
  worktree_bound_to_the_subject_repository_is_recognized
  worktree_pointing_at_another_repository_refuses_and_names_both
  worktree_remote_read_failure_is_not_a_mismatch
  empty_remote_url_is_unreadable_not_bound_elsewhere

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

* Make observe_pull_requests total over the REST outcome

Stacks on the REST replay seam (#7600), which is merged into this branch
so the work can proceed; when #7600 lands on main this diff collapses to
the Publication half.

github.Pulls.List declares outcome: RestOutcome, so a non-2xx now arrives
as a value instead of escaping as a raise. Three consequences, and the
third is the one that mattered:

result.pulls is read ONLY under RestOk. The realization leaves the
ordinary body-derived fields uninhabited on a non-success outcome, so
reading .pulls on any other arm would report the absence as an empty list
of open pull requests - and an empty list is a perfectly ordinary answer
meaning "not published yet". A 401 would have been indistinguishable from
a genuine unpublished state, and the belt would have written a receipt
saying so. The previous unconditional read was correct only because
dispatch_rest raised and the arm was unreachable; making the outcome data
is what makes that line newly wrong.

PullRequestsUnreadable carries a typed cause instead of detail: String.
Four failures reach it and they are not four phrasings of one event: no
usable credential is a local decision taken before a request exists; a
status refusal means a remote authority answered and the body is usually
the only place it says why; a transport refusal means no status exists at
all; an undecodable body is a decoder fault, not an access one. This is
the repair remote_branch_read_failure_note already describes, applied to
the peer observation that did not get it - same shape, same renderer
discipline, and the renderer names github.Pulls.List through its
OperationRef rather than the path and query.

The 401 witness stops asserting a hand-written sentence. It constructed
PullRequestsUnreadable { detail: "401 from the forge" } and grepped its
own string back out; it now constructs the real status refusal and
asserts the status, the body, and the repository each survive into the
refusal detail.

Compiles clean through both consumer entries (belt actuate, publish
witness test). Witnesses for the four new arms are NOT yet written or
executed - that is the next commit, and the live four-case receipt still
waits on #7600 merging so the interpreter half is on main.

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

* Mark the new seed Rust with the repo's hand-Rust gate (review 46616)

DESIGN 7 requires a seed-retained region to be a declared row with a
reason and a migration trigger - countable and prioritizable, never a
silent escape hatch. A comment explaining intent is not that, and both
new regions had only the former.

Two HAND-RUST GATE explicit deferrals, matching the existing pattern at
native_cache_rebase_workspace_dir and resolve_host_tool_program. Each
states what is actually deferred, which is much narrower than the line
count suggests:

  witness frame - the policy is modeled (v2.std.witness_evaluation owns
  the carrier; rest_exchange_resolution owns lookup, equality and handler
  selection, and the interpreter calls back into .dag for the decision).
  Seed-side is the dynamic-extent push/pop, which no modeled construct
  can express while the seed is the evaluator.
  Lane: ROADMAP v1-materialization-kernel.

  REST bridge - every decision is modeled (RestOutcome,
  RestExchangeObservation, rest_bound_invocation_eq,
  rest_exchange_fixture_lookup). Seed-side is projecting them onto the
  declared output record, which needs the interpreter's Value/Node.
  Lane: ROADMAP v1-interpreter-quarantine -> v1-interpreter-delete.

Both deletion conditions are checkable by execution rather than by
assertion, and the REST one is EARLIER than its v1-exit lane: when the
response block becomes the single authority and output derives from its
2xx arm, the opt-in disappears, so rest_outcome_output_field has no field
to detect and deletes outright, taking the status >= 400 raise with it.
Its control is that rest_operation_without_outcome_still_refuses must be
REPLACED rather than kept green, since a Legacy operation with no outcome
field can no longer exist.

evaluate_in_witness_frame_seed_note gains the same receipt, and splits
out what the original note blurred: the two stubs
(witness_diagnostic_rendered_reason returning "" and
evaluate_in_witness_frame always answering WitnessReturned) are fail-open
in the direction that matters - under a pure evaluator a refusal is
indistinguishable from a success carrying an empty reason. Nothing is
wrong today because every consumer runs the realized path, but that debt
gets its own nearer trigger rather than sheltering under a lane whose
trigger is "witnesses emit to native code".

NOT copied forward: the deletion row this pattern points at,
dag/gunbc/v1_deletion_plan.dag ^witness_realization_kernel, no longer
exists - that file's own v1_exit_model_doc records the brick ledger being
retired 2026-07-28. Two live comments in the tree still cite it. These
deferrals name verified-live roadmap node ids instead and record why;
repointing the stale siblings belongs to the lane that owns them.

cargo check -p v1-compiler clean, run locally with
CTRL_BUILD_BYPASS_SHIMS=1 (ctrl-build executes remotely and leaves the
local tree untouched, so a green from it would prove nothing here). The
first cut of the frame deferral was a /// block before a thread_local!
invocation and drew "unused doc comment" - it attaches to no item, so the
marker would have been dropped; converted to //.

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

* Witness the four read-failure causes, and drop a cast that only fails when run

The four arms landed in the previous commit with no executing consumer,
which is the shape this lane keeps producing. Four witnesses now execute
them:

  four_pull_request_read_failures_stay_four_distinct_causes - matches the
  coproduct rather than the rendered sentence, so a future edit that
  collapsed two arms into one phrasing is a compile error instead of a
  silently identical string that keeps the claim green.

  a_status_refusal_renders_operation_repository_status_and_body - asserts
  the operation is named through its OperationRef and the path is NOT,
  following ls_remote_operation_ref_note's derivation for the sibling
  reader.

  a_transport_refusal_carries_no_status_at_all - asserts the absence. A
  sentinel zero would be a plausible value standing where the honest
  answer is that the question does not apply.

  every_pull_request_read_failure_refuses_rather_than_reporting_unpublished
  - the remotes are read-and-empty, so the pull-request side is the only
  thing that can refuse; a cause handled by falling through to the remote
  reading would show up as BranchNotPublished and red.

THE CAST THAT COMPILED AND DIED. The first cut wrote `401 as HttpStatus`
and compiled with 0 blocking errors; four of the five witnesses then
failed on the first run with `cannot cast Int to HttpStatus`. HttpStatus
is `Int where range(min: 100, max: 599)` and a refined position takes the
bare literal - which is what the replay test's denied_observation already
does. The tell I should have read before writing it: `grep -rn "as
HttpStatus" dag/` returned only my own new sites, so I had invented the
idiom rather than followed one. `to_string(status as Int)` went with it;
to_string accepts the refined Int directly.

Executed, not compiled: 5/5 PASS via claim_batch on this tree.

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

* Delete rest_outcome_transported, which had no consumers (review 46657)

One occurrence in the tree: its own definition. A predicate nobody calls
is specification-without-execution, in the PR whose whole purpose is
removing that from the REST surface - so the review's either/or resolves
to delete rather than tag. A disposition tag would have made an unused
predicate declared rather than used, and DESIGN 5 treats a dead scaffold
as a wall-now class rather than something to annotate.

The distinction it drew is real and the type still carries it:
RestTransportRefused is the only arm where no status exists, which is the
difference between "the remote said nothing" and "we never reached the
remote". RestOutcome expresses that structurally, so a consumer matches
the arm it cares about instead of folding four states to a Bool and
losing which of the three transported ones occurred.

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

* Drop three backslash-escaped apostrophes the new lexer correctly refuses

CI red at 280ceed: parse error in roadmap_dispatch_actuator.dag, "expected
expression, found Unknown". The file carried A\'s and B\'s inside a
double-quoted note - an apostrophe needs no escape there, so the sequence
was always wrong. It was introduced by an auto-commit of my own work
(b81026f) and is not on main.

WHY IT BROKE NOW rather than when it was written: #7585 merged, closing
the unknown-escape fail-open. The old lexer passed an unrecognized escape
through silently; the new one refuses it as a located ShUnknown token,
which is what "found Unknown" is. The escape did not become wrong - it
became AUDIBLE, which is the entire point of that PR.

WHY MY LOCAL CHECK SAID IT WAS FINE, recorded because it nearly produced
the wrong report: the binary was built before the main merge, so it still
had the permissive lexer and compiled the file clean. I was one step from
telling the operator this looked like a CI-side problem. Rebuilt against
the current tree, then ran the control on the SAME binary - reintroducing
one escape reproduces the exact CI message, and removing it compiles 0
blocking. Compile-green under a stale binary proves nothing about the
tree CI parses.

Swept every .dag file this branch touches for the same sequence; this was
the only one.

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

* Correct a note this PR's own code made false

`pull_observation_transport_bound_note` still declared that a non-2xx "does
not reach this function as a value" and that "Publication cannot claim a
live trip" -- both true when the paragraph was written and both falsified by
this same PR, which adds `outcome: RestOutcome` to `github.Pulls.List` and
matches its four arms in `observe_pull_requests`. Caught by review 46728.

The paragraph now states what is true and keeps the prior state, because the
history is what names the defect: the raise-on-non-2xx behavior, why it made
the refusal unreachable through a real call, and the live receipt that
closed it -- absent credential refused locally with no request, present-but-
rejected returned 401 with its body intact, valid credential read 30 pulls.
The last is the discriminating control: a mis-wired outcome field would
plausibly have emptied the pulls projection on success too.

Also merges main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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: gunbai-bot[bot] <289086189+gunbai-bot[bot]@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