Put the campaign maps on main, where the agents reading them actually look

A Hands agent working the selection cut reported that the map had no fix
batch 4 section. It did not, on the branch it could see: the live copy was on
my own working branch, main was a day behind, and the selection work branch
carried a copy from 2026-09-17. It worked from its brief instead and said so,
which is the only reason this surfaced rather than producing a cut built on
five-day-old design.

That is my failure, not the agent's. Self commits the maps and I had been
committing them wherever I happened to be standing. A map is only current if
it is current on the branch the reader has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-22 21:15:21 +02:00
commit 7fd16ec45a
2 changed files with 281 additions and 3 deletions

View file

@ -671,8 +671,183 @@ edit `src/` at the same time.
five-field bodies with an HMAC-SHA256 digest, and they differ in base64
flavour and separators. That is correct and stays. Rust's own doc said so
first.
- **To check:** pass B reports three Caching failures as pre-existing, where
pass A reported one. Soul settles it against `main`.
- **Settled by Soul:** three Caching failures, identical at `b3d9cf7` and
`52082ec`. All three expect a write to a read-only directory to fail, and
the container is root. **Environmental.** Pass B's count was right.
**Soul's whole-cut pass, 2026-09-22** (Opus, at `52082ec`). **Cut 1 may not
merge.**
- **Held:** Networking 238 and 2 skips, Mesh 255, cultnet-rs 213. R-Q holds
(both replica applies, the startup rebuild, and a shard-spanning refusal).
R-O holds against a stranger and across a restart, and the astral key
round-trips. R-H's digest covers every selection term. R-R holds and is
honest. R-A holds. R-F's door is inside `EvaluateAll`, which every path
reaches. The canonical-number door is anchored `\A..\z`.
- **S-1, high: the hop indexes rows by record key alone.** `byKey`
(`CultNetSelectionEvaluator.cs:176-178`) and Rust's `HashMap<&str, &R>`
(`selection.rs:1457`) both keep the last row at a key. CultCache keys are
unique **per schema**, and the Mesh tests already put two schemas at one
key. With rows in one order the selection refuses valid data as
`reference_outside_target`; in the other order it succeeds. Under `cited`
the index is a set of bare keys, so a row of one schema counts as cited
because something cites another schema at the same key.
- **S-2, high: R-T's `cites` half did not land in C#.** C# filters edges by
the citation's key (`:355`) **before** `EnsureWithinDeclaredTarget` (`:359`);
Rust checks first. A many-reference declared to one leaf but holding an edge
into another is refused by Rust and accepted by C#.
- **S-3, high: a selection with both hops loses its `cites` edges in Rust.**
`selection.rs:1541` computes one direction for every edge; C# anchors each
edge by its own hop. No vector covers both hops.
- **S-4, high: the error message fails its own schema.** Nothing decodes real
`CultNetErrorMessage` bytes against `cultnet.error.schema.json`.
`routingHint` is undeclared under `additionalProperties: false`, and nil
`details.asOf`, `details.current`, `code` and `details` all fail their
declared types. The header schema shows the nullable pattern was understood
and simply not applied here.
- **S-5, medium: Stryker leaves 85 survivors** over the cut's two core files:
396 mutants, 310 killed, 85 survived, **75 with no coverage at all**, score
66%. The old "every entry killed" was an artifact of a hand-picked list.
- **S-6, medium: the cursor is malleable in its own position.** The digest
covers the selection but not the body's `asOf`, `ordinal`, `schemaId` or
`recordKey`. A caller holding one valid cursor can rewrite its position and
reuse the digest.
- **S-7, medium: R-I's claim is false.** The vectors compare ids, `matched`,
`hasNext` and edges. They never compare page bytes, `asOf`, `next`, or a
refusal's code, and `rs-written.json` carries no projection vector at all.
- **S-8, low: the out-of-target null branch is dead.** D11 refuses a bare
`ICultRecordRef` in both scalar and many form, so every surviving shape has
a non-null target.
- **S-9, low: `asOf` is the maximum across all shards**, so a single-shard
page advertises another shard's number, and an unrelated write invalidates
a live cursor.
- **S-10, low: `MiniJsonSchemaValidator` silently ignores** `minimum`,
`maximum`, `maxLength`, `maxItems`, `anyOf`, `allOf`, `if/then/else`,
`uniqueItems` and `format`. The committed schemas use `minimum` 34 times,
including `limit`'s bound. Its `pattern` also runs through .NET `Regex`,
whose `$` accepts a trailing newline.
- **S-11, low:** the v1 subscribe refuses a hop before running the door and
always names field `cites`.
- **S-12, low:** the fixture's prose still calls the alias vectors a
cross-runtime defect, which the alias port fixed. Rust's port also drops
`SchemaVersion` and `CompatibleSchemaIds`, so R-E is two rules agreeing on a
subset.
- **Where the evidence lives.** The pass ran against `52082ec`. Its nine hand
probes — eight C# and the Rust both-hops probe that proved S-3 on a matched
pair — are on branch `soul-probe` in the scratch clone `C:\ss5`
(`e0fd672..107ff5f`), **unpushed**. Nothing reached origin and the pinned
checkout at `F:\Projects\CultLib` was never touched. Notes are at
`scratchpad/soul-sel-final-notes.md`. Probes are scratch by doctrine and are
meant to be thrown away; this pointer exists because S-1 through S-3 were
proven there and fix batch 4's tests have to reproduce them independently. If
`C:\ss5` is gone, the probes are gone with it, and that is acceptable — the
findings are recorded here, which is the durable copy.
- **Soul's own corrections, same pass.** It first reported a wedged
`cargo mutants` job on Yggdrasil and asked for it to be killed. Checking the
box rather than its log, the work directory was already gone: what it had
lost was its detached ssh's output. Self had by then killed the process,
which was Soul's own run, so nothing else was affected. **The shape is worth
keeping: a log is not the machine, and Self acted before confirming the
process was actually stalled.**
- **The Stryker triage is owed, after four attempts, none of them Stryker's
fault.** The `dotnet/sdk:10.0` image has no `python3`; the JSON reporter threw
on `CreateDirectory`; two runs with a different output path ran about fifty
minutes without finishing, against three minutes for the mutation phase that
did complete. The completed run stands, and the command is in Soul's notes:
`dotnet-stryker --project GameCult.Networking.csproj --mutate "**/CultNetSelection*.cs" --reporter json --reporter progress --concurrency 4`.
**Read the HTML report Stryker writes beside the JSON**, rather than parsing
JSON in the container.
- **The triage landed on the fifth attempt, 2026-09-22, and it sharpens S-5
rather than softening it.** Most of the 85 are message-string mutations and
are not worth chasing; roughly a dozen are real, and they sit on the rules
this cut exists to enforce:
- `CultNetSelection.cs:439`, `Any()` → `All()` survived. §2's "a comparison
on an alias any reachable schema declares non-numeric is refused" degrades
to "refused only if every reachable schema declares it non-numeric". No
fixture has an alias numeric on one reachable leaf and a string on
another — the exact case the rule exists for.
- `CultNetSelection.cs:387`, `||` → `&&` survived. A `cites.target` with a
schemaId and a blank recordKey, or the reverse, stops being refused.
- `member.IndexAlias ?? member.MemberName` → `member.MemberName` survived at
all four sites (`CultNetSelectionEvaluator.cs:352,384,404` and
`CultNetSelection.cs:445`). §2's role-naming rule for the hop is untested
everywhere it is implemented.
- `CultNetSelectionEvaluator.cs:203,204`, `ThenBy` ↔ `ThenByDescending`
survived on both paths: the `(ordinal, schemaId, recordKey)` tiebreak is
pinned by nothing.
- `CultNetSelectionEvaluator.cs:290-297`, five order flips in `EdgesFor`, all
survived. **R-B's "page-row order, then `(from, role, to)` in code-point
order" has no test pinning a single component**, though R-B exists because
that ordering was wrong once already.
- `CultNetSelectionEvaluator.cs:590-605` has no coverage at all:
`ComputeDigest`'s `cites` and `cited` branches, plus the number-presence
flag at `:584-585`. R-H's collision-freedom rests on a reading, not a test.
- `:673,678` survived: `Cursor.Parse`'s length-prefix bounds checks, on
attacker-supplied base64. `:307,322` survived: cursor positioning under
`descending`, and `ComparePosition`'s schema-vs-key tiebreak. `:710,715,729`
survived: the code-point comparator's surrogate path, despite R-C having a
vector for it.
- **Equivalent, not to be chased:** `Selection.cs:103` `<=`, `:118` `>=`,
`:121` `dot <= 0` are guarded upstream and unreachable for canonical input;
`:103,107` turn `sign * x` into `sign / x` with `x` in {-1,1}; `:109`
Max→Min is a no-op because `PadRight` to a shorter length does nothing;
`:80` is a `RegexOptions` flag; `Evaluator:272` is equivalent because
`full.Edges` is empty without a hop.
- **Soul withdrew a finding of its own in the same pass.** It had reported that
C#'s cursor digest does not sort its lists while Rust's does, offered as a
low-severity intra-runtime difference. `CultNetSelectionEvaluator.cs:637`
sorts by code point, same as Rust. The remark belongs in no fix list.
- **cargo-mutants cannot run on `cultnet-rs` at all.** `schema_discovery.rs`
uses `include_str!("../../../contracts/…")`, which escapes the package, so
the tool's copied tree fails its baseline build. **There is no mutation
evidence for the Rust half**, and the package's own source shape is why.
**Self's rulings for fix batch 4, 2026-09-22:**
- **R-V (S-1). A row's identity is `(schemaId, recordKey)`, never the key
alone.** Both runtimes index rows and incoming edges by the pair. `RecordRef`
already carries both. Tests: two schemas at one key, in both row orders,
give the same answer; `cited` does not match a row of another schema at the
same key.
- **R-W (S-2 and S-3). The hop behaves identically in both runtimes.** The
target check runs **before** any key filtering, and each edge is anchored by
its own hop, so a selection carrying both `cites` and `cited` returns both
sets. C# adopts Rust's ordering for the first; Rust adopts C#'s per-edge
anchoring for the second. Vectors cover both.
- **R-X (S-4 and S-10). A message that fails its own schema is a defect.**
`cultnet.error.schema.json` declares `routingHint` and every nullable field
the way the header schema does. A test decodes real bytes for every code.
`MiniJsonSchemaValidator` **refuses a schema containing a keyword it does
not implement**, rather than ignoring it, and implements `minimum`,
`maximum`, `maxLength` and `maxItems`. Its `pattern` anchors so a trailing
newline cannot pass.
- **R-Y (S-6).** The cursor's HMAC covers the body as well as the selection,
so the position cannot be rewritten. Test: a rewritten ordinal or key is
refused.
- **R-Z (S-7).** The vectors compare **page bytes**, plus `asOf`, `next` and a
refusal's code, and carry a header-projection and a document-projection
vector on both sides. Where a field is legitimately per-server, such as a
cursor, the vector states that and compares the rest.
- **R-AA (S-5).** Triage all 85 survivors by name. The 75 uncovered mutants
mean whole regions have no test; write behavioural tests for them. A
survivor that is genuinely equivalent gets a one-line reason in the report.
**The triage half is done** (see the list under S-5, 2026-09-22). What is
owed is a behavioural test for each named live survivor: the alias
disagreement across reachable schemas, the half-blank `cites.target`, the
four index-alias role sites, the row tiebreak, `EdgesFor`'s ordering,
`ComputeDigest`'s hop branches, `Cursor.Parse`'s bounds, descending cursor
positioning, and the surrogate comparator. The equivalent mutants are
recorded and are not to be chased.
- **R-AB. Make `cultnet-rs` self-contained enough to mutate.** Replace the
escaping `include_str!` with a build script that stages the contracts into
`OUT_DIR`, or another route that keeps every source path inside the package.
Prove it by running cargo-mutants on the crate.
- **S-8:** delete the dead branch.
- **S-9:** `asOf` is the watermark of the shards the selection actually
covers.
- **S-11:** run the door first, and name the field that is actually set.
- **S-12:** correct the fixture's prose, and state Rust's alias reduction
where R-E is described.
**Ledger correction.** Commits 0 and 1 came in at about twice the §14
estimate:

View file

@ -773,7 +773,110 @@ and P3. **Cut 3 still does not close.** The method moved the gaps outward.
checks. Prove it with 5 rounds under 16 burners on Windows.
6. **F6.** Fix the prose.
To Hands (Sonnet) as the eighth fix batch.
**Superseded the same day, 2026-09-22, by the operator's retirement of the
mutation harness.** The operator's words: *"The manual mutation testing turned
out to be a terrible idea, lots of guarding the shape of the code instead of
the behavior. Better to have nothing than a harness that punishes
refactoring."* `scripts/mutate-cultmesh.mjs` and every committed suite are
deleted, with no fallback. Four of the six rulings above named harness entries
(R1, W1–W3, P1, Z1, Z2); those clauses are dead text. What each ruling
protected is not:
1. **F1 stands as behaviour.** The scenarios gain a **release** configuration
on both targets, run against the release library. The release build is the
thing that ships, so a rule the release build never runs is not pinned.
Without the harness there is no R1 entry; the release path is proven by the
scenarios themselves going red when the seam's `#else` shortens the wait,
which Soul checks by hand on a scratch copy.
2. **F2 stands.** The seam wraps the whole argument to `wait_for` — the
duration expression — and `waitseam` asserts it equals
`milliseconds(host argument)`. The assertion is the pin; W1–W3 are Soul's
hand probes, not committed entries.
3. **F3 stands.** `pollhammer` also runs against a runtime holding live
objects: an open connection to an unroutable address, plus a listener and a
stream where the fixture can make them.
4. **F4 stands.** Any `timeout_ms <= 0` returns within a small bound, timed
with a generous margin. A negative timeout is probed. The seam's sentinel
moves to a value that cannot collide, or becomes a separate recorded flag.
5. **F5 stands**, and its 5 rounds under 16 burners run **on Yggdrasil**. The
burners that motivated this ruling are what froze the operator's
workstation on 2026-09-22; no CPU burner runs on Starfire again.
6. **F6 widens.** The prose fix now includes removing every reference to the
deleted harness, not only the stale 1300 ms history and the scenario list.
**There is no mutation evidence for the bridge and there will not be one
committed.** It is C++, and no ecosystem mutation tool is wired here. The
bridge's rules are pinned by behavioural scenarios at the layer that decides
them; a mutant is a thing Soul tries by hand on a scratch copy and throws away.
**The eighth fix batch landed** (Sonnet) at `5646ff7`, before the retirement,
and the harness deletion followed at `796433f`. Both are on
`hands/cultmath-erf`, not yet on `main`. What landed:
- the seam wraps the whole duration handed to `wait_for`, and its "unset"
sentinel became a separate `debug_wait_recorded` flag, so a recorded `-1`
cannot read as nothing recorded;
- `pollhammer` runs against a runtime holding a live connection to an
unroutable address (RFC 5737 TEST-NET-1), a stream on it, and a real
never-connected listener;
- a `zerotimeout` scenario pins that `timeout_ms <= 0` returns within 50 ms,
probing 0, -1 and `INT32_MIN`;
- `closerace`'s settle waits for `debug_peak_calls` to reach the poller count
instead of sleeping a guessed duration;
- `kGenerousLateToleranceMs` narrows from 800 ms to 400 ms, so the wall-clock
layer catches arithmetic applied outside the seam's own argument —
`waitseam`'s equality check structurally cannot see that.
The release configuration and the seven mutation entries (R1, W1–W3, P1,
Z1–Z2) landed **inside the harness**, and went out with it two commits later.
The scenario runner and every seam survived untouched: the README and the dev
Dockerfile now document building with `-DCULTMESH_QUIC_BUILD_TESTS=ON
-DCULTMESH_QUIC_DEBUG_ASSERTS=ON` and running each scenario as a direct
argument to the binary. **The release configuration is therefore owed again as
a plain build, not as a harness target**, and R1's substance — that the
release macro is unexercised — is a Soul hand probe now.
**The Linux leg ran on Yggdrasil, 2026-09-22** (Sonnet), against
`hands/cultmath-erf` HEAD, in an image built from the committed
`scripts/quic-native-linux-dev.Dockerfile` (pinned `debian:13`, tagged by
content hash `cultlib-quic-native-dev:13d7fbd109fc`). No repo change was
needed and none was made.
- **Asserts build, all ten scenarios pass**, 20 rounds each: `closerace`,
`holdclose`, `holdtimeout`, `polltimeout`, `pollbusy`, `pollhammer`,
`zerotimeout`, `payloadfit`, `latecall`, `waitseam` (11 probes, 40 repeated
polls on one runtime).
- **Release build, asserts off, all four required scenarios pass**:
`polltimeout`, `pollbusy`, `pollhammer`, `zerotimeout`.
- Worst overshoot anywhere was 4 ms, against `kGenerousLateToleranceMs` of
400 ms. **F1 is closed on Linux**: the build that ships now runs the timeout
rule.
**A defect in the README's own worked example, found by running it.** The
example builds the test binary and runs it directly, and every scenario dies
with `libmsquic.so.2: cannot open shared object file`. Both the bridge and the
test executable carry `INSTALL_RPATH "$ORIGIN"` and expect the MsQuic runtime
beside them in `bin/`, but nothing puts it there for a test build — only
`build-quic-native.sh`'s packaged output directory gets that copy. Hands
worked around it with a `cp` after each build and correctly did not treat it
as a bridge defect. **This is the documented path, written in `796433f` when
the harness was deleted, and it has never worked.** A documented path that
fails on first use is worse than no documentation: it was published in the
same commit that removed the working one.
**Still outstanding for Cut 3:**
- The **Windows release configuration**. It lived inside the harness and left
with it, exactly as Linux's did; Linux has now been re-run without the
harness and Windows has not.
- **F5's stress under load cannot be run on Windows at all.** It wants five
rounds with sixteen CPU burners, and those burners are what froze the
operator's workstation on 2026-09-22. `closerace` is Windows-sensitive (it
went red under load there in four of five rounds), so the check matters most
on the platform where it is now forbidden. **This is an operator fork, not
something to route around:** either Windows stress finds a host that is not
Starfire, or the check is accepted as unproven on Windows and recorded that
way.
**Cut 1 Soul findings, 2026-09-16.** Held: every test count, both negative
greps, all twelve mutations rerun and killed, the control catching a