Files
hyperframes/packages/core/src/canaryRegistry.ts
T
Vance IngallsandClaude Opus 5 3f8dca165d fix(cli,core): refresh telemetry posture at the render boundary
R6/R7 blockers.

An already-open Studio kept emitting server-side render telemetry after
another process disabled CLI telemetry. refreshTelemetryPosture() only ran
while serving a fresh SPA document and on /api/telemetry-identity, which
Studio has no consumer for, so the render POST and its async outcome used
the posture cached when the preview server booted. It now refreshes at the
render boundary and again immediately before the completion/error event,
so an opt-out during a long render is honoured.

The identity tests were passing vacuously: their mocks omitted
readConfigFresh and resetTelemetryPostureCache, and the resulting
missing-export error was swallowed by the refresh's own catch. Mocked
properly, plus the enabled -> external disable -> next response transition
and the suppression path at the layer that drops the event.

A full reset also did not persist its new lineage in a long-lived process:
syncInstallState returned early on a process-lifetime memo even after
~/.hyperframes was deleted, so install-state was never recreated and the
next config-only re-mint rolled a third seed instead of inheriting the
second. The memo is now revalidated against the file.

Also drops a stale reference to assertNoOverdueCanaries and stops the
workflow and docs claiming the sunset job routes anything to the owner —
it names them in the run log and notifies nobody.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 18:11:45 -07:00

117 lines
5.4 KiB
TypeScript

/**
* The canary registry — every staged rollout in the product, in one file.
*
* Why a registry rather than a percentage inlined at each call site: the repo
* already carries 57 loose `HF_*` / `PRODUCER_*` toggles with no index, so
* nobody can answer "what is currently rolling out, to how many people, and
* who owns it" without grepping. One table fixes that, and gives the
* telemetry and `doctor` surfaces something to enumerate.
*
* ## Adding one
*
* 1. Add an entry below. Start at `percentage: 0` and merge that — a canary
* at 0 is dead code you can land safely and ramp without a code review.
* 2. Read it at the decision point via the surface's binding (in the CLI,
* `isCanaryEnabled("your-feature")`).
* 3. Ramp by editing `percentage` in a patch release: 0 → 5 → 25 → 100.
* Widening is inclusive, so the earlier cohort stays enrolled and the
* before/after comparison survives the ramp.
* 4. At 100 and holding, DELETE the entry and the branch it guarded. That is
* the point of `sunsetAfter`.
*
* ## Overriding
*
* `HF_CANARY_<FEATURE>` with the feature name upper-snake-cased, e.g.
* `HF_CANARY_DE_PARALLEL_ROUTER=on` (also: off/true/false/1/0/yes/no).
* An override always wins over the percentage, in both directions.
*/
export interface CanaryDefinition {
/** Registry key. Kebab-case; also the hash input, so renaming reshuffles the cohort. */
name: string;
/** 0-100. Start at 0, ramp in patch releases. */
percentage: number;
/** What turning this on actually changes, in one line. */
description: string;
/** Who to ask. */
owner: string;
/**
* ISO date after which this canary is overdue for removal. A canary that
* outlives its rollout is a permanent fork of the product with none of the
* review a permanent fork would have received. The scheduled `Canary sunset`
* workflow runs `scripts/check-canary-sunset.ts` weekly and fails once the
* date passes, so this is an enforced deadline rather than a good intention.
*/
sunsetAfter: string;
}
export const CANARIES: readonly CanaryDefinition[] = [
// ── Calibration ──────────────────────────────────────────────────────────
// Two INERT canaries that gate nothing. They exist to validate the rollout
// mechanism against real traffic before anything real depends on it, and
// they answer questions the synthetic tests cannot:
//
// 1. Does a requested percentage land on target in the wild? The unit
// tests use generated UUIDs and weight every install equally; real
// render volume is heavily skewed toward a few heavy installs, so the
// render-weighted share could differ from the install-weighted one.
// 2. How fast does CUMULATIVE exposure drift above the target? Install
// ids churn (measured: 24.7x more distinct ids over 30 days than in
// any single day), so the set of installs enrolled AT SOME POINT grows
// even though the instantaneous share stays flat. That drift is the
// real limit on a canary's blast-radius guarantee.
// 3. Are two canaries actually independent on real ids, not just on
// generated ones? Overlap should be ~p1*p2, not ~min(p1,p2).
//
// Two different percentages so the answer is a line, not a point.
// Delete both once the calibration window is read.
{
name: "calibration-10",
percentage: 10,
description: "Inert. Validates rollout accuracy and cumulative-exposure drift at 10%.",
owner: "vance",
sunsetAfter: "2026-09-15",
},
{
name: "calibration-50",
percentage: 50,
description:
"Inert. Second calibration point, and an independence check against calibration-10.",
owner: "vance",
sunsetAfter: "2026-09-15",
},
// ── Real rollouts ────────────────────────────────────────────────────────
{
name: "de-parallel-router",
percentage: 0,
description:
"Route auto multi-worker renders to verified parallel drawElement streaming (HF_DE_PARALLEL_ROUTER). Ramp only alongside the per-install circuit breaker.",
owner: "vance",
sunsetAfter: "2026-10-01",
},
] as const;
export function findCanary(name: string): CanaryDefinition | undefined {
return CANARIES.find((c) => c.name === name);
}
/** Env-var name for a feature's manual override: `de-parallel-router` → `HF_CANARY_DE_PARALLEL_ROUTER`. */
export function canaryEnvVar(name: string): string {
return `HF_CANARY_${name.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
}
/**
* Names of canaries whose sunset date has passed — either finish the rollout
* and delete the entry, or push the date with a reason. Exposed as a function
* (not a lint rule) so the check runs in the normal test suite.
*/
export function overdueCanaries(now: Date = new Date()): string[] {
return CANARIES.filter((c) => {
// End of the sunset day, not its start. The field is documented as the
// date AFTER which a canary is overdue, but comparing against midnight UTC
// made it overdue ON that date — and earlier still for anyone west of UTC.
const sunsetEnd = Date.parse(`${c.sunsetAfter}T23:59:59.999Z`);
return Number.isFinite(sunsetEnd) && now.getTime() > sunsetEnd;
}).map((c) => c.name);
}