diff --git a/docs/cultnet-selection-cut.md b/docs/cultnet-selection-cut.md index a2758c36..f2e34d95 100644 --- a/docs/cultnet-selection-cut.md +++ b/docs/cultnet-selection-cut.md @@ -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: diff --git a/docs/typescript-quic-realtime-cut.md b/docs/typescript-quic-realtime-cut.md index 8c32f679..b68a2dae 100644 --- a/docs/typescript-quic-realtime-cut.md +++ b/docs/typescript-quic-realtime-cut.md @@ -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