Skip to content

Fix stale spec_hasher test expectations - #112

Merged
twitchyliquid64 merged 3 commits into
mainfrom
fix/spec-hasher-hashes
Apr 10, 2026
Merged

twitchyliquid64 merged 3 commits into
mainfrom
fix/spec-hasher-hashes

Conversation

@bryan-minimal

Copy link
Copy Markdown
Member

Summary

  • 5 spec_hasher tests have been failing silently — CI cargo cache masked the breakage
  • Root cause: stdlib changes (allow_missing_interpreter in OutputBin from 03e5608, AttrValue::Number in hasher from 541ee18) altered nickel evaluation output, changing the blake3 hashes
  • Updated all 5 expected hash literals to match current stdlib

Tests

All 49 graph tests pass in minimal container.

Generated with Claude Code

The expected blake3 hashes in spec_hasher tests became stale after
stdlib changes (allow_missing_interpreter in OutputBin, AttrValue::Number
in hasher). CI masked this via cargo target cache. Regenerated from
current stdlib output.

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

Copy link
Copy Markdown
Contributor

I think I know whats going on here, the spec_hash depends on the target it was built for. So the hash is different on amd64 vs aarch64.

Feel free to either: Use #[cfg] or whatever on the values so the test checks the correct value for the current arch, or simply limit the test to amd64.

Hashes differ between amd64 and aarch64 due to target-sensitive nickel
stdlib evaluation. Added an arch_hash! macro that selects the correct
expected value per architecture.

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

Copy link
Copy Markdown
Member Author

Added #[cfg(target_arch)] gating via an arch_hash! macro — selects the correct expected hash per architecture. arm64 values verified locally (5/5 pass in minimal container), amd64 values preserved from main. CI will validate the amd64 side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@twitchyliquid64
twitchyliquid64 merged commit 2c9cb45 into main Apr 10, 2026
7 checks passed
@twitchyliquid64
twitchyliquid64 deleted the fix/spec-hasher-hashes branch April 10, 2026 22:58
mitodrummer added a commit that referenced this pull request Oct 7, 2026
Re-pin the architecture link to gominimal/arch 823f45f, where every
open question citing arch#97, #98, #99, #112, #113, #114 and #123 is
merged, and turn those questions into citations.

- BOX-163: refuse a held name only for a creation that asks for no
  fresh instance; a fresh instance is named <name>-<n> (arch#117).
- BOX-071: project egress.deny_subnets as network.egress_deny_subnets
  (Gatehouse v1.26).
- BOX-002 and the id-mint invariant: scoped to un-enrolled VM hosts;
  the STS mints on enrolled nodes (Gatehouse v1.27).
- BOX-013 states the 10 s default grace; BOX-025's name refusal is
  exit 2 (arch#97 row 2).
- BOX-165: the implicit [sessions.default] entry (arch#124), marked
  implicit by BOX-161 and covered by BOX-145's vectors.

Three open questions remain: the NET/GWI reading (#2041),
[params] delivery (arch#100) and exec_enabled, still proposed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdRDynXbYoKR6dByN13KsD
yermakoffivan pushed a commit to yermakoffivan/minimal that referenced this pull request Oct 8, 2026
…mal#1697)

* docs(specs): add BOX, the local-first box spec from inbox#731

One record, one spec, one grammar for every session and task on a stock
install: 146 EARS requirements across the box record and lifecycle, the
Box Spec in minimal.toml and its deterministic expansion, the min box
grammar with one-release aliases, memory admission, and the un-enrolled
local provider. T1 property tests on expansion, type constraints and
names; T2 Kani harnesses on the record state machine and admission
arithmetic; the three local Box Provider API requirements wait on
gominimal/arch#45.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): link BOX open questions to gominimal/arch#97–gominimal#100

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): fix box id entropy, rename events, detach stdout, name lookups

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): split BOX into three siblings and apply review cycle 1

BOX keeps the record, the Box Spec and the grammar (122 requirements,
ids stable with gaps). Resources move to 26-spec-box-resources (BRES,
13 requirements, T2 admission harnesses intact) and Box Volumes to
27-spec-box-volumes (BVOL, 8 requirements). The dash goes to an
amendment of 07-spec-min-dash-tui, the local Box Provider API waits on
gominimal/arch#45, and the test-suite meta-requirements are carried by
every tier/verify line.

Review cycle 1: resume hooks, the bare `min stop` alias window, the
projection field set per Gatehouse 6.3.2, `min box spec self` as a
nesting non-goal, the brokered-secret enforcer, the generality claim,
stopped-record exit codes, a `detached` event, `min provider list
--all` (BOX-147), and the first slice.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): review cycle 2 and fold split cases under their parents

Cycle 2 across the three siblings: [machine] and on_oom owned by BRES
alone, BRES limits scoped to enforced hosts, volumes keyed by project
(arch AT21) with an access mode and a hold that survives stop and
resume, the type-noun verb aliases (BOX-148), ram_reserved against a
resolved auto ram (BRES-015), and the BRES/gominimal#698 overlap recorded as an
open question.

BOX: 24 refusal and flag cases that were splits of a parent behaviour
are now indented sub-bullets of that parent, tier and verify lines
kept; references point at the parent. 99 top-level requirements.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): review cycle 3 across BOX, BRES and BVOL

BOX-076 admits BVOL-011's volume items; the min shell ambiguity case
sits under BOX-090; parent tier lines precede their sub-bullets; the
network non-goal range is corrected. BRES-006 covers an omitted ram,
BRES-016 adds the advisory-host creation warning arch requires. BVOL
mounts at /volumes/<name> (open question for arch) and BVOL-012 keeps
prune off held volumes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): split the min grammar out of BOX into 28-spec-box-cli

BOX now specifies the model only: the record, lifecycle, events, exec
operations, the Box Spec and its expansion, and the un-enrolled path,
with triggers stated as operations rather than verbs (60 requirements).
BCLI carries the min box verb set, type nouns, shortcuts, exec flags,
output schemas, exit codes, the one-release alias window, docs, and the
host and provider commands (52 requirements), each binding to a BOX
operation by id and carrying the BOX id it came from.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): consistency pass across BOX, BCLI, BRES and BVOL

Exec client loss scoped to non-PTY, non-detached execs; a stopped box's
"exists" mapped onto NET-013/015 with an open question for NET and GWI;
min box rename bound (BCLI-058) and the alias table completed, with
session destroy mapping to box rm --force so BEP-043 still fires; the
stop, rm, prune, cp and events verbs bound to their BOX operations
(BCLI-059 to 062); verbs removed from the model's remaining text; task
end keyed on lifetime; a running record on daemon restart becomes
stopped (BOX-154); detached output capture (BOX-155); legacy mode
spellings named; stored exit codes stated; BRES-004's reservation cap
takes arch's overcommit-policy value; BVOL's hold and bare-name
decisions recorded as the owner's.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): review cycle 5: NET "exists" means running, stored signals

NET-013/015 and GWI-005's "exists" now maps to the running state and
"destroyed" to leaving it; a stopped box is neither kept running nor
answers its name. A signal-ended stop stores the signal and min box
wait returns 128 + signal (137 for a record the daemon restart marked
stopped). The restart-to-stopped and host-stop-stops-boxes-first
choices are recorded as the owner's; the bare-volume-name reason
matches the record.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): read NET's destroyed as reaped; stopped names answer NODATA

Per NET-128 and the networking design §7.1 a not-running box keeps its
name in the zone and answers NODATA; NXDOMAIN starts at reap. Resume
and name reuse need NET to register at resume as well as at finalise;
the open question asks NET and GWI to confirm.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): answer NXDOMAIN at reap only for the last holder of a name

A reused name follows its remaining holder (A if running, NODATA if
not); own-address NODATA is put to NET as a question; resume cites
BOX-025 and BOX-030.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): refuse resume while a running box holds the name

NODATA applies while no running box holds a name; BOX-004's property
covers resume; BOX-025 refuses a resume whose name a running box holds
and BCLI reports it with exit 2 and a rename hint.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): consistency pass over the box specs, cycle 10

Accepted keys are arch's box.toml plus the additions this spec names;
legacy mode spellings canonicalise into the spec and expire with the
legacy tables; prune reaps only stopped or exited boxes matching all
selectors; NET-015's stop list gains timeout, on_oom end_box and
entrypoint return; resume applies to any pty_enabled box; the
--network/--ingress overrides are the last expansion layer; provider
aliases expire with the rest; secrets render as references in every
output mode (BCLI-063); issue citations point at the arch#97/gominimal#98
comments that now carry the items.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): min box spec takes the override flags; volume hold ends on exit

min box spec <entry> accepts --network and --ingress through the same
last expansion layer as start, so the pre-flight render is what the
daemon receives. A volume write hold is kept while a box is stopped
and released when it reaches exited or is reaped (owner decision), so
a task re-runs at once; resume from exited while another box holds the
volume is refused with exit 5 (BVOL-013).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): overrides in the expansion properties; resume after volume removal

The T1 properties for byte-identical and enrolment-invariant expansion
range over command-line overrides too. A resumed box whose volume was
removed while it was exited gets a fresh volume created and reattached
by project and name. The name-conflict check on resume runs before the
volume-hold check, so a resume meeting both exits 2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): order the resume refusals: PTY check, name check, volume hold

BVOL-013 applies only to pty_enabled boxes and runs after BOX-030 and
BOX-025's name check; BCLI-007 and BCLI-009 cite that order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): place the unsatisfiable-spec check in the resume refusal order

BOX-025 states the full order once (PTY, name, spec, volume hold);
BVOL-013 and BCLI cite it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): min session attach aliases the top-level min attach shortcut

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): the volumes first slice includes the single-writer refusal

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): qualify the fresh-volume claim on resume after removal

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): write the roles lines as prose, not a concatenation

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): an address that resolves to no box exits 4

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): close the arch consistency findings on the box specs

Type constraints are checked against the expanded spec and a derived
type may not loosen its parent (BOX-061, BOX-156). The flat egress keys
the arch examples write get one release of hint-and-canonicalise grace
(BOX-157). Rename refuses a held or reserved name (BOX-010) and name
resolution is per host with a cross-host ambiguity refusal (BOX-005).
Volumes drop the read-only mode until object storage lands and the
expanded list is deduplicated by name (BVOL-014). OOM kills end the
box per on_oom (BRES-017, BRES-018). min shell --new takes a suffixed
name (BCLI-065), min session run is a usage error (BCLI-066), the
attached exec id goes to stderr and min box show lists live execs
(BCLI-067). BCLI-041 and BCLI-050 duplicated the alias table and are
removed; BOX-021 drops the no-op stopped selector.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): settle the BRES split with the guest-memory epic

BRES owns admission, the memory limit write, its read-only surface and
exec-join (BRES-004 to BRES-009); inbox#698 keeps the guest size, swap,
the cgroup2 mount and daemon reserve, pressure warnings, the shell
banner and the min ls headroom line. Its S3b clamp rule gives way to
BRES-004/005's refusal with figures. Closes the two clarification
markers that named the overlap.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): move the resources spec to inbox#698

Resources now belong to gominimal/inbox#698, which will get a fresh
BRES spec. BOX and BCLI cite that spec by id without requirement
numbers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): move BVOL and BCLI to their own PRs

The volumes and CLI specs now land separately (docs/spec-box-volumes and
docs/spec-box-cli) so the BOX model spec is reviewed on its own; both
files are byte-identical on their new branches.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): date the BOX spec to its last revision

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): pin the BOX spec's arch link to a commit

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): align BOX-011 with BOX-154 and split BOX-152's event fields

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): apply the BOX arch-conformance review decisions

Map the projection onto Gatehouse 6.3.2's field names with the defaults
filled before canonicalisation and a base64url digest; scope BOX-001's
host-side minting to the box_id; add the daemon's admission constraint
check (BOX-160), record listing and reading (BOX-158, BOX-159) for the
first slice, and per-value provenance (BOX-161); state the exec
contract, every process in the box, and BOX-040's triggers; raise
BOX-001 and BOX-072 to T1 and justify the T0 security requirements;
name owners for attribute rules, runtime spec change, org types and
the unenforced sections, which now warn at creation.

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

* docs(specs): limit BOX-160's admission check to the built-in root

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

* docs(specs): drop the daily shortcuts from BOX's grammar non-goal

BCLI no longer specifies min shell, min run, min attach or min ls; they
are UX work layered on the grammar, so the non-goal names the long forms
and the type nouns only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): credit the OOM end to BRES and cite the DNS reading once

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): re-transcribe BOX's resume and task-run stories

The epic now names min session attach, min box resume and min task run
in S3 and S4 instead of the daily shortcuts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): spell out BOX id ranges that span unused numbers

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): renumber the BOX spec to 29, clear of gominimal#1586 and gominimal#1988

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): settle BOX's first-slice type, parent and event reads

The first slice records the type from the creating verb. A box created
from outside any box has no parent. Events carry box, parent, timestamp
and type, and BOX-162 reads the full retained stream and follows it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): record BOX's project root and bound exec to running boxes

A box records its project root, which listing and reading return. An
exec into a box not running is refused with exit 2, runs under the
box's ceilings, and a detached exec closes stdin. Open questions cite
gominimal/arch#112 (prune --stopped) and gominimal#113 (override layer).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): limit BOX expansion to sessions and tasks; log from start

BOX-046 and BOX-063 expand only session and task entries and refuse the
other four built-in types with exit 3 naming gominimal/inbox#678.
BOX-155 logs a box's output from its start, attached or not.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): tabulate BOX-011's end data and tidy BOX's minor findings

BOX-011's end data and BOX-076's added keys become tables. Timeout and
exec client loss end with SIGTERM, a wait, then SIGKILL. The network
non-goal points at Design reasoning, and the VM host is local-minvmd0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): state BOX-001's type property for both creation paths

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): let a BOX exec carry its own timeout

A BOX-149 sub-bullet ends an exec at its own timeout with BOX-150's
sequence, records exec_exited with reason timeout, and keeps the box
running. Design reasoning and the arch-gap open question record it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): date BOX to 2026-10-06 and mark O3 as moved to BCLI

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): cite gominimal/arch#114 for BOX's per-exec timeout gap

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): apply msample's five BOX findings

BOX-063 refuses the four excluded types per entry, and BOX-145's vectors
are the arch example's session and task entries. BOX-163 names a box at
creation and refuses a running holder with exit 2 (gominimal/arch#122).
Project file rules are left out like secret-store rules (arch#123).
BOX-164 puts the record and log out of the box's reach. Expansion writes
store references' upstream and inject into the spec for the projection.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): scope BOX-010's property to successful renames

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): cite BOX-151 for exec timeouts; add store rules to parity

The exec-timeout sub-bullet cites BOX-151, which owns the end sequence.
BOX-065 and BOX-142 take the client's [secret-store-rules] set as an
input, since expansion now resolves store references from it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(specs): name the boxes crate in BOX's verify commands

The sessions crate is being renamed to boxes before BOX is implemented,
so that the crate holding the box domain types says box. Point BOX's ten
`cargo nextest run -p sessions` checks at `-p boxes`. The test names are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdRDynXbYoKR6dByN13KsD

* docs(specs): re-pin BOX to arch 823f45f and adopt its rulings

Re-pin the architecture link to gominimal/arch 823f45f, where every
open question citing arch#97, gominimal#98, gominimal#99, gominimal#112, gominimal#113, gominimal#114 and gominimal#123 is
merged, and turn those questions into citations.

- BOX-163: refuse a held name only for a creation that asks for no
  fresh instance; a fresh instance is named <name>-<n> (arch#117).
- BOX-071: project egress.deny_subnets as network.egress_deny_subnets
  (Gatehouse v1.26).
- BOX-002 and the id-mint invariant: scoped to un-enrolled VM hosts;
  the STS mints on enrolled nodes (Gatehouse v1.27).
- BOX-013 states the 10 s default grace; BOX-025's name refusal is
  exit 2 (arch#97 row 2).
- BOX-165: the implicit [sessions.default] entry (arch#124), marked
  implicit by BOX-161 and covered by BOX-145's vectors.

Three open questions remain: the NET/GWI reading (gominimal#2041),
[params] delivery (arch#100) and exec_enabled, still proposed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdRDynXbYoKR6dByN13KsD

* docs(specs): move NET's sentences back to its Non-goals bullet

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdRDynXbYoKR6dByN13KsD

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Oct 14, 2026
…he last snapshot

Clippy 1.99 under -D warnings found: needless borrows of a Copy closure in
args; double_must_use on tonic-generated async_trait code in remote-proto
(expected at the include, generated code); the new pedantic assert_is_empty
in sessions and paths, allowed in their hand-kept tables since the rewrite
says less at the site; must_use on functions returning types already
must_use, and three accessors without it, in sessions; a single-element
loop in mfile; chunks_exact(2) over a byte slice in minimald-rpc and the
switch checksum, now as_chunks; Result<_, ()> on three session-host methods
whose only error is a dead host, expected with that reason; and the
fetch_update deprecation in a minvmd test helper, expected until the MSRV
floor reaches the rename.

The nightly's mutation scope changes. A weekday shard of the workspace is
about 2,500 mutants (cargo mutants --list), which no 120-minute job can
run; run #112 was in that phase when this was written. The nightly now
tests the lines changed since the newest snapshot in the history (or the
merge-base with origin/main on a branch), under a wall-clock budget
(MINIMAL_METRICS_MUTANTS_BUDGET_SECS, 2700 by default); a run that hits the
budget reports what it tested with `partial` set, since cargo-mutants
rewrites outcomes.json after every mutant. The snapshot records the
reference under `mutants.since`.

Verified with a standalone 1.99.0 toolchain: workspace clippy clean, fmt
clean, clippy-strict clean on the changed lines, the touched crates' tests
green except minvmd's port_holder_is_none_when_the_lookup_fails, which
reads the host socket table and fails in this sandbox on main as well.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Oct 19, 2026
…he last snapshot

Clippy 1.99 under -D warnings found: needless borrows of a Copy closure in
args; double_must_use on tonic-generated async_trait code in remote-proto
(expected at the include, generated code); the new pedantic assert_is_empty
in sessions and paths, allowed in their hand-kept tables since the rewrite
says less at the site; must_use on functions returning types already
must_use, and three accessors without it, in sessions; a single-element
loop in mfile; chunks_exact(2) over a byte slice in minimald-rpc and the
switch checksum, now as_chunks; Result<_, ()> on three session-host methods
whose only error is a dead host, expected with that reason; and the
fetch_update deprecation in a minvmd test helper, expected until the MSRV
floor reaches the rename.

The nightly's mutation scope changes. A weekday shard of the workspace is
about 2,500 mutants (cargo mutants --list), which no 120-minute job can
run; run #112 was in that phase when this was written. The nightly now
tests the lines changed since the newest snapshot in the history (or the
merge-base with origin/main on a branch), under a wall-clock budget
(MINIMAL_METRICS_MUTANTS_BUDGET_SECS, 2700 by default); a run that hits the
budget reports what it tested with `partial` set, since cargo-mutants
rewrites outcomes.json after every mutant. The snapshot records the
reference under `mutants.since`.

Verified with a standalone 1.99.0 toolchain: workspace clippy clean, fmt
clean, clippy-strict clean on the changed lines, the touched crates' tests
green except minvmd's port_holder_is_none_when_the_lookup_fails, which
reads the host socket table and fails in this sandbox on main as well.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
norrietaylor added a commit that referenced this pull request Nov 12, 2026
…flow (#2339)

* feat(metrics): snapshot pipeline with clippy, coverage, size, modules, shear and layer gates

The first slice of spec 30. `just metrics` runs the collectors and writes a
schema-v1 snapshot: whole-tree counts of every strict lint and the three
complexity lints, line coverage through nextest under a coverage profile,
tokei size per crate, module cycles and orphans per crate, unused
dependencies, the crate-layer check over cargo metadata, and hotspots
ranked by churn times findings. Gates come from metrics/thresholds.toml;
the verdict is the worst gate. A collector that cannot run is recorded as
not measured and the run exits non-zero, never clean.

`just metrics-layers` is the no-compile layer check and joins `just ci`.
metrics/layers.toml carries the layer table derived from
docs/architecture.md and the edges the tree still carries as dated
exceptions. architecture.md §3 gains minzoned in the session plane and a
pointer to the enforced table.

scripts/metrics_test.sh is the fixture harness the spec's verify lines
name; crates/common/tests/shell_harnesses.rs picks it up. lint-shell.sh now
recurses so scripts/metrics/ is shellchecked.

Measured here with the sandbox's rustc 1.98 (the tree pins 1.97): the
strict pass completes with exit 0 and five diagnostics outside the strict
set, all from lints newer than the pin. CI measures with the pin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(metrics): module cycles from the DOT graph, orphans per target, profiler check

The first full run over the tree showed three collector faults. cargo-modules
runs its --acyclic check before its filters, so every type owning a method
that uses the type read as a cycle; the module graph is now dumped as DOT per
target and the collector finds the strongly connected components itself,
leaving out edges through the crate root and test modules, which are
structure. Orphans were the union over a crate's targets, so every
binary-only module read as an orphan of the library; they are now the
intersection. And a toolchain built without the profiler runtime cannot
compile under -C instrument-coverage; the run says so before paying for a
cold instrumented build.

modules.cycles moves from a zero ceiling to a down ratchet: the tree carries
sibling-module cycles today, the largest in sessions and minimald.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(metrics): static SVG dashboard and history pruning

Third change of the quality-metrics work (spec 30): the history side of
the snapshot pipeline.

scripts/metrics/dashboard.py renders a snapshot history into one
self-contained index.html (QM-016): inline SVG, a style block, no script,
nothing fetched, light and dark via prefers-color-scheme. It charts
coverage, strict and complexity lint totals, unused dependencies, code
size and layer edges over snapshot date, with a heading and a table of
the drawn values under every chart; from the newest snapshot it lists the
gates, the top hotspots and the promotable strict lints. A metric a
snapshot did not measure is a gap in the line, not a zero. Files that
fail schema.py are skipped and named on the page. The page carries no
timestamp, so the same history renders the same bytes.

scripts/metrics/prune.py keeps every snapshot within ninety days of the
newest and the earliest of each month beyond that, decided from the file
names alone (QM-015); keep() is a pure function the harness checks the
spec's property against. --dry-run lists without deleting.

The justfile gains metrics-dashboard and metrics-prune over a
.scratch/metrics/history checkout of the metrics-data branch. A synthetic
fixture history under scripts/metrics/testdata/history, one snapshot with
coverage null, backs the new harness cases prune_keeps_daily_then_monthly
and dashboard_renders_offline_with_tables. metrics/README.md documents the
dashboard and the retention rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(metrics): experimental rustqual and cargo-anatomy sections

Full mode gains an `experimental` step (skipped by `--no-experimental` and
in static mode) that runs rustqual per in-scope crate over its `src/`,
leaving remote-proto out because its code is generated into OUT_DIR, and
cargo-anatomy once over the workspace. collect.py records them under
`crates.<name>.experimental.{rustqual,anatomy}` and
`workspace.experimental`; the whole block is null only when neither tool
ran. Nothing in `metric()` or `evaluate_gates` reads the block, and the
harness asserts that from the source as well as from the verdict (QM-005).
A missing tool is a not_measured entry, a tool failing on one crate is an
`error` in that crate's block, and neither fails the run.

install-tools.sh gets an `experimental_pins` array installed only with
`--experimental`; `--check` lists those pins as optional. The schema
requires the `rustqual` and `anatomy` keys when the block is present.

Both collectors are fixture-verified only: neither rustqual nor
cargo-anatomy installs in this sandbox (crates.io downloads are blocked
and there are no prebuilt binaries), so the fixtures follow the tools'
published docs, every lookup is a `.get`, and testdata/SHAPES.md records
which shapes are unverified against a run. The run.sh branch was
exercised end to end with shims standing in for the two binaries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(metrics): cargo-mutants in-diff and weekday shards, cargo-crap

Coverage says a line ran, not that a test would notice it being wrong.
Two collectors fill the `mutants` and `crap` blocks the snapshot schema
already reserved (spec 30, QM-017).

`run.sh mutants [BASE]` diffs the working tree against the merge-base
with BASE (origin/main by default), the way clippy-strict.sh does, and
hands the hunks to `cargo mutants --in-diff` under nextest; the scope is
recorded as `in-diff`. An untracked `*.rs` file is not in that diff, so
it is listed under `untracked_outside_scope` rather than silently left
out. `full` tests one of seven workspace shards chosen by the weekday
and records the shard; `--no-mutants` skips it. The baseline suite is
green only on Linux, so mutants mode refuses to start on Darwin and the
nightly records the collector as not measured there. A diff that reaches
no mutant, and a baseline that fails, are told apart by the presence of
outcomes.json and the baseline's own outcome in it.

collect.py attributes each mutant by its file to a crate and reports
tested, caught, missed, timeout and unviable with the score as caught
over caught + missed + timeout; an unviable mutant never compiled and
says nothing about the tests. A crate the run generated no mutants for
stays null, not a perfect score. cargo-crap runs after coverage on the
lcov.info it wrote, through one `run_crap <lcov> <out>` hook so the diff
mode can reuse it; functions strictly over `[crap] threshold` are
counted per crate and in the workspace.

Gates: `mutants.score_pct` is an unseeded floor until the shards have
gone round the week once; `crap.over_threshold` is a ratchet down,
because the tree carries such functions today and a ceiling of zero
would be red from the start.

The `.minimal` task `mutants` (graph wire.rs only) becomes
`metrics-mutants`, pinned to the same cargo-mutants the install script
pins. Fixtures outcomes.json and crap.json follow the shapes of
cargo-mutants 27.1.0 and cargo-crap 0.7.0 as run on a throwaway
workspace; the harness cases named by QM-017 drive run.sh with a fake
cargo that logs its argv.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(metrics): metrics-diff grading, quality-gate task, shared changed-lines helper

Grade a change against its base ref (spec 30, QM-009 to QM-013, QM-N01),
on top of the snapshot pipeline.

scripts/metrics/diff.py reads two things the change cannot write: the
thresholds at the merge-base with the base ref and the newest snapshot on
the history ref (origin/metrics-data). Each gate is evaluated against the
stricter of the base's threshold and the head's (floors take the max,
ceilings and ratchet tolerances the min, a gate only one side declares is
kept), so a pull request may tighten the bar at once and loosen it only
after review has merged the loosening. The gates are re-evaluated with
collect.py's own evaluate_gates, never a copy. With no baseline every
ratchet is not_evaluated and the report says so. report.md and
report.json carry no times (meta.json does), so identical inputs give
identical bytes; report.md is appended to GITHUB_STEP_SUMMARY when that
names a writable file. When head's compiling collectors covered a subset
of the workspace, the baseline's clippy and coverage totals are re-summed
over the same crates before the ratchets compare them.

run.sh grows two modes. `gate` is the no-compiler grade: the review
runtime runs a declared task as a probe with a 300 s budget, no arguments
and the sha256 of its output recorded as evidence, so the task takes no
parameters, derives its base ref itself, runs nothing that reaches rustc
(cargo metadata --no-deps only; cargo-shear is skipped and recorded as not
measured because its full cargo metadata probes rustc -vV) and ends stdout
with `quality-gate: <verdict> report=<path> sha256=<hex>`. It takes well
under a second on this tree. `diff` adds clippy over the crates the change
touched, scoped the way clippy-strict.sh scopes itself; --coverage adds
llvm-cov. `just metrics-diff [BASE]` drives it. .minimal/minimal.toml
declares the quality-gate task and ends agent-verify with the same run.

The base-ref resolution and changed-range extraction move out of
clippy-strict.sh into scripts/lib/changed-lines.sh, and the range parsing
into scripts/metrics/changed_lines.py, so the two gates agree on which
lines are the branch's own. clippy-strict.sh keeps its output and exit
codes; a harness case drives it against a fixture repo with a canned
cargo and checks the seeded hit (2), the clean run (0) and the no-change
run.

Harness cases: base_thresholds_win_when_stricter (with a randomised
property check over stricter()), no_baseline_means_not_evaluated,
report_carries_every_section, step_summary_is_appended,
gate_runs_without_rustc (fake rustc and cargo-clippy exit 99 and log
every call; the log must stay empty),
darwin_scope_lists_uncovered_crates_in_report, gate_fits_probe_budget,
clippy_strict_unchanged, and report_is_byte_stable now covers the report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci(nightly): metrics job publishing snapshots to metrics-data

The nightly `metrics` job runs `just metrics`, renders the dashboard from
the fetched history plus the new snapshot, uploads both as an artifact, and
on main hands the snapshot to scripts/metrics/publish-snapshot.sh. That
script keeps the history on the orphan `metrics-data` branch: it fetches or
creates the branch in a temporary worktree, adds history/<date>-<sha7>.json,
prunes, commits as the Actions bot and pushes with a refspec that names the
branch on both sides, so it cannot reach main. It refuses a snapshot whose
mode is not full or whose tree was dirty. Non-blocking while it beds in;
`notify` is untouched.

Verified here: the harness pushes to a bare fixture remote and asserts main
is untouched, the branch is an orphan, and refusals exit 2; zizmor 1.30.1
reports no findings on the workflow. actionlint is not packaged for this
sandbox and did not run.

This touches .github/workflows/, which is CODEOWNER-gated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(metrics): name the failing collector when the experimental case sees exit 3

The case ran green alone and red once inside the full suite while a cargo
build held the checkout; the message now carries run.sh's FAILED line so a
recurrence says which collector it was.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(toolchain): pin 1.99.0

1.99.0 is current stable (2026-09-28). The metrics snapshot is measured
with the pin, so a pin two releases old would record lint counts that no
developer's clippy agrees with. Comments that state the pin follow it.

Not built here: the sandbox packages rustc 1.98.1 and nothing newer, so
`just clippy -D warnings` under 1.99 is proved by the lanes, not locally.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(metrics): snapshot summary on the run's step summary

`full` and `static` runs now write summary.md beside the snapshot (the
verdict, every gate, the workspace totals, module cycles, promotable lints
and the top hotspots) and append it to GITHUB_STEP_SUMMARY when set, so the
nightly's summary page shows the snapshot without opening the artifact. The
grading modes keep appending their report instead.

The experimental harness case failed once more with the collector now
named: cargo shear could not load a git dependency while another cargo
held the same CARGO_HOME. Concurrent use of one cargo home, not the
collector; green alone and in a serial suite run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(clippy): clean under 1.99; nightly mutants test the lines since the last snapshot

Clippy 1.99 under -D warnings found: needless borrows of a Copy closure in
args; double_must_use on tonic-generated async_trait code in remote-proto
(expected at the include, generated code); the new pedantic assert_is_empty
in sessions and paths, allowed in their hand-kept tables since the rewrite
says less at the site; must_use on functions returning types already
must_use, and three accessors without it, in sessions; a single-element
loop in mfile; chunks_exact(2) over a byte slice in minimald-rpc and the
switch checksum, now as_chunks; Result<_, ()> on three session-host methods
whose only error is a dead host, expected with that reason; and the
fetch_update deprecation in a minvmd test helper, expected until the MSRV
floor reaches the rename.

The nightly's mutation scope changes. A weekday shard of the workspace is
about 2,500 mutants (cargo mutants --list), which no 120-minute job can
run; run #112 was in that phase when this was written. The nightly now
tests the lines changed since the newest snapshot in the history (or the
merge-base with origin/main on a branch), under a wall-clock budget
(MINIMAL_METRICS_MUTANTS_BUDGET_SECS, 2700 by default); a run that hits the
budget reports what it tested with `partial` set, since cargo-mutants
rewrites outcomes.json after every mutant. The snapshot records the
reference under `mutants.since`.

Verified with a standalone 1.99.0 toolchain: workspace clippy clean, fmt
clean, clippy-strict clean on the changed lines, the touched crates' tests
green except minvmd's port_holder_is_none_when_the_lookup_fails, which
reads the host socket table and fails in this sandbox on main as well.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci(nightly): cap the metrics job's mutation run at 25 minutes

Coverage is a cold instrumented build and the module graphs load every
crate; the mutation run takes what is left of the 120-minute job.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(metrics): no colon in the module graph file names

actions/upload-artifact refuses a path with a colon, and the per-target DOT
files were named crate.bin:<name>.dot. The target label is now bin-<name>.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(metrics): fetch before cargo-shear; coverage survives failing tests

On a fresh runner cargo-shear ran before anything had fetched the git
dependencies, so its --offline metadata failed. run.sh now fetches once
before it. cargo llvm-cov exited 100 (nextest: tests failed under the
instrumented build) and took the whole coverage result with it; it now
runs with --ignore-run-fail, and the failing tests are counted from
nextest's junit and recorded under workspace.coverage, on the summary page
too, so the number is read with its caveat beside it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ci(nightly): install the experimental metrics tools, best effort

rustqual and cargo-anatomy have no prebuilt binaries, so the job compiles
them from crates.io and goes on without them when that fails; the snapshot
records the two collectors as not measured in that case.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(metrics): the harness never writes the job's step summary

Run inside the nightly's coverage pass, the gate cases inherited
GITHUB_STEP_SUMMARY and wrote a grading report of HEAD against itself
above the snapshot on the metrics job's summary page. The harness unsets
the variable; the cases that check the append set their own file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(metrics): drop minimal-tui and rename sessions to boxes in the layer table

minimal-tui folded into the minimal crate and the sessions crate became
boxes on main, so the table named two crates that no longer exist and
left boxes unassigned. The metadata fixture is regenerated from the
merged tree.

* ci(metrics): give the metrics job its own workflow, called by nightly-tests

metrics.yml takes the job unchanged and runs on workflow_dispatch as well
as workflow_call, so a snapshot can be taken without the rest of the
nightly. continue-on-error moves inside the called job, since a job that
calls a reusable workflow cannot set it.

* test(metrics): read the metrics job from metrics.yml

The job moved out of nightly-tests.yml, so the budget case read an empty

block there and failed the shell-harness test. It now checks metrics.yml

and that the nightly calls it.

* fix(metrics): place main's bep crate in the session layer

Main added the bep crate (Box Egress Proxy). It has no internal
dependencies and only session and product crates use it, so it joins
the session layer. Without it the layer check, which `just ci` runs,
fails on an unassigned crate. The metadata fixture is regenerated from
`cargo metadata --no-deps` with the same trimmed keys; it differs only
by bep and its users. docs/architecture.md lists bep in the session
plane and rewrites the layers paragraph to clear its prose alerts.

Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc

* fix(metrics): address review findings on the layer check and snapshot

- layers.py fails on a stale exception, so an exception that no longer
  names a forbidden edge has to be removed rather than left to let the
  edge back in.
- layers.py places only `workspace_members`, so full `cargo metadata`
  given through --metadata does not report dependencies as unassigned
  crates. small-metadata.json now lists its members, as cargo does.
- run.sh passes --coverage-tests only when nextest's junit parsed; a
  truncated junit no longer makes collect.py crash and drop the
  nightly snapshot (QM-001).
- collect.py records a null mutation score when no mutant was tested,
  not 0 %, so an all-unviable change cannot fail a seeded floor.
- crates/minimald/clippy.toml, added on main, restates the complexity
  thresholds: clippy reads only the nearest clippy.toml. The agreement
  test now checks every per-crate clippy.toml.

Each fix has a case in scripts/metrics_test.sh.

Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc

* ci(metrics): publish from a separate job; the collector holds no write token

The metrics job builds and tests the checked-out tree and installs tools
from crates.io, and it held `contents: write` with the token persisted in
.git/config. Any build script or tool could push to the unprotected
metrics-data branch the gate grades against, and a dispatch on any ref
got the same token.

The collector now runs at `contents: read` with persist-credentials off
and checks out metrics-data without keeping credentials. A `publish` job,
on main only and only for a complete snapshot (status 0 or 2), holds the
write token. It downloads the `metrics` artifact and runs main's
publish-snapshot.sh, and builds and installs nothing. nightly-tests still
grants `contents: write` because the called workflow's publish job needs
it. `just` and `cargo-binstall` are pinned to 1.58.0 and 1.24.0.

Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc

* fix(metrics): address the second review round on the harness

- Same-day snapshots: diff.py's baseline and the mutants `auto`
  reference took the last name in sort order, so two snapshots of one
  day were ordered by sha7. prune.py's new `newest` keeps the date
  order and breaks a tie by the committer time of the commit each
  snapshot measured (the snapshot itself holds no time, QM-006).
- Grading exit: in gate and diff modes a failed gate (2) now outranks
  an incomplete snapshot (3). Full mode keeps 3 first, because the
  nightly publishes only on 0 or 2; run.sh's header says so.
- metrics-mutants: run.sh mutants mode now fails (exit 3) when
  cargo-mutants or cargo-nextest is missing, instead of passing with no
  run. The task installs both through `install-tools.sh --only`, which
  owns the pins, so it drops its own copy of the cargo-mutants pin.
- layers.py matched dependency edges by name, so a registry or
  out-of-tree crate named like a member counted as an internal edge. It
  now needs a path dependency into that member's directory. The metadata
  fixture is regenerated with each dependency's `source` and `path`.
- run.sh is reachable by macOS's stock bash 3.2 (`#!/usr/bin/env bash`,
  called straight from the justfile). Empty-array expansions now use
  `${a[@]+"${a[@]}"}`, and a read loop replaces `mapfile`.

Each behaviour change has a case in scripts/metrics_test.sh that fails
against the previous scripts.

Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc

* build(deps): ctutils 0.4.5, for RUSTSEC-2026-0385

Port of #2503 so cargo-deny's advisories check passes on this branch.
The same Cargo.lock change; it becomes a no-op once #2503 lands on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc

* fix(metrics): validate the snapshot fields publish-snapshot.sh puts in paths

The publish job holds the write token and runs publish-snapshot.sh over
an artifact the collector job wrote after running the tree's build
scripts and crates.io tools, so the snapshot is untrusted input.
`git.sha7` went into the history file name unchecked: a value of `../x`
wrote outside history/, and malformed values were pushed under names
prune.py does not recognise.

The script now reads the fields in one Python step. It requires `mode`
to be the string "full", `dirty` the boolean false and `sha7` seven
lowercase hex digits, and it checks `--date` against YYYY-MM-DD over the
whole string. Nothing is written or pushed for a refused snapshot.
These are the only artifact fields that reach a path or a git command;
prune.py reads only the history branch's file names.

The harness feeds bad sha7, dirty and date values through and checks
that each is refused and nothing reaches metrics-data. It also asserts
that only the `publish` job holds the write token, on main only.

Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc

* fix(metrics): refuse a publish date that is not on the calendar

publish-snapshot.sh checked only the YYYY-MM-DD shape, so --date
2026-02-30 published a snapshot whose name prune.py later skips. The
date now also has to parse as a real date. The publish test adds
2026-02-30 and 2026-13-01 to its refused dates.

The non-blocking assertion in nightly_budget_is_declared now reads only
the metrics job's lines, so continue-on-error on publish alone no longer
satisfies it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc

* fix(metrics): publish only a snapshot of the checkout's own commit

publish-snapshot.sh checked the shape of git.sha7 but not that it named
the commit being published. The publish job checks out the run's own
commit, so a snapshot whose sha7 is not a prefix of HEAD was not made by
this run's collector; it is now refused with exit 2 before anything is
written. The publish test builds its snapshot from the test repo's HEAD
and adds a case for a well-formed sha7 of another commit, which the
previous script published.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc

* docs(metrics): the quality-metrics spec is 31

#1586 (PKV) holds docs/specs/30-spec-pkg-verify, so this one moves to 31.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc

* build(deps): crypto-bigint 0.7.7, for RUSTSEC-2026-0387

Port of #2517 so cargo-deny's advisories check passes on this branch.
The same Cargo.lock change; it becomes a no-op once #2517 lands on main.

Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc

* ci(metrics): build the metrics tools from source, cached on main

The collector installed cargo-binstall, so install-tools.sh ran each
tool's prebuilt release binary with nothing checking a digest of it,
inside the job whose snapshot the gate takes as its baseline. Without
binstall the script builds every tool with `cargo install --locked`,
from crates.io sources checked against the index.

The built binaries are cached on the pins and the toolchain, so a run
compiles only what a pin change moved. Only main saves the cache, so a
branch's build is never what main restores. The harness asserts both.

Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc

* test(metrics): require the main-only guard on every cache save

The check passed when any one save step in the collector carried the
guard, so a second, unguarded save would have slipped past it. It now
counts the save steps and the guarded ones, and they must match.

Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc

* fix(metrics): place client-config in the leaf layer and the metadata fixture

* chore(toolchain): correct the pin named in the MSRV comment

rust-toolchain.toml pins 1.99.1, not 1.99.0.

* docs(metrics): remove references to the unpublished spec 31

The spec and its QM requirement ids exist on no branch, so the links and
ids pointed nowhere. Comments keep what the ids meant; the docs point at
metrics/README.md.

* ci(metrics): make the quality gate non-blocking in agent-verify

The gate still runs and prints its report, but a failed (2) or incomplete
(3) verdict is logged instead of failing the task until the gate has soaked.

* fix(metrics): refuse to publish a snapshot that fails the schema

* fix(metrics): cap the published snapshot's size and name the renamed task

publish-snapshot.sh refuses a snapshot over 8 MiB before parsing it, so an
artifact cannot bloat the history branch every later run fetches.
docs/fuzzing.md names the task this branch renamed, metrics-mutants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhCC5TEBNzPeWgytRsU4rc

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants