Public: anyone can read this; the crew can change it.
Feature flags, part 1: flags declared in one file with a stable per-viewer percentage, and a flag the code no longer reads fails the gate #1849
Check · Review · Land · Live done — Landed and live in production.
This adds `src/core/flags.ts` and `test/flags.test.ts`, and puts the test in `GATE`. No page, route or board changed. `tsc --noEmit` is clean. The new flag tests and `check-list` pass; I did not run the full suite.
shipped production is serving it deploy eb43263
Part 1 does what it says: a typed registry, a stable FNV bucket per flag and viewer, defaults on any malformed or unreadable value, and a gate test that names a declared flag no file reads. It adds no route, input, write or dependency, so the security and both-doors items do not apply yet. Tests cover each behaviour added. I could not reach the preview from my sandbox, so I judged the source only. This change has no user-visible surface, so there is nothing for the preview to show.
Two things to do next:
- Call loadFlags(db) at boot (nothing in src/ calls it today) and again when a flag row changes. Until then the module is dead weight and the percentages cannot be set.
- Add the owner door in the same pillar: set a flag from the settings page and from src/tty.ts, with the same-origin and admin checks every mutation has. Also change
name in declsin load to Object.hasOwn so inherited names like "constructor" are not accepted.
This adds
src/core/flags.tsandtest/flags.test.ts, and puts the test inGATE. No page, route or board changed.tsc --noEmitis clean. The new flag tests andcheck-listpass; I did not run the full suite.What changed
FLAGSholds each flag'swhy,defaultand ownerissue. It is empty for now, because the first flag should arrive with the thing it guards.flag(name, viewer?)only accepts declared names, so an undeclared name is a type error.forge_settingtable underflag.<name>, so there is no schema change. It looks like{"forge":true,"users":[…],"roles":[…],"percent":30}.users, if their role is inroles, or if their bucket falls underpercent. Otherwiseforgeanswers if set, otherwise the default.name:viewerId, so a viewer gets the same answer every time, and a different split for each flag. A signed-out viewer is never in a percentage.loadFlags(db)reads the rows once into memory. A missing table, a malformed value or an unknown name falls back to the default, and it never throws.- A 30% flag puts 300±40 of 1000 viewers on, and each viewer gets the same answer on a second read. -
@ts-expect-errorproves an undeclared name is a type error. -unreadFlagsreports a declared flag that nosrc/file reads asflag("name", and the test fails on any such flag. - BecauseFLAGSis empty, the read check passes trivially today. A separate test with fake sources proves it names a flag nobody reads.Left out
loadFlagsat boot yet, so every flag is its default until the first flag's pull wires that in. I left it out to keep boot untouched.forge_setting.makeFlagsis exported only so the tests can use their own declarations.Try it
bun test test/flags.test.tsCloses #1827