c37bc263
docs: M15 closed — 12 tasks, 37 commits, suite unchanged at 20/33, soak 10/10, regrade 3/3 at the unit layer, refused dial 15001ms→1ms
a73x 2026-08-12 21:06
Commit message
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -2339,7 +2339,9 @@ the load-bearing arm is `serviceObserver` for M13's reason: `muxd | |||
| 2339 | endpoint` never attaches, so it is an observer for its whole life. A | 2339 | endpoint` never attaches, so it is an observer for its whole life. A |
| 2340 | `quic_owned` flag records whether the listener was created by this path, | 2340 | `quic_owned` flag records whether the listener was created by this path, |
| 2341 | so `deinit` hands back exactly what it took and a daemon started with an | 2341 | so `deinit` hands back exactly what it took and a daemon started with an |
| 2342 | explicit `--quic` keeps ownership of its own. | 2342 | explicit `--quic` keeps ownership of its own. **Became a `union(enum)` |
| 2343 | in M15 (Task 9)**, which makes the `(null, owned)` pair untypable rather | ||
| 2344 | than merely unreachable. | ||
| 2343 | 2345 | ||
| 2344 | `src/handoff.zig` — one grammar, one place. Cache read/write, announce | 2346 | `src/handoff.zig` — one grammar, one place. Cache read/write, announce |
| 2345 | `format`/`parse`, the dial-host strip and the deadline constant all live | 2347 | `format`/`parse`, the dial-host strip and the deadline constant all live |
| @@ -2492,6 +2494,273 @@ tempting alternatives all produce a green report that means nothing. | |||
| 2492 | grep and its no-budget bound were never driven red. Cheap candidates | 2494 | grep and its no-budget bound were never driven red. Cheap candidates |
| 2493 | for the next regrade, and cheaper than the audit that found them. | 2495 | for the next regrade, and cheaper than the audit that found them. |
| 2494 | 2496 | ||
| 2497 | ## 2026-08-12 (M15 — refactor: typed invariants, single owners, module seams) | ||
| 2498 | |||
| 2499 | **Verdict: cleared.** Twelve tasks, each run as implementer + spec | ||
| 2500 | reviewer + quality reviewer, landed in **37 commits without moving the | ||
| 2501 | product**: the e2e suite stayed at **20 scenario checkpoints over 33 | ||
| 2502 | convergence points** from the first commit to the last, and `SOAK_N=10 | ||
| 2503 | make soak` was **10/10 on the code that ships** (445d80b). The unit layer | ||
| 2504 | grew from **245 to 255 tests**, and exactly **two behaviours changed** — | ||
| 2505 | both in QUIC dialling, both intended, both measured below. Spec: | ||
| 2506 | superpowers/specs/2026-08-12-m15-refactor-design.md. | ||
| 2507 | |||
| 2508 | Four modules were carved out with their tests — `paint.zig` (295), | ||
| 2509 | `quic.zig` (586), `delta.zig` (233), `sockpath.zig` (122) — and the two | ||
| 2510 | files they came out of shrank where it counts: `server.zig` **4708 → | ||
| 2511 | 4474**, `quic_server.zig` **2412 → 1972**. `client.zig` went the other | ||
| 2512 | way, **2406 → 2506**, and the split says why: its implementation half is | ||
| 2513 | flat at **1640 → 1647**, and **+93 of the +100 is new test code**. That | ||
| 2514 | is the coverage section below arriving in the line count, and it is the | ||
| 2515 | reason a whole-file `wc -l` is a poor clause to write a refactor against | ||
| 2516 | — it charges a file for the pins the refactor bought it. | ||
| 2517 | |||
| 2518 | ### The two behaviour changes | ||
| 2519 | |||
| 2520 | Both live in QUIC dialling, and both were the same omission seen from | ||
| 2521 | two sides: **nothing distinguished a refusal from silence.** | ||
| 2522 | |||
| 2523 | `drain()` had been discarding the queued ICMP refusal with a bare `catch | ||
| 2524 | return` — M14 recorded this as an incidental — so the `ConnectionRefused` | ||
| 2525 | branch in `readable` never saw it and every failure ran the deadline out. | ||
| 2526 | Task 4 gave both call sites one verdict, `sendRecvFailed`, so a refusal | ||
| 2527 | is acted on wherever it lands. Separately, `quic://` had no attach budget | ||
| 2528 | of its own and gave up only at the connection's 15000ms idle timeout; | ||
| 2529 | `QuicTarget.deadline_ms` split the two, so the attach budget is 2000ms | ||
| 2530 | and `idle_ms` goes back to meaning what its name says. | ||
| 2531 | |||
| 2532 | Local, medians, same box, before and after: | ||
| 2533 | |||
| 2534 | | Dial | Before | After | | ||
| 2535 | | --- | --- | --- | | ||
| 2536 | | refused — an unbound loopback port | 15001ms | **1ms** | | ||
| 2537 | | blackholed address | 15001ms | **2002ms** | | ||
| 2538 | | wrong PSK — a live port answering nothing | 15009ms | **2012ms** | | ||
| 2539 | |||
| 2540 | **The 2000ms deadline stands, and silence is still what sets it.** Only | ||
| 2541 | the refusal got fast; the two silent cases land within 12ms of the | ||
| 2542 | budget, which is the deadline doing its job rather than a bound anyone | ||
| 2543 | tightened. | ||
| 2544 | |||
| 2545 | The named cost: **`--quic-idle-ms` no longer raises the attach budget by | ||
| 2546 | accident.** It used to, because the attach shared the idle timeout, and | ||
| 2547 | that made it an undocumented escape hatch for a slow link. The hatch is | ||
| 2548 | gone. It is irrelevant below roughly 645ms RTT, and no path mux has been | ||
| 2549 | measured on comes close. | ||
| 2550 | |||
| 2551 | ### On the LAN box, against an M14-era daemon (2026-08-12) | ||
| 2552 | |||
| 2553 | The box at `192.168.0.109` was left on its pre-M15 `muxd` **on purpose**. | ||
| 2554 | Both behaviour changes are client-side, so measuring against the old | ||
| 2555 | daemon also measures cross-version compatibility. Version strings match | ||
| 2556 | (`mux 0.0.1-3` local, `muxd 0.0.1-3` remote) because the release was not | ||
| 2557 | cut until M15 closed; the binaries do not — the remote one was built at | ||
| 2558 | **06:27**, eight hours before M15's first commit at 14:31. | ||
| 2559 | |||
| 2560 | | Leg | Reps | Median | | ||
| 2561 | | --- | --- | --- | | ||
| 2562 | | refused — `quic://192.168.0.109:11997`, the box answers ICMP | 1 / 1 / 1 ms | **1ms** | | ||
| 2563 | | blackholed — `quic://192.0.2.1:11997` | 2002 / 2002 / 2003 ms | **2002ms** | | ||
| 2564 | | control — attach `quic://192.168.0.109:4433` | exit 0, marker painted, `\034` detach clean, daemon reported `clients=0` | — | | ||
| 2565 | |||
| 2566 | The control leg is the cross-version one: an M15 client drove a session | ||
| 2567 | on an M14 daemon, painted, detached cleanly and was observed gone from | ||
| 2568 | the daemon's own count. The refusal that cost ~15s before M15 now costs | ||
| 2569 | 1ms across a real network, and the blackhole still costs one deadline. | ||
| 2570 | |||
| 2571 | ### The regrade table | ||
| 2572 | |||
| 2573 | One sitting, one tree, one set of binaries. Each mutant was required to | ||
| 2574 | **compile**, `git status --porcelain` was empty and `make test` green | ||
| 2575 | between every row, and every revert was a file copy — never a `git | ||
| 2576 | checkout`, for the reason in the process note. | ||
| 2577 | |||
| 2578 | | Resurrection | Layer | Test that answered | Verdict | | ||
| 2579 | | --- | --- | --- | --- | | ||
| 2580 | | `drain()`'s send-error arm reverted to `catch return` (leaving `sendRecvFailed` itself) | unit | `client: handoff: dead coordinates are a fast no, and the pipe is the fallback` | **CAUGHT** | | ||
| 2581 | | `Transport.open`'s `.sock` and `.via` arm bodies swapped, captures renamed so the bodies moved verbatim | unit | `client: Transport.open: a --via target yields a pipe, a --sock target an fd` | **CAUGHT** | | ||
| 2582 | | `keyRefusalBody`'s `chmod 600` → `chmod 601`, implementation line only | unit, **two** | `quic: keyRefusalBody: the words four binaries print, byte for byte` **and** `client: openFailure: a quic:// target names the key or the address, and only an abort exits 0` | **CAUGHT** | | ||
| 2583 | |||
| 2584 | **All three answered at the unit layer; `make e2e` was never needed.** | ||
| 2585 | That is not the unit-layer mutation sweep the roadmap has banked since | ||
| 2586 | M11 — three aimed mutants are not a sweep — but it is the third data | ||
| 2587 | point arguing for one, and the cheapest yet: seconds per row instead of | ||
| 2588 | the two minutes an e2e run costs. | ||
| 2589 | |||
| 2590 | Each catch named the property rather than the symptom. The refusal | ||
| 2591 | mutant printed its witness line: | ||
| 2592 | |||
| 2593 | ``` | ||
| 2594 | error: 'client.test.handoff: dead coordinates are a fast no, and the pipe is the fallback' failed: mux: quic://127.0.0.1:1 unreachable, attaching over ssh | ||
| 2595 | refused dial took 302ms of a 300ms budget: the ICMP refusal was swallowed, not acted on | ||
| 2596 | ``` | ||
| 2597 | |||
| 2598 | — a number, a bound, and the diagnosis, in one sentence. (The first line | ||
| 2599 | carries the client's own stderr, which is the fallback working | ||
| 2600 | correctly; the assertion is the second.) The dispatch mutant printed its | ||
| 2601 | named decision: | ||
| 2602 | |||
| 2603 | ``` | ||
| 2604 | error: 'client.test.Transport.open: a --via target yields a pipe, a --sock target an fd' failed: a --via target must spawn a command, not connect a socket: open failed with FileNotFound | ||
| 2605 | ``` | ||
| 2606 | |||
| 2607 | The literal mutant printed a byte-for-byte diff at **both** layers, the | ||
| 2608 | vocabulary module's and the caller's, with `('\x30')` against `('\x31')` | ||
| 2609 | under the caret — which is the one-owner refactor demonstrating that it | ||
| 2610 | kept two independent pins rather than collapsing them into one. | ||
| 2611 | |||
| 2612 | ### Findings | ||
| 2613 | |||
| 2614 | Seven, each stated as the rule it became. | ||
| 2615 | |||
| 2616 | 1. **A panic prints no assertion, so pin ORDERING buys attribution.** A | ||
| 2617 | mutation that aborts is charged to whichever test is running, not to | ||
| 2618 | the test whose property it violated — unless the pin that states the | ||
| 2619 | property runs first. Ordering is not cosmetic in a suite that grades | ||
| 2620 | itself by mutation. | ||
| 2621 | 2. **A comptime error is a third prints-nothing route.** The module | ||
| 2622 | simply vanishes from the run (227 of 251 tests, and no property | ||
| 2623 | named) rather than failing loudly. Length-asserts go first and the | ||
| 2624 | slices get sized at runtime, so a bad mutation fails as a test rather | ||
| 2625 | than as an absence. | ||
| 2626 | 3. **Flipping a conjunction's operands is not negation.** Shared | ||
| 2627 | structure absorbs it — two paths on one filesystem share a `dev`, so | ||
| 2628 | swapping the operands of the identity check changed nothing | ||
| 2629 | observable. A mutation meant to negate must be written `!`. | ||
| 2630 | 4. **When a pin shares the implementation's literal, a sentence-wide | ||
| 2631 | `sed` self-heals.** Mutate the implementation line and nothing else, | ||
| 2632 | or the mutation edits its own detector and grades zero. | ||
| 2633 | 5. **A substring pin is satisfiable by incidental prose.** Anchor to the | ||
| 2634 | position that carries the meaning — `"\n muxd " ++ name`, not | ||
| 2635 | `name` — or the pin passes on a mention rather than a definition. | ||
| 2636 | 6. **Wire-format mutations wedge the suite silently.** Second data | ||
| 2637 | point; a wedged step prints nothing at all, so it must be watched by | ||
| 2638 | wall clock rather than by output. | ||
| 2639 | 7. **An e2e scenario can die UNNAMED under `set -eu`** when the failing | ||
| 2640 | pipeline is the client itself: the script's own `FAIL` line never | ||
| 2641 | prints. Banked as legibility debt. | ||
| 2642 | |||
| 2643 | ### Plan amendments, all made at source | ||
| 2644 | |||
| 2645 | **Nine**, each written into the plan with the evidence that forced it, | ||
| 2646 | rather than worked around in an implementation. The pattern worth | ||
| 2647 | keeping is that the plan was wrong in ways only the code could reveal: | ||
| 2648 | |||
| 2649 | - Task 1's assumed frame `0x86` was `stats_reply`; `exit_status` is | ||
| 2650 | `0x82`. | ||
| 2651 | - `writeFrame`'s "golden pin" was a false doc claim — there was no such | ||
| 2652 | pin until the task wrote one. | ||
| 2653 | - `ensurePtyModeSent` was **reversed**: the name promised a skip the body | ||
| 2654 | did not perform. | ||
| 2655 | - The strike named the wrong target (`waitReady`'s doc, not | ||
| 2656 | `default_idle_ms`). | ||
| 2657 | - Task 5's specified mutation aimed at an unpinned function, so | ||
| 2658 | `renderClipped`'s epilogue got its first pin as a rider. | ||
| 2659 | - Task 6's regrade premise was false — there was **no** unit pin on | ||
| 2660 | sock/via dispatch at all; the rider added it, and it is regrade row 2 | ||
| 2661 | above. | ||
| 2662 | - Task 7's null-means-silent premise was false: aborts print. | ||
| 2663 | - Task 10's specified mutation was operand-flipping, not negation | ||
| 2664 | (finding 3). | ||
| 2665 | - Task 11's `sed` note, which became finding 4. | ||
| 2666 | |||
| 2667 | Correction ran in every direction, which is the part that is easy to | ||
| 2668 | lose. Reviews corrected implementers; **implementers corrected | ||
| 2669 | reviewers** (an error-set `||` needs parens on 0.15.2; a sketch that was | ||
| 2670 | described rather than quoted got rewritten from the spec); and | ||
| 2671 | **reviewers corrected the lead** — the `ensurePtyModeSent` reversal, | ||
| 2672 | `egress_cap`'s home, the vestigial stat-before-close ordering, and the | ||
| 2673 | anchored usage pin were all lead decisions overturned with evidence. | ||
| 2674 | |||
| 2675 | ### What a survey is good for | ||
| 2676 | |||
| 2677 | The pre-task surveys **located structure correctly and systematically | ||
| 2678 | undercounted instances**: 17 union edit sites turned out to be 33, 5 path | ||
| 2679 | literals 6, 8 message classes 12. One survey premise was simply false | ||
| 2680 | (the dispatch decision was described as pinned; it was not). | ||
| 2681 | |||
| 2682 | **Surveys locate; they do not count.** A plan that budgets from a survey | ||
| 2683 | count is budgeting from a lower bound. | ||
| 2684 | |||
| 2685 | ### Coverage: what got its first pin | ||
| 2686 | |||
| 2687 | M15 was a refactor, so most of its test growth is coverage that had never | ||
| 2688 | existed rather than coverage that moved. First-ever pins landed for: | ||
| 2689 | |||
| 2690 | - the refusal **wire bytes**, plus the proof that the QUIC and socket | ||
| 2691 | producers emit the same ones; | ||
| 2692 | - `writeFrame`'s golden bytes; | ||
| 2693 | - `renderClipped`'s epilogue **and** its prologue position; | ||
| 2694 | - the transport dispatch decision; | ||
| 2695 | - `openFailure`'s **12 message classes** and its truncation policy; | ||
| 2696 | - `keyRefusalBody`; | ||
| 2697 | - `PathId`, including the historical sockfs bug carried as a live | ||
| 2698 | assertion rather than a comment; | ||
| 2699 | - `oneShotQuery`'s nobody-serving exit; | ||
| 2700 | - usage-names-every-verb, anchored (finding 5). | ||
| 2701 | |||
| 2702 | Two pieces of coverage debt were banked rather than paid: `uses_socket` | ||
| 2703 | is pinned for **1 of its 9 rows** (`dump`; `--version`'s row is | ||
| 2704 | structurally dead), and a session-full refusal **over QUIC** is still | ||
| 2705 | unexercised behaviourally. | ||
| 2706 | |||
| 2707 | ### Banked by M15 (the M16 candidates) | ||
| 2708 | |||
| 2709 | - **The stat-after-close reorder in `deinit`** — narrows the window in | ||
| 2710 | which a successor daemon's socket could be claimed by a departing | ||
| 2711 | one's cleanup. The analysis is already written, in the `sockpath` | ||
| 2712 | comment; only the reorder is owed. | ||
| 2713 | - **`egress_cap` ↔ transport-params, one owner** — `256*1024` is spelled | ||
| 2714 | three times and the tie between the copies is prose only. | ||
| 2715 | - **Fold `Transport`'s `alloc`/`qout` into the `.quic` `Link` payload** — | ||
| 2716 | deletes the struct's one remaining `undefined`. | ||
| 2717 | - **`Pty.write` is dead in production** — adopt or delete. | ||
| 2718 | - **e2e scenario-naming legibility** — finding 7: a scenario that dies | ||
| 2719 | unnamed, because `set -eu` kills the script before its own `FAIL` line. | ||
| 2720 | - **`predict.zig`'s retired channel** — adopt or delete. | ||
| 2721 | - **`keyRefusalBody`'s e2e coverage** — the announce-none path pins it | ||
| 2722 | only loosely; regrade row 3 was caught by unit pins alone. | ||
| 2723 | |||
| 2724 | ### Process note | ||
| 2725 | |||
| 2726 | **The file-copy revert idiom is now standing practice.** It became one | ||
| 2727 | the expensive way: a `git checkout` used as a mutation revert ate an | ||
| 2728 | uncommitted test. Copy the pristine file aside, mutate, copy it back, | ||
| 2729 | and verify with `git status --porcelain` plus a green `make test` before | ||
| 2730 | the next row. | ||
| 2731 | |||
| 2732 | **Relay sketches verbatim, never described.** A described sketch was | ||
| 2733 | reconstructed wrongly and had to be rewritten from the spec — one of the | ||
| 2734 | nine amendments above. | ||
| 2735 | |||
| 2736 | Two permission-classifier blocks were handled correctly: the agent | ||
| 2737 | stopped, reported the step as **unobserved** rather than assumed, and | ||
| 2738 | handed it to an implementer. An unobserved step reported as observed is | ||
| 2739 | the one failure mode this process has no other guard against. | ||
| 2740 | |||
| 2741 | **The soak's pre-flight hygiene check earned its keep at the close.** The | ||
| 2742 | first `SOAK_N=10` invocation refused to start, naming one stray | ||
| 2743 | `mux-e2e-*` file in `/tmp` left by an earlier run. It was real evidence: | ||
| 2744 | a capture of `muxd dump --sock` printing `nothing listening on …` where | ||
| 2745 | the `sun_path` scenario demands `socket path too long`. It was also | ||
| 2746 | **stale** — the current binaries refuse the long path in both binaries, | ||
| 2747 | and the file's timestamp put it inside Task 12's working tree, between | ||
| 2748 | the commit before the subcommand spec table and the commit that landed | ||
| 2749 | it. The check cannot tell a live leak from a fixed one, which is exactly | ||
| 2750 | why it refuses to guess and makes a human look. Inspect, confirm against | ||
| 2751 | the current binaries, then clear — never clear first. | ||
| 2752 | |||
| 2753 | ### Errata | ||
| 2754 | |||
| 2755 | Recorded rather than rewritten, since the commits are published: | ||
| 2756 | |||
| 2757 | - Task 4's fix commit says "eight named cascade failures"; seven are | ||
| 2758 | below the pin. | ||
| 2759 | - Task 9's commit message claims the test edits were three and nothing | ||
| 2760 | else; two comments also changed. | ||
| 2761 | - "12 pins" in Task 7's report was the message-**class** count, not a | ||
| 2762 | count of pins. | ||
| 2763 | |||
| 2495 | ## Open (owed by later milestones) | 2764 | ## Open (owed by later milestones) |
| 2496 | 2765 | ||
| 2497 | - Scrollback retention *tuning*. The policy itself was decided in M1 and | 2766 | - Scrollback retention *tuning*. The policy itself was decided in M1 and |
docs/roadmap.md
| Old | New | ||
|---|---|---|---|
| @@ -5,7 +5,7 @@ The forward view, one item per line, ranked. History and evidence live in | |||
| 5 | this file at each milestone close and whenever the queue reorders; the | 5 | this file at each milestone close and whenever the queue reorders; the |
| 6 | queue's order is set by the user, not by this file. | 6 | queue's order is set by the user, not by this file. |
| 7 | 7 | ||
| 8 | **Now:** M1–M14 complete; `v0.0.1-2` published as a Linux tarball; in | 8 | **Now:** M1–M15 complete; `v0.0.1-2` published as a Linux tarball; in |
| 9 | field trial on real VMs, and the trial is now producing the queue. | 9 | field trial on real VMs, and the trial is now producing the queue. |
| 10 | Trial feedback outranks everything below — what actually hurts in use is | 10 | Trial feedback outranks everything below — what actually hurts in use is |
| 11 | better data than any of this ranking, and the proof is that the three | 11 | better data than any of this ranking, and the proof is that the three |
| @@ -136,26 +136,75 @@ still landing the session over ssh, one deadline and one line later | |||
| 136 | En route: the announce had to become **mandatory in both directions** | 136 | En route: the announce had to become **mandatory in both directions** |
| 137 | because silence and a slow ssh are indistinguishable at the reading end | 137 | because silence and a slow ssh are indistinguishable at the reading end |
| 138 | — `endpoint none` is now an explicit negative — and the deadline was | 138 | — `endpoint none` is now an explicit negative — and the deadline was |
| 139 | pinned from measurement showing there is **no fast-failure case** at | 139 | pinned from measurement showing there was **no fast-failure case** at |
| 140 | all, not even a refused loopback port. | 140 | all, not even a refused loopback port *(refusal now fails fast as of |
| 141 | 141 | M15: 1ms, local and on the LAN box)*. | |
| 142 | ## M15 candidates — and still outranked by trial feedback | 142 | |
| 143 | 143 | ## M15 — refactor: typed invariants, single owners, module seams — complete | |
| 144 | What remains is what was already ranked behind M13 and M14, plus two | 144 | |
| 145 | items M14 created. Nothing here is a field finding; the next one that | 145 | **Verdict: cleared.** Twelve tasks, each run as implementer + spec |
| 146 | arrives outranks all of it. | 146 | reviewer + quality reviewer, landed in **37 commits without moving the |
| 147 | 147 | product**: the suite held at **20 scenario checkpoints over 33 | |
| 148 | - **`drain()` swallows `ECONNREFUSED`** — created by M14 and the | 148 | convergence points** from first commit to last, `SOAK_N=10 make soak` was |
| 149 | cheapest win on the list. On a connected UDP socket the queued ICMP | 149 | **10/10 on the code that ships**, and the unit layer grew **245 → 255** |
| 150 | refusal is consumed by the `sendto` in `Client.drain`, whose `catch | 150 | tests. Four modules were carved out with their tests — `paint.zig`, |
| 151 | return` discards it, so the `ConnectionRefused` branch never fires and | 151 | `quic.zig`, `delta.zig`, `sockpath.zig` — and `server.zig` fell **4708 → |
| 152 | an unreachable port costs the full 2s deadline instead of ~1 RTT. | 152 | 4474**, `quic_server.zig` **2412 → 1972**. |
| 153 | Fixing it re-times two existing bounds rather than deleting them | 153 | |
| 154 | (decisions.md, M14). | 154 | Exactly **two behaviours changed**, both in QUIC dialling and both the |
| 155 | - **The transport recipe as a `union(enum)`** — also from M14. | 155 | same omission seen from two sides: nothing distinguished a refusal from |
| 156 | `Transport.open` takes four mutually-exclusive nullables, two of them | 156 | silence. A refused dial went **15001ms → 1ms**, and `quic://` got an |
| 157 | sharing a type; `union(enum) { sock, via, quic, hand }` would make the | 157 | attach budget of its own instead of inheriting the 15000ms idle timeout. |
| 158 | invariant unstatable-wrong instead of merely documented. | 158 | The 2000ms deadline stands and silence is still what sets it — a |
| 159 | blackholed address costs 2002ms, a wrong PSK 2012ms. Confirmed on the | ||
| 160 | LAN box against a deliberately **M14-era daemon**, which made the same | ||
| 161 | run a cross-version compatibility check: refusal 1ms, blackhole 2002ms, | ||
| 162 | and an M15 client attaching, painting and detaching cleanly on the old | ||
| 163 | daemon. The named cost is that `--quic-idle-ms` no longer raises the | ||
| 164 | attach budget by accident; that escape hatch is gone, and irrelevant | ||
| 165 | below ~645ms RTT. | ||
| 166 | |||
| 167 | Leg 2 is three for three, and **all three were caught at the unit | ||
| 168 | layer** — `make e2e` was never needed, which is a third argument for the | ||
| 169 | unit-layer mutation sweep still banked below, at seconds a row instead of | ||
| 170 | two minutes. En route, the surveys that fed the plan were found to | ||
| 171 | **locate structure correctly and undercount instances every time** (17 | ||
| 172 | union edit sites were 33, 5 path | ||
| 173 | literals were 6, 8 message classes were 12), which forced **nine plan | ||
| 174 | amendments, each made at source**; and most of M15's test growth is | ||
| 175 | coverage that had never existed — the refusal wire bytes, `writeFrame`'s | ||
| 176 | golden bytes, the transport dispatch decision, `openFailure`'s 12 message | ||
| 177 | classes and `keyRefusalBody` all got their **first** pins | ||
| 178 | (decisions.md, M15, which carries the tables and the failure lines). | ||
| 179 | |||
| 180 | ## M16 candidates — and still outranked by trial feedback | ||
| 181 | |||
| 182 | What remains is what was ranked behind M13 and M14 and survived M15, | ||
| 183 | plus the seven items M15 created. Nothing here is a field finding; the | ||
| 184 | next one that arrives outranks all of it. | ||
| 185 | |||
| 186 | - ~~**`drain()` swallows `ECONNREFUSED`**~~ — paid by M15 (Task 4). Both | ||
| 187 | socket paths now route the refusal through one verdict, and an | ||
| 188 | unreachable port fails in **1ms** instead of a full deadline | ||
| 189 | (decisions.md, M15). | ||
| 190 | - ~~**The transport recipe as a `union(enum)`**~~ — paid by M15 (Task 6). | ||
| 191 | `Transport.open` takes `union(enum) { sock, via, quic, hand }`, and the | ||
| 192 | dispatch decision it makes got its first pin in the same task. | ||
| 193 | - **The stat-after-close reorder in `deinit`** — created by M15 (Task | ||
| 194 | 10). Narrows the window in which a departing daemon's cleanup could | ||
| 195 | unlink a successor's socket. The analysis is already written, in the | ||
| 196 | `sockpath` comment; only the reorder is owed. | ||
| 197 | - **`egress_cap` ↔ transport-params, one owner** — also M15. `256*1024` | ||
| 198 | is spelled three times and the tie between the copies is prose only. | ||
| 199 | - **Fold `Transport`'s `alloc`/`qout` into the `.quic` `Link` payload** — | ||
| 200 | deletes the struct's one remaining `undefined`. | ||
| 201 | - **`Pty.write` is dead in production** — adopt or delete. | ||
| 202 | - **e2e scenario-naming legibility** — a scenario can die *unnamed*, | ||
| 203 | because under `set -eu` a failing client pipeline kills the script | ||
| 204 | before its own `FAIL` line prints (decisions.md, M15, finding 7). | ||
| 205 | - **`predict.zig`'s retired channel** — adopt or delete. | ||
| 206 | - **`keyRefusalBody`'s e2e coverage** — the announce-none path pins it | ||
| 207 | only loosely; M15's regrade caught that mutant on unit pins alone. | ||
| 159 | - **First-backoff tuning**: a reconnect after a tear can pay a 200ms–2s | 208 | - **First-backoff tuning**: a reconnect after a tear can pay a 200ms–2s |
| 160 | backoff step; measured ~4x win available on fast links, ~2x on slow | 209 | backoff step; measured ~4x win available on fast links, ~2x on slow |
| 161 | (decisions.md, M7). | 210 | (decisions.md, M7). |
| @@ -184,7 +233,9 @@ arrives outranks all of it. | |||
| 184 | delta row's leading reset, where the existing unit pin turned out to | 233 | delta row's leading reset, where the existing unit pin turned out to |
| 185 | be the whole guard). Both were discovered by accident, while aiming an | 234 | be the whole guard). Both were discovered by accident, while aiming an |
| 186 | e2e campaign at the product. A sweep aimed at the unit layer would not | 235 | e2e campaign at the product. A sweep aimed at the unit layer would not |
| 187 | need the accident. | 236 | need the accident. M15 adds the cost argument: all three of its regrade |
| 237 | mutants died at the unit layer, in seconds a row, and `make e2e` was | ||
| 238 | never reached (decisions.md, M15). | ||
| 188 | - **ASAN or valgrind over the QUIC tests** — carried from M9 unchanged, | 239 | - **ASAN or valgrind over the QUIC tests** — carried from M9 unchanged, |
| 189 | and unchanged is the finding: an e2e mutation campaign says nothing | 240 | and unchanged is the finding: an e2e mutation campaign says nothing |
| 190 | about use-after-free. M9's two reasoned-not-pinned `deinit` orderings | 241 | about use-after-free. M9's two reasoned-not-pinned `deinit` orderings |