Skip to content

Refactor: single source of truth for RuleId, enforced at compile time#243

Merged
twschiller merged 1 commit into
mainfrom
refactor/single-source-rule-id
Jun 10, 2026
Merged

Refactor: single source of truth for RuleId, enforced at compile time#243
twschiller merged 1 commit into
mainfrom
refactor/single-source-rule-id

Conversation

@twschiller

Copy link
Copy Markdown
Contributor

Summary

Collapses the rule catalog's two independent RuleId types into one, and moves catalog agreement from a runtime test to author-time compile errors.

Background

Adding a rule had to keep multiple hand-maintained lists in agreement, with two independent RuleId types only reconciled by catalog.test.ts:

  • rules/index.ts derived RuleId from RULES_TUPLE
  • rules/rule-metadata.ts derived RuleId from RULE_DEFAULTS

Investigating this surfaced a worse problem: index's RuleId had silently collapsed to string. Factory-built rules (and a couple of : Rule-annotated rules) widen id to string, and a single id: string element poisons the whole (typeof RULES_TUPLE)[number]["id"] union. So RuleId was a vacuous supertype, and every RuleStates / RuleAvailabilityStates map keyed by it was effectively Record<string, …> — the runtime test was the only thing keeping the catalog honest.

What changed

  • rule-metadata.ts is the sole RuleId / RULE_IDS source. index.ts re-exports both and constrains its tuple rather than re-deriving:
    • satisfies readonly { id: RuleId }[] → registering a rule without a RULE_DEFAULTS entry is a compile error (forward).
    • a reverse type-level assertion that names the offender → declaring a RuleId without a registered runtime is a compile error.
    • The catalog test stays as belt-and-suspenders.
  • Literal id now flows through so the checks aren't vacuous: createSelectorHideRule and defineInlineTextRedactRule are generic over Id extends RuleId; chat-widget-hide, ads-hide, irrelevant-sections-redact pin their id.
  • CatalogRule = Rule & { id: RuleId } narrows RULES, fixing previously-unsound string-keyed indexing in rule-engine, storage, availability, lifecycle, RuleList, parse-config, page-world-hooks (these were the type holes string-RuleId had masked).

Deliberately not done

RULE_LABELS stays in popup/rule-labels.ts — the labels are the canary for check-background-purity.ts (it greps background.js for each label string), so they can't move into worker-imported rule-metadata.ts. That duplication remains, guarded by its sync test.

Verification

  • bun run check
  • bun run typecheck
  • bun run test — 2052 tests ✓
  • bun run build ✓ (background.js purity: 39 canaries, no leaks)
  • Controlled negative tests confirm both compile-time guards fire with self-explaining messages.

🤖 Generated with Claude Code

The rule catalog had two independent `RuleId` types — `rules/index.ts`
derived one from `RULES_TUPLE`, `rules/rule-metadata.ts` derived one from
`RULE_DEFAULTS` — kept in agreement only by a runtime test
(`catalog.test.ts`). Worse, index's `RuleId` had silently collapsed to
`string`: factory-built rules and `: Rule`-annotated rules widen `id` to
`string`, and one such element poisons the whole `(typeof tuple)[number]["id"]`
union. So `RuleId` was a vacuous supertype and every `RuleStates` /
`RuleAvailabilityStates` map keyed by it was really `Record<string, …>`.

Make `rule-metadata.ts` the sole `RuleId` / `RULE_IDS` source and have
`index.ts` constrain its tuple against it instead of re-deriving:

  - `satisfies readonly { id: RuleId }[]` — registering a rule without a
    `RULE_DEFAULTS` entry is now a compile error (forward direction).
  - a reverse type-level assertion naming the offender — declaring a `RuleId`
    without a registered runtime is now a compile error.

Both replace runtime-only catalog checks with author-time guarantees (the
catalog test stays as belt-and-suspenders).

To make those checks non-vacuous, the literal `id` now flows through:
`createSelectorHideRule` and `defineInlineTextRedactRule` are generic over
`Id extends RuleId`; `chat-widget-hide`, `ads-hide`, and
`irrelevant-sections-redact` pin their id with `as const` / `satisfies`.

With `RuleId` now a real union, a new `CatalogRule = Rule & { id: RuleId }`
type narrows `RULES`, fixing the previously-unsound `string`-keyed indexing in
`rule-engine`, `storage`, `availability`, `lifecycle`, `RuleList`,
`parse-config`, and `page-world-hooks`.

Labels stay in `popup/rule-labels.ts`: they are the canary for
`check-background-purity.ts`, so they can't move into worker-imported metadata.

Verified: check, typecheck, 2052 tests, build (purity ok), and controlled
negative tests confirming both compile-time guards fire.

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

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agent-browser-shield-demo-site Ready Ready Preview, Comment Jun 10, 2026 2:51am

Request Review

@twschiller twschiller merged commit 1ef2411 into main Jun 10, 2026
7 checks passed
@twschiller twschiller deleted the refactor/single-source-rule-id branch June 10, 2026 02:54
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.

1 participant