fix(cli,core): close the remaining canary review findings

Six findings from review, none behaviour-critical on their own but three
of them quietly corrupt the data the rollout is judged by.

Endpoint no longer serves bucketSeed (studioServer.ts). Studio gets its
canary answers from the injected decisions map now, so nothing needed the
seed over HTTP — and an unauthenticated local endpoint is a strictly
worse place for it than a script scoped to Studio's own document. The
endpoint itself predates this PR and still serves distinctId, so it also
gains a Host guard: a remote page can rebind its hostname to 127.0.0.1
and read the response as same-origin, but the request still carries THAT
hostname, which is what makes it refusable.

predecessorFound no longer reports corruption as a fresh install. It
returned null for both "file absent" and "file unreadable", so a partial
disk write looked like a new machine — understating recoverable churn,
the one thing the field measures. Now distinguishes absent from corrupt
and emits install_state_file_corrupt alongside.

A mangled markerAt no longer discards a salvageable bucketSeed. markerAt
is only a timestamp and can be restamped; the seed cannot be recovered,
and losing it silently re-rolls the install's cohort.

The seed backfill no longer ignores its write result. An unwritable
~/.hyperframes meant a different seed every invocation with no
diagnostic, and made the field's own "backfilled once" docstring false.
Warns once per process with the underlying error.

FNV-1a's ASCII constraint is now explicit rather than incidental. It
hashes UTF-16 code units while reference FNV-1a is byte-oriented, so the
two agree only on ASCII; the registry's kebab-case assertion is what
makes non-ASCII unreachable, and both ends now say so. Not a live bug —
names are kebab-case and units are UUIDs.

de-parallel-router is pinned at 0%. The registry is data, so a ramp is a
one-line edit with no review surface, and its own description says to
ramp only alongside the circuit breaker.

Tests: 8 new (corruption vs absence, seed salvage, backfill write
failure, 17 host-guard cases, registry pin). One existing test asserted
predecessorFound: false on corruption — that was the bug, updated with a
note. Fault injection: restoring the old corrupt handling fails 4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-07-30 16:38:11 -07:00
co-authored by Claude Opus 5
parent 98b23a8850
commit 31361b8e5b
8 changed files with 294 additions and 36 deletions
+22 -2
View File
@@ -222,10 +222,30 @@ describe("parseCanaryOverride", () => {
});
describe("registry", () => {
it("has unique, kebab-case names", () => {
// Also load-bearing for the hash, not just for tidiness: fnv1a32 walks
// charCodeAt, i.e. UTF-16 code units, while reference FNV-1a is byte
// oriented. The two agree only for ASCII. Names are hashed as
// `feature:unit`, so a non-ASCII name (an accented owner tag, an emoji, a
// full-width dash from autocorrect) would silently disagree with every
// other FNV-1a implementation — including any external tool that recomputes
// cohorts. This regex is what makes that unreachable; loosening it means
// fixing the hash first.
it("has unique, ASCII kebab-case names — the hash depends on this", () => {
const names = CANARIES.map((c) => c.name);
expect(new Set(names).size).toBe(names.length);
for (const n of names) expect(n).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
for (const n of names) {
expect(n).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
// eslint-disable-next-line no-control-regex -- explicit ASCII range check
expect(n).toMatch(/^[\x00-\x7F]*$/);
}
});
// The registry is data, so a ramp is a one-line edit with no code review
// surface. This canary's own description says "ramp only alongside the
// per-install circuit breaker" — without an assertion, bumping it to 5
// before that wiring lands would go green.
it("keeps de-parallel-router at 0% until the circuit breaker is wired", () => {
expect(findCanary("de-parallel-router")?.percentage).toBe(0);
});
it("has in-range percentages and a parseable sunset date", () => {