Skip to content

fix(benchmark): an UNGRADEABLE grade is an ABSENCE, not a capability zero - #2281

Closed
joelteply wants to merge 17 commits into
canaryfrom
fix/ungradeable-is-an-absence-not-a-zero
Closed

fix(benchmark): an UNGRADEABLE grade is an ABSENCE, not a capability zero#2281
joelteply wants to merge 17 commits into
canaryfrom
fix/ungradeable-is-an-absence-not-a-zero

Conversation

@joelteply

Copy link
Copy Markdown
Contributor

The defect

SweGradeResult.error documents its own contract in its doc comment:

a result with error is an ABSENCE, not a zero, and must never be tallied as a failed attempt

The grader earns that honestly — for the env class it re-runs the pristine tree before declaring a fault, so a genuinely broken patch is never mislabelled. Then both consumers dropped the classification:

  • benchmark.attempt.end / benchmark.autograde published resolved=false gate_ok=false and nothing else. Every wire consumer (probe router → rooms, exam-room widgets, pulse monitors) reads that as a citizen who tried and lost.
  • fold_run_card read the RESULT's infra_error but never the GRADE's error, so the board folded resolved: false + phase failed for the same runs.

The attempt loop already broke correctly on g.error.is_some() — attempts 2 and 3 were never burned. The loss was purely in what got published, which is the only part anything downstream can read.

Measured

On this box, 2026-08-13: 8 of 36 distinct instances (14 of 91 receipts, 22%) grade UNGRADEABLE — requests, pylint, pytest, sympy. Every one of those zeros was indistinguishable from a capability failure on the wire, so the denominator of any rate computed off this stream was poisoned.

Found by digging into sympy__sympy-11400: p2p 0/29 on the pristine tree — the suite does not run in that environment at all.

#380 / #383 own fixing the environments. This PR owns never again reporting their faults as scores.

Change

  • attempt.end + autograde carry ungradeable + grade_error
  • infra_error takes the grade's error too — one field meaning "no valid verdict, and why", fed by both sources rather than a second parallel field
  • resolved returns to None when ungradeable — the same "no verdict" a pre-grade card carries, because that is the truth
  • new phase ungradeable, ordered ahead of failed (a run can carry both a failed marker and an ungradeable grade; the absence is the truer of the two)

Test

Asserts absence-not-zero on the real sympy-11400 shape, with a positive control (same shape, no grade error) that must still fold as a capability zero — so the test cannot pass by simply never reporting failure.

cargo test -p continuum-core --lib --features metal,accelerate run_projection → 6/6 pass.

Same class as #384/#386 one layer up: those classified INFRA at the solve level; this classifies at the grade level and gets it onto the wire.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

joelteply and others added 15 commits August 13, 2026 08:20
… as text (#274/#396)

Joel, 2026-08-13: "Always use uuid and never corrupt them with random prefixes" and
"UUID's are NOT strings. If you're using strings for id you are writing slop."
Both landed on code written minutes earlier, and the grep that followed found the
same defect older and wider than my one file.

Three fixes, in order of how badly they were wrong:

1. The four shipped recipe ids were hand-drawn patterns (c0de0001-0000-4000-...),
   a name wearing a UUID's costume — readable, collidable, and fake. Replaced with
   genuine v4s in both the authored JSON and the `shipped::` constants, written
   grouped (0xfed332c3_383c_45bb_...) so the two are checkable by eye.

2. comms::{MessageId, CorrelationId} were `String` newtypes. `MessageId::new("msg-1")`
   let any caller invent a namespace that collides with every other caller's. Both
   are now `Uuid`: MessageId::new() MINTS (no caller-supplied form exists), and
   CorrelationId::of_exchange(id) states the derivation the old
   `CorrelationId(id.0.clone())` left implicit. Still distinct TYPES — the compiler,
   not a naming convention, is what stops one being passed as the other.

3. EndpointId is deleted. Its values were `EndpointId::new("browser")` and
   `("rust-core")` — a client-kind label standing in for an identity, and with it
   the assumption that the web client is a distinguished endpoint. It is one client
   among many (mobile, SDK, TUI, another node's core). TransportEnvelope.source and
   .target are now PeerId, the substrate's one actor identity per identity/mod.rs.
   Zero callers outside comms/mod.rs; the stale generated binding goes too.

Also closes the on-disk authoring hole the required `id` field opened: a recipe file
that names no id now gets one DERIVED from its purpose (RFC 4122 v5 under a frozen
namespace), so "author a file, zero code" keeps meaning zero code — no uuidgen — and
every node that loads the same file agrees on its identity with nothing to reconcile.
A recipe that DOES carry an id keeps it verbatim.

Tests: comms 23 pass (envelope wire shape now asserts ids round-trip as their own
UUIDs; minted_message_ids_are_unique pins the collision fix), experience 43 pass
including the previously-failing an_experience_authored_on_disk_needs_no_rust.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…t, not the deploy path

Joel, 2026-08-13: "Headless rust period. No need for node to run everything except
for the web interface which is one of many, including mobile apps/sdk." — and then,
because I kept treating the correction as a code-only matter: "Fix these severe
misunderstandings ... regardless of where they are."

This file is where the misunderstanding REPRODUCES. It is loaded into every agent
session, and it opened with "EVERY TIME YOU EDIT CODE: Run `npm start` (MANDATORY)"
under `cd src` — a directory that no longer exists — plus "ALL Rust binaries MUST be
built via npm start". A fresh agent under amnesia reads that and concludes Node runs
the system. It doesn't, and `continuum --help` has been saying so in its own first
line the whole time: "build + run the headless Rust core".

Rewrote the CRITICAL WORKFLOW section around what is actually true, and swept the
other 11 Node-as-deploy claims scattered through the file:

- The core is Rust and boots with no Node. Node builds the WEB desktop, which is one
  client among several (mobile, SDK, TUI, MCP, another node's core over the grid).
  Named the consequence, because it already cost us: a feature that lives in a client
  exists only for that client — how voice ended up web-only with every other citizen
  structurally mute (#58). Behaviour goes in the core; clients render.
- The deploy path is `continuum reboot` (Rust build + relaunch + running-SHA verify),
  with `deploy-verify` and the version trio called out — that verification exists
  because a reboot once shipped a stale binary and reported success (#194).
- `cargo build` stays discouraged for the RIGHT reason (a hand-built binary exists
  only on your machine, and a fresh clone must work with no manual steps, #291) —
  not the old implication that Rust must be built through npm. `cargo check` is
  named as the correct type-check-while-you-work tool, with the shared CARGO_TARGET_DIR.
- `npm run build:ts` now says what it actually covers: the web client, and nothing
  about whether the core compiles.
- Flagged `./jtag` inline as the legacy Node CLI. Left the invocations that follow,
  since their command NAMES are still accurate — it is the driver that is stale.

No code change; the one surviving "npm start" is the sentence warning you off it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ed to a binary that does not exist

Joel: "Cu conflicts with some Unix program. Fix for all cases including windows"
and "It's a bug Claude" — it is, and worse than doc drift.

`cu` is /usr/bin/cu (UUCP call-unix) on every Unix. `uu` — the double-U of
contin-UU-m — is THE official short alias, and start-server.sh has installed
exactly that (plus a squatter guard) since 2026-08-01, with a comment naming this
very collision. Nothing in the tree installs `cu`. But the REFERENCES never
followed:

- Six benchmark harnesses defaulted to `~/.continuum/cache/cargo-target/{release,debug}/cu`.
  Neither file exists — the only built CLI is `release/continuum`. So the default
  resolved to nothing and every default-args run died at the first invocation.
  matrix.py even carries a comment about this exact class of failure biting once
  before (2026-07-22, stale debug-only default silently no-opping the sweep).
- Fixed by RESOLVING rather than renaming: a shared `_resolve_cli()` prefers what
  is actually installed on PATH (`uu`, then `continuum`) and falls back to the
  release build — so it works from a fresh clone, an installed box, or a dev tree,
  on any platform, instead of hard-coding one machine's layout.
- Flag renamed `--cu` → `--uu` with all in-repo call sites updated
  (sweep_all → matrix → headtohead → preflight_gpu chain).

Also swept 53 `cu <command>` occurrences in docs, Rust comments, and generated-TS
doc comments to `uu`. Left the ones that are ABOUT the collision (memory-bridge
README's "never bare `cu`", WAKEUP-AND-JOIN's rename note) — those are correct as
written, and legacy/ stays quarantined.

README dev section reworked in the same pass: the boot path is `continuum start` /
`continuum reboot` / `continuum ping`, with Node named as what it actually is —
the web client's build dependency, one client among mobile/SDK/TUI/MCP, not the
thing that runs the system.

Verified: all six harnesses compile (py_compile), cargo check clean, zero
`target/{release,debug}/cu` paths left in the tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ilently swallowing the job

Joel: "Fix all nodejs and python dependencies that are breaking our headless rust
core ... we must find all smell all the time and never ignore it."

AUDIT RESULT FIRST, because one half is good news:

  Node spawns from the running core: ZERO. `Command::new("node"|"npm"|"npx"|
  "deno"|"bun")` has no occurrences anywhere in continuum-core. The core is
  genuinely headless Rust at runtime; Node exists only to build the web client.

  Python spawns from the running core: FOUR sites, and neither kind was honest.

1. THREE were vestigial and were hiding untested code. `file_engine.rs` had three
   `#[test]`s opening with `if Command::new("python3").arg("--version").output()
   .is_err() { return; }` — a skip-guard from when the syntax gate shelled out to
   `python3 -m py_compile`. That interpreter is GONE: the production path is
   `code::syntax::validator_for` → `unbound_calls`, pure Rust. So on any box
   without python3 — a CI runner, a fresh clone — three tests reported PASS while
   asserting nothing at all. Guards removed; all 55 file_engine tests pass without
   an interpreter present, which is the proof they never needed one.
   Also corrected the doc on `introduced_undefined_calls`, which still told the
   reader the analysis returns None when there is "no python". There is no python.

2. ONE is a REAL runtime dependency, and it was failing silently. `forge/start`
   spawns `python3 <alloy_executor>` — a script that lives in the SIBLING
   sentinel-ai repo, which a fresh clone of continuum does not have. When
   `find_alloy_executor()` returned None the handler used pid 0 and wrote
   `state: "queued"` — indistinguishable from a job legitimately waiting its turn.
   So on every machine that had only cloned this repo, `forge/start` returned
   SUCCESS for work that nothing would ever run. Now it fails loud, names the
   missing script, says where to get it, and states plainly that the job was NOT
   queued. The dependency itself is still there — excising it to Rust is #52/#99
   — but it can no longer pretend to have worked.

The pattern in both: a Python dependency that had already been removed or had
never been satisfiable, still shaping behaviour through a stale guard and a
fallback. `[[fallbacks-are-illegal-fail-loud]]`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…, and the compiler now knows

Joel: "UUID's are NOT strings … Well defined and named structs by reference must be
used." Chasing that through persona_id turned up something sharper than the count.

THE FINDING. `PersonaWorkspaceRegistry::resolve_persona` exists specifically to close
what its own doc calls "the loose-`String` id boundary … the defect class that fed a
dead id to a doomed eval." It has SEVEN call sites. SIX are its own tests. ONE is
production (eval.rs). Against 55 `persona_id: String` fields. The check was correct
and essentially nothing called it — a correct check nothing calls is nastier than a
missing one, because it reads as covered.

Nothing forced the call, because both sides were `String`.

THE FIX — two types, one door:
- `PersonaRef` (new, in identity/): what a CALLER writes — full UUID, 8-char short-id,
  or name. Explicitly NOT an identity. Its only accessor is `as_str()`; there is no
  `as_peer_id()`, because a name is ambiguous, mutable, and meaningless without a
  roster.
- `PeerId` (existing canonical actor identity): what everything downstream holds.
- `resolve_persona(&PersonaRef) -> Result<PeerId, _>` is now the ONLY bridge. Taking
  the newtype rather than `&str` is what makes resolution unskippable.

`From<PeerId> for PersonaRef` exists (an identity is always a valid reference to
itself); the reverse deliberately does not — it requires a roster.

Wire shape is unchanged: `#[serde(transparent)]` over the same string callers already
send, so no client, recipe, or stored payload changes. Short-id and name PX (#161)
keeps working — that ergonomics is the whole reason a reference type has to exist
separately rather than everything becoming a UUID.

Converted, types pushed DOWN rather than laundered at the seam:
- `CognitionEvalParams.persona_id` → `PersonaRef`
- `restore_persona_workspace(&PersonaRef)` (was `&str`)
- `append_failed_ledger(&PersonaRef, …)` (was `&str`)

HELD, and stated rather than fudged: `CognitionEvalResult.persona_id` stays `String`.
The struct derives `Default` across 21 fields and a persona reference has no sensible
default — an empty one is a nonsense value that reads as a real answer. The real fix
is splitting the fire-and-poll HANDLE from the completed RESULT (a handle knows only
the requested ref; a result knows the resolved id), which is its own slice. Inventing
a default to satisfy the type checker is the `unwrap_or` reflex: compiler quiet,
runtime wrong. The reason is recorded at the field.

Tests: persona_workspace 10, eval 22, identity 23 — all pass; full lib test build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… 21 sites

Second slice of the id-typing migration. All ten `commands/memory/*` params plus the
`MemoryManager` API they call now carry `PersonaRef` instead of `String`/`&str`:
append_memory, append_event, load_corpus, has_corpus, get_corpus, multi_layer_recall,
consciousness_context, persona_db_handle, hydrate_corpus_if_missing.

Types went DOWN into the layer rather than being unwrapped at each call — the whole
point of the previous slice. `.as_str()` now appears only where the value is genuinely
being USED as text (a map key, a `starts_with` shape check, a directory handle), never
to satisfy a signature one call later.

Two `Default` derives removed (`ConsciousnessContextParams`, `LoadCorpusParams`)
rather than giving `PersonaRef` a default. No caller used `::default()` on either, and
an empty persona reference is a nonsense value that reads as a real answer — same
reasoning as the eval result field held in the previous commit.

What this makes visible, and does not yet fix: these commands still never RESOLVE.
They accept a reference and hand it straight to the storage layer as a key, so a name
or short-id reaches the DB unresolved. That was invisible while everything was
`String`; it is now legible in the signatures. Wiring `resolve_persona` into the memory
command path is the next slice (#164/#396).

Tests: memory 219, rag 150 — all pass. Full lib test build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…rsona/*, cognition/observe

Third slice. Seven more command param structs stop typing a persona reference as
`String`: agent/solve, persona/identity/{get,set}, persona/instances/{get,despawn},
persona/wall/pin, cognition/observe (params + the `assemble` signature + its `Meta`,
so the type is consistent through the result rather than converted on the way out).

Same discipline as the memory slice: `.as_str()` appears only where the value is
being USED as text — `id_resolve::resolve` takes a `&str` by design because it also
serves rooms and cards — never to satisfy a signature.

Running total across the three slices: 39 persona params + the memory API + the
resolver itself. `persona_id: String` is down from 55 to 16 in the crate, and every
one that remains is an internal struct holding an already-resolved id (those want
`PeerId`, the next slice) rather than an unresolved caller reference.

Tests: full lib test build clean, commands suite passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…rce path carries it down

Fourth slice. `CognitionTraceParams`, the four other introspect params,
`CognitionReplayParams` + result, `RagComposeRequest`, and `DatasetFromTurnsParams`
now carry `PersonaRef`.

The RAG one went two levels deep rather than stopping at the param: `load_source`,
`load_memory_source`, and `load_consciousness_source` all take `&PersonaRef` now, which
deleted the two `&persona_id.into()` conversions the memory slice had left at those
call sites. That is the shape to aim for — when the type reaches the bottom, the
adapters in the middle disappear rather than accumulating.

`persona_id: String` in continuum-core: 55 → 8. Every remaining one is an internal
RECORD (memory/types, should_respond's AIDecisionContext, live/types, projection,
shell_types, ai/types, sentinel) holding a value copied from a param.

Those are deliberately NOT converted to `PeerId` yet. They hold whatever the caller
sent, and nothing on those paths resolves — typing them as an identity today would
assert something the code does not do, which is worse than leaving them `String`.
They become `PeerId` in the same slice that wires `resolve_persona` into those paths,
not before (#164/#396).

Tests: replay 3, rag 18, full lib test build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…— the stub's premise expired

`StubAircCitizen::subscribe_all_rooms` was an `unreachable!()` justified by a comment
that said no test drives it. That was true when written and stopped being true at
bf11a66 (#398 slice 3), which gave `PersonaSupervisor::materialize` a
`subscribe_all_rooms` call to wire the doctrine/wall cache invalidators. Every
supervisor test that materializes an adapter has been dying in that panic since —
5 of the 10, and they only surface on a FULL-suite run, which is how they sat unnoticed.

Found while verifying my own id-typing slices: full suite came back 7073 passed /
5 failed, and the first question was whether I caused it. I did not — the diff of my
five commits touches `commands/persona/*` only, never `persona/supervisor.rs` or
`persona/airc_citizen.rs`, and `git log` on those two files points at #398.

Fix: return `AircError::Transport` instead of panicking. This is NOT a fallback —
the caller already handles that exact case explicitly (keeps both sources uncached,
"correct, just slow", logs loud), so the tests now exercise the real degradation
branch rather than aborting, and a stub still never pretends to hold a live stream.
An empty stream WOULD have been the fallback: it would have looked like a working
subscription that silently never invalidates.

Comment rewritten to state what is true now, including why the old assertion was
right when it was written. An assertion that outlives its premise is worse than no
assertion — it reads as a guarantee.

persona::supervisor: 10/10 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…e panic I removed

`stub_subscribe_panics_loudly` existed to prove the `unreachable!()` fired, so removing
that panic left the assertion failing for the right reason. Rewritten to pin what the
contract actually is now:

  Err(Transport) — required
  Err(anything else) — fail (the caller branches on Transport specifically)
  Ok(stream) — fail LOUDEST, because that is the real fallback: a stub handing back a
               stream looks like a live subscription that silently never invalidates

Matched rather than `expect_err`d because `FilteredEventStream` is not `Debug`.

FULL LIB SUITE NOW GREEN: 7078 passed, 0 failed, 42 ignored. Before this session's
last two commits it was 7073/5 — five supervisor tests panicking since #398 slice 3,
only visible on a full run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ARED, or the build fails

Joel: "eliminate all smell or you will copy it." That is literally the mechanism —
a model reading this tree learns its conventions FROM it, and `persona_id: String`
was 55 sites teaching that ids are text. I did exactly that this session: minted
`c0de0001-…` fake UUIDs because fake-looking ids were already normal here. Prose in
CLAUDE.md does not stop that. A failing test does.

Three tests in identity/mod.rs, running on every PR via the existing
`cargo test -p continuum-core --lib` workflow (no skip pattern matches them):

1. `every_string_typed_identity_field_is_declared` — walks src/, finds any
   `<identity-name>_id: String | Option<String>` field, and fails unless it appears
   in LOOSE_IDS. Comments stripped first, so a doc line can never register as a
   field. The error names the file:field and tells the reader which typed form to
   reach for (`PeerId` for an actor, `PersonaRef` for an unresolved reference, a
   `*Id(Uuid)` newtype otherwise).
2. `no_declaration_outlives_its_defect` — a declaration whose field HAS been fixed
   fails too. Learned directly from `StubAircCitizen::subscribe_all_rooms`, whose
   comment stayed true-sounding for months after its premise expired and cost 5
   silently-failing tests.
3. `declarations_carry_a_real_reason` — every entry starts with external:/pending:/
   defect: and is longer than a shrug, the same bar the module-wiring audit (#344)
   holds.

73 declarations, honestly categorized:
- **external** — LiveKit participant/room ids, log-envelope correlation fields.
  Another system owns the wire format.
- **pending** — ours, but nothing on that path RESOLVES yet. Typing it as an
  identity today would assert something the code does not do. Converts in the slice
  that wires resolution (#164/#396).
- **defect** — `peer_id: String` × 5. These ARE `PeerId`. I attempted the conversion
  in this session and reverted it: `PeerId` has no `JsonSchema` impl and the
  construction sites hold `&str`, so it needs its own slice rather than a rushed
  cascade. Declared as a defect so it stays visible instead of blending in.

POSITIVE CONTROL, because a guard nobody has watched fail is the exact shape of
defect I found earlier today: added `struct PositiveControlProbe { pub owner_id:
String }`, confirmed the guard failed naming `identity/mod.rs: owner_id`, removed it,
confirmed green. The guard also caught 12 sites my own inventory grep had missed —
it is already strictly better than the method I was auditing with.

Full lib suite: 7081 passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…now --from-source

`start` shelled unconditionally into tools/scripts/start-server.sh, which runs a
full cargo build. A "governed" lifecycle verb was a wrapper around a bash file
that only exists inside a repo checkout (BigMama's find, 2026-08-13):

  - a user holding ONLY the installed binary, with no source tree, could not
    start a core at all;
  - the CLI printed one line then went silent for the length of a compile —
    called hung three separate times;
  - the façade's honesty depended entirely on the script underneath.

Same class as the rest of the night's defects: a governed surface over a
hand-rolled path.

Now: `launch_core` locates the installed `continuum-core-server`
(CONTINUUM_CORE_SERVER override → next to the running exe → ~/.continuum/bin →
target/{release,debug} walking up) and execs it directly, keeping the existing
detach/log/pidfile handling unchanged. Building is an EXPLICIT request
(`continuum start --from-source`), never the silent default, and the
no-binary fallback says WHY it fell back and that it compiles first, rather
than going quiet for minutes.

The override refuses loudly when set but not a file — a wrong override must not
look like an absent one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…uard debt cleared)

The working tree could not build tests. This finishes the identity-typing work
that broke it and clears the guard debt it left behind. `cargo test -p
continuum-core --lib --features metal,accelerate`: 7081 passed, 0 failed.

PEER_ID IS A TYPE, NOT TEXT. The production conversions landed earlier; the
test fixtures were left holding `&str`, so the crate compiled and the test
target did not. Fixtures now derive a real UUID from the literal they used to
carry (v5 under NAMESPACE_OID), which preserves every cross-site equality the
tests depended on — "peer-a" in two places is still the same peer, it is just
an id now instead of a name in costume.

CAUGHT BY DOING IT: contracts/verification had the manifest keyed by a derived
PeerId while the EVENT still claimed a raw string signer. The lookup is BY that
signer, so converting one side made every verification test fail as
MissingPeerManifest. One `test_peer_id` / `test_peer_str` pair now feeds both
sides — the same shape of defect the newtype exists to prevent, reproduced in
the fixtures while removing it from production.

REAL HARDENING, not just fixture churn: `AircPeerManifest::validate` had DROPPED
its empty-peer_id check on the theory that typing the field made it impossible.
Typing killed `""`. It did NOT kill `Uuid::nil()`, which is still constructible
and still means nobody — the type narrowed the hole rather than closing it. The
guard is back, at the remaining expressible form.

GUARD DEBT CLEARED: loose_id_guard's `no_declaration_outlives_its_defect` went
red, correctly — four `peer_id` entries in LOOSE_IDS described fields that are
now `PeerId`. Removed. The guard failing here is it working: a declaration list
that can rot into a graveyard is worth nothing.

Also includes the accumulated session tree: the Joel→Operator fixture sweep (no
person's name hardcoded in test data), ts-rs regeneration, and rustfmt across
the crate. That is why this touches ~717 files; the behavioural change is the
identity typing and the nil-PeerId guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
`k3-serving` had no lifetime because it was named for a SUBSYSTEM, so it
never died — a month-old durable subscription whose board reads were all
corpses. Parted from both scopes.

It also sat in TEST FIXTURES, which is the transmission vector: fixtures
teach the next reader the convention, so a fixture naming a room after a
subsystem teaches that rooms are named for subsystems. Renamed to
`bench-swe-run-1` — an activity with a lifetime, which is what a room IS.

The two remaining mentions in work.rs are the INCIDENT RECORD (4 of 12
cards there carried a 134h-expired lease) and are kept, now annotated
RETIRED so nobody copies the naming from them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…zero — carry it on the wire and the board

`SweGradeResult.error` documents its own contract: "a result with `error` is an
ABSENCE, not a zero, and must never be tallied as a failed attempt." The grader
earns that honestly — for the env class it re-runs the PRISTINE tree before
declaring a fault, so a genuinely broken patch is never mislabelled.

Then both consumers dropped the classification:

- `benchmark.attempt.end` / `benchmark.autograde` published `resolved=false
  gate_ok=false` and nothing else. Every wire consumer (probe router → rooms,
  exam-room widgets, pulse monitors) reads that as a citizen who tried and lost.
- `fold_run_card` read the RESULT's `infra_error` but never the GRADE's `error`,
  so the board folded `resolved: false` + phase `failed` for the same runs.

The attempt loop already broke correctly on `g.error.is_some()` (attempts 2 and 3
were never burned) — the loss was purely in what got PUBLISHED, which is the part
anything downstream can actually read.

Measured on this box 2026-08-13: 8 of 36 distinct instances (14 of 91 receipts,
22%) grade UNGRADEABLE — requests, pylint, pytest and sympy. Every one of those
zeros was indistinguishable from a capability failure on the wire, so the
denominator of any rate computed off this stream was poisoned. Found by digging
into sympy__sympy-11400: p2p 0/29 on the PRISTINE tree, i.e. the suite does not
run in that environment at all. (#380/#383 own fixing the environments; this
commit owns never again reporting their faults as scores.)

- attempt.end + autograde now carry `ungradeable` + `grade_error`
- `infra_error` takes the grade's error too — one field meaning "no valid
  verdict, and why", fed by both sources rather than a second parallel field
- `resolved` returns to `None` when ungradeable — the same "no verdict" a
  pre-grade card carries, because that is the truth
- new phase `ungradeable`, ordered ahead of `failed` (a run can carry both a
  failed marker and an ungradeable grade; the absence is the truer of the two)

Test asserts absence-not-zero on the real sympy-11400 shape, with a positive
control (same shape, no grade error) that must still fold as a capability zero
— so the test cannot pass by simply never reporting failure.

This is the #384/#386 class one layer up: those classified INFRA at the solve
level, this classifies it at the GRADE level and gets it onto the wire.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
joelteply and others added 2 commits August 13, 2026 15:16
…UN, instead of caching a tree that grades every attempt UNGRADEABLE

Root cause of the 22% ungradeable rate, glass-boxed on sympy__sympy-11400.

The era pin at the top of this block downgrades pytest to the instance's own
date, which is right when the era INTERPRETER rung (#2253) found a matching
interpreter. When it can't — no Python 3.5 on a modern macOS — the venv falls
back to a modern interpreter while pytest stays pinned to the instance's year,
and the resulting PAIR can be structurally unable to run.

Measured: pytest 2.9.2 (correct for 2016) on Python 3.9.6 dies in
`pytest_configure` with INTERNALERROR before collecting anything — reproduced on
a two-line trivial test, outside the repo, no conftest, no sympy involved. Every
test then "fails", the pristine p2p reads 0/29, the tree grades UNGRADEABLE.
8 of 36 distinct instances on this box are in that state.

So: prove the harness executes before handing the env to a citizen. A version
pin is a GUESS about compatibility; running it is the evidence. `--version` is
not enough — it answers happily for a pytest that dies on any real run.

This REFUSES rather than self-heals, and that is measured, not assumed. The
obvious repair (reinstall a modern pytest) was tried against this exact tree and
does NOT work:

  pytest 8.4.2 → loads, dies in sympy 1.0's 2016 conftest on the `py.path` hook
                 API removed in pytest 7
  pytest 6.2.5 → dies on `py.test.mark.slow`, removed in pytest 4
  pytest 2.9.2 → cannot run on Python 3.9 at all

The band that both RUNS on 3.9 and LOADS a 2016 conftest is EMPTY. No version
choice rescues this class, so an auto-repair would silently trade one void tree
for another.

What DOES work, verified on this tree — 30/30 passing — is sympy's OWN runner
(`sympy.test(...)`) on the same interpreter. That is #383's shape ("django needs
its OWN test runner") generalised: the runner is a property of the repo era, not
a pytest version to search for. `run_tests` is pytest-only today, so until it
grows a runner seam this env genuinely cannot produce a verdict — and it now says
so loudly, naming the incompatibility and pointing at the runner gap, instead of
caching a broken env for every later run to inherit.

[[brittleness-is-the-highest-priority-work-there-is]] — heal what is known-safe,
REPORT what needs a human decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… parallel runner (Joel's ruling, + STOP gate)

The consequence that makes it law: the learning flywheel consumes ROOM TURNS (L1
lifts tool-traces from captured turns, L2 triggers on turn-completion). A detached
agent/solve writing progress/<run>.grade.json produces NO turns — so a citizen can
burn 12 acts, write a patch, take a graded verdict, and none of it reaches the
curriculum. Maximum effort, zero learning. That, not the pass rate, is why
benchmarks have failed.

Names what is parallel today (ledger files, scraped probes, a second board
projection in fold_run_card, private grade.json), the target shape (import
task+oracle only → project into a recipe → the ROOM is the runner → grading is the
activity outcome → learning falls out because the work happened as turns), and a
one-line acceptance test: can a citizen standing in the room perceive the run's
state through the same ViewState pipe the human's screen uses?

Adds a CLAUDE.md STOP gate over benchmark.rs / agent/solve.rs / swe_bench.rs so an
agent arriving under amnesia must read it before touching run state. Written
because that is exactly what happened: this session shipped two correct fixes that
HARDEN the parallel path instead of dissolving it, including adding a field to a
benchmark probe so external consumers could parse it better — which is the smell
the doc now names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
@joelteply

Copy link
Copy Markdown
Contributor Author

Closing as FULLY LANDED: rebasing this branch onto current canary produces zero surviving commits — the ungradeable-is-an-absence work is already in canary (via the merged benchmark commits, e.g. c2a5063's gold-gate family + the #2307 line). Nothing left to merge.

@joelteply joelteply closed this Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant