diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02f0929..4bfd582 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,13 +46,12 @@ jobs: # Codegen freshness is its own step because preflight regenerates the # files internally — the diff has to be captured against pristine git # state before preflight runs. - - name: Codegen freshness (site data + injection patterns + rule defaults) + - name: Codegen freshness (site data + injection patterns) run: | bun run build-site-data bun run build-injection-patterns - bun run build-rule-defaults - if ! git diff --exit-code -- src/rules/site-data.generated.ts src/rules/injection-patterns.generated.ts src/rules/rule-defaults.generated.ts; then - echo "::error::Generated files are out of date. Run \`bun run build-site-data && bun run build-injection-patterns && bun run build-rule-defaults\` locally and commit the result." + if ! git diff --exit-code -- src/rules/site-data.generated.ts src/rules/injection-patterns.generated.ts; then + echo "::error::Generated files are out of date. Run \`bun run build-site-data && bun run build-injection-patterns\` locally and commit the result." exit 1 fi - name: Preflight (lint + typecheck + knip + test with coverage) diff --git a/AGENTS.md b/AGENTS.md index 5790fc4..a9cc5df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -121,12 +121,14 @@ itself, so name collisions and convention drift surface at lint time. ## Rule defaults The initial enabled/disabled state for each rule lives in -`extension/data/rule-defaults.json`, not on the rule modules themselves. The -codegen in `extension/scripts/build-rule-defaults.ts` validates that every -registered rule id has a default (and that no unknown ids appear) and emits -`extension/src/rules/rule-defaults.generated.ts`, which -`extension/src/lib/storage.ts` imports. Adding a rule without picking a default -fails the build; do not edit the generated file. +`extension/src/rules/rule-metadata.ts`, not on the rule modules themselves. This +hand-edited file is the source of truth for `RuleId`, `RULE_IDS`, and +`RULE_DEFAULTS`; `extension/src/lib/storage.ts` imports it. It is kept out of +`rules/index.ts` so the service-worker bundle doesn't pull rule files' top-level +DOM access. Adding a rule means appending an entry both here and in +`rules/index.ts`; the catalog test in +`extension/src/rules/__tests__/catalog.test.ts` enforces that the two stay in +sync. `extension/build.ts` accepts a `--defaults ` CLI flag (or `EXTENSION_DEFAULTS_FILE` env var) pointing at a JSON file in the same shape as diff --git a/README.md b/README.md index a2b67f2..ace4661 100644 --- a/README.md +++ b/README.md @@ -76,12 +76,14 @@ bun run build ### Customize build-time defaults Which rules ship on by default is enumerated in -[`extension/data/rule-defaults.json`](./extension/data/rule-defaults.json). To -ship a build with a custom set without forking the repo, pass an override file -to `bun run build`. The file is a flat JSON object whose keys are rule ids (same -keys the Options-page export uses) plus a small set of reserved non-rule keys — -currently `optionsButton` (boolean, default off) to enable the floating on-page -button that opens the options page: +[`extension/src/rules/rule-metadata.ts`](./extension/src/rules/rule-metadata.ts). +To ship a build with a custom set without forking the repo, pass an override +file to `bun run build`. The file is a flat JSON object whose keys are rule ids +(same keys the Options-page export uses) plus a small set of reserved non-rule +keys — currently `optionsButton` (boolean, default off) to enable the floating +on-page button that opens the options page. See +[`extension/data/defaults-overrides.example.json`](./extension/data/defaults-overrides.example.json) +for a starting template: ```sh bun run build --defaults ./my-defaults.json diff --git a/docs/src/content/docs/install.md b/docs/src/content/docs/install.md index 28365ac..ae86098 100644 --- a/docs/src/content/docs/install.md +++ b/docs/src/content/docs/install.md @@ -62,12 +62,14 @@ The extension is now active. Reload any open tabs to pick up the content script. ## Customizing defaults at build time Which rules ship on by default is enumerated in -[`extension/data/rule-defaults.json`](https://github.com/pixiebrix/agent-browser-shield/blob/main/extension/data/rule-defaults.json). +[`extension/src/rules/rule-metadata.ts`](https://github.com/pixiebrix/agent-browser-shield/blob/main/extension/src/rules/rule-metadata.ts). For one-off changes, edit that file and rebuild. For infrastructure deployments where the same custom set of defaults should ship in every build (so an agent doesn't have to flip toggles in the Options page on -each fresh session), pass a JSON override file to `bun run build`: +each fresh session), pass a JSON override file to `bun run build`. A starting +template lives at +[`extension/data/defaults-overrides.example.json`](https://github.com/pixiebrix/agent-browser-shield/blob/main/extension/data/defaults-overrides.example.json): ```sh cat > my-defaults.json <<'EOF' diff --git a/docs/src/content/docs/rules.md b/docs/src/content/docs/rules.md index 40bfd84..fa3f611 100644 --- a/docs/src/content/docs/rules.md +++ b/docs/src/content/docs/rules.md @@ -16,7 +16,7 @@ grouping the options page uses; rule IDs here match the filenames in [`extension/src/rules/`](https://github.com/pixiebrix/agent-browser-shield/tree/main/extension/src/rules), which is the authoritative source for behavior. Initial enabled/disabled state for each rule lives in -[`extension/data/rule-defaults.json`](https://github.com/pixiebrix/agent-browser-shield/blob/main/extension/data/rule-defaults.json). +[`extension/src/rules/rule-metadata.ts`](https://github.com/pixiebrix/agent-browser-shield/blob/main/extension/src/rules/rule-metadata.ts). If this page disagrees with either, trust the source. The [Install page](/install/#customizing-defaults-at-build-time) covers how to override defaults at build time without forking the repo. diff --git a/extension/build.ts b/extension/build.ts index 7b9eb51..01c64c4 100644 --- a/extension/build.ts +++ b/extension/build.ts @@ -5,7 +5,6 @@ import { readFileSync } from "node:fs"; import { cp, mkdir, rm } from "node:fs/promises"; import { isAbsolute, join, resolve } from "node:path"; import { generateInjectionPatterns } from "./scripts/build-injection-patterns"; -import { generateRuleDefaults } from "./scripts/build-rule-defaults"; import { generateSiteData } from "./scripts/build-site-data"; import { checkBackgroundPurity } from "./scripts/check-background-purity"; import { loadDefaultOverrides } from "./scripts/load-default-overrides"; @@ -83,19 +82,16 @@ const defaultsEnvPath = readEnvValue("EXTENSION_DEFAULTS_FILE"); const defaultsPath = defaultsFlagPath ?? (defaultsEnvPath || undefined); async function build(): Promise { - // Regenerate src/rules/site-data.generated.ts from data/sites/*.yaml, - // src/rules/injection-patterns.generated.ts from data/injection-patterns.yaml, - // and src/rules/rule-defaults.generated.ts from data/rule-defaults.json. + // Regenerate src/rules/site-data.generated.ts from data/sites/*.yaml and + // src/rules/injection-patterns.generated.ts from data/injection-patterns.yaml. // Cheap and idempotent; ensures dev never forgets to rerun codegen. generateSiteData(); generateInjectionPatterns(); - generateRuleDefaults(); // Resolve the optional --defaults / EXTENSION_DEFAULTS_FILE override - // against the codegen output's RULE_DEFAULTS so the validator knows the - // current rule registry. Importing the generated file after codegen above - // means the rule id set is always fresh. - const { RULE_DEFAULTS } = await import("./src/rules/rule-defaults.generated"); + // against the hand-edited RULE_DEFAULTS so the validator knows the current + // rule registry. + const { RULE_DEFAULTS } = await import("./src/rules/rule-metadata"); const knownRuleIds = Object.keys(RULE_DEFAULTS); const overrides = defaultsPath ? loadDefaultOverrides({ diff --git a/extension/data/defaults-overrides.example.json b/extension/data/defaults-overrides.example.json new file mode 100644 index 0000000..f89392e --- /dev/null +++ b/extension/data/defaults-overrides.example.json @@ -0,0 +1,8 @@ +{ + "reviews-redact": false, + "ads-hide": false, + "irrelevant-sections-redact": true, + + "optionsButton": true, + "runOnInactiveTabs": false +} diff --git a/extension/data/rule-defaults.json b/extension/data/rule-defaults.json deleted file mode 100644 index 4104526..0000000 --- a/extension/data/rule-defaults.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "defaults": { - "pii-redact": true, - "secrets-redact": true, - "reviews-redact": true, - "comments-redact": true, - "prompt-injection-redact": true, - "countdown-timer-redact": true, - "scarcity-redact": true, - "confirmshame-sanitize": true, - "footer-redact": true, - "checkout-checkbox-sanitize": true, - "cookie-banner-hide": true, - "chat-widget-hide": true, - "html-comment-strip": true, - "hidden-text-strip": true, - "unicode-invisibles-strip": true, - "noscript-strip": true, - "json-ld-sanitize": true, - "attribute-injection-sanitize": true, - "meta-injection-strip": true, - "newsletter-modal-hide": true, - "svg-sprite-strip": true, - "svg-text-strip": true, - "social-embed-redact": true, - "ads-hide": true, - "cart-addon-annotate": true, - "link-spoof-annotate": true, - "search-url-helper": true, - "roach-motel-annotate": true, - "irrelevant-sections-redact": false, - "cross-origin-frame-redact": false, - "schema-trust-sanitize": false, - "trust-badge-annotate": false, - "disguised-ad-flag": true, - "encoded-payload-redact": true, - "webdriver-probe-annotate": false, - "closed-shadow-root-annotate": false, - "hidden-fee-annotate": true, - "form-prefill-annotate": true, - "hidden-affiliate-sanitize": true - } -} diff --git a/extension/data/rule-defaults.schema.ts b/extension/data/rule-defaults.schema.ts deleted file mode 100644 index fee4231..0000000 --- a/extension/data/rule-defaults.schema.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2026 PixieBrix, Inc. -// Licensed under PolyForm Shield 1.0.0 — see LICENSE. - -// Schema for extension/data/rule-defaults.json — the single source of truth -// for which rules ship on by default in the prebuilt extension. The codegen -// script `scripts/build-rule-defaults.ts` validates the JSON against -// `RuleDefaultsSchema` and emits `src/rules/rule-defaults.generated.ts`. -// -// Completeness (every registered rule id appears, no extras) is enforced by -// the codegen, not by zod, because it has to read `RULE_IDS` from the rule -// registry at generate time. This schema just enforces the file shape: -// `{ "defaults": Record }` with no other top-level keys, so -// the file can grow sibling metadata later (e.g. preset names) without -// breaking older builds. - -import { z } from "zod"; - -export const RuleDefaultsSchema = z - .object({ - defaults: z.record(z.string().min(1), z.boolean()), - }) - .strict(); - -export type RuleDefaultsFile = z.infer; diff --git a/extension/eslint.config.js b/extension/eslint.config.js index 6e190e6..6098340 100644 --- a/extension/eslint.config.js +++ b/extension/eslint.config.js @@ -286,11 +286,11 @@ export default tseslint.config( }, { // lib/ owns the runtime engine and shared helpers. It depends on the - // rule catalog (rules/index), the contract (rules/types), and the - // generated defaults table — never on a specific rule implementation. - // Reaching into one rule from lib quietly couples a helper to that - // rule's internals and makes the next rule that needs the same hook - // copy-paste instead of factor. + // rule catalog (rules/index), the contract (rules/types), the + // generated tables, and the hand-edited rule-metadata table — never on + // a specific rule implementation. Reaching into one rule from lib + // quietly couples a helper to that rule's internals and makes the next + // rule that needs the same hook copy-paste instead of factor. files: ["src/lib/**/*.ts", "src/lib/**/*.tsx"], ignores: ["src/lib/**/__tests__/**"], rules: { @@ -302,11 +302,11 @@ export default tseslint.config( target: "./src/lib", from: "./src/rules/*.ts", except: [ - rulePath("{index,types}.ts"), + rulePath("{index,types,rule-metadata}.ts"), rulePath("*.generated.ts"), ], message: - "lib/ may only depend on rules/{index,types,*.generated} — not on a specific rule file.", + "lib/ may only depend on rules/{index,types,rule-metadata,*.generated} — not on a specific rule file.", }, ], }, diff --git a/extension/package.json b/extension/package.json index f723ba8..9cc09c3 100644 --- a/extension/package.json +++ b/extension/package.json @@ -27,7 +27,6 @@ "fetch-justdeleteme": "uv run --directory .. scripts/fetch_justdeleteme.py", "build-site-data": "bun run scripts/build-site-data.ts", "build-injection-patterns": "bun run scripts/build-injection-patterns.ts", - "build-rule-defaults": "bun run scripts/build-rule-defaults.ts", "build-icons": "bun run scripts/build-icons.ts", "clean": "rm -rf dist", "test": "jest", @@ -40,7 +39,7 @@ "lint:eslint:fix": "eslint . --fix", "knip": "knip", "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json --noEmit", - "preflight": "bun run build-site-data && bun run build-injection-patterns && bun run build-rule-defaults && bun run check && bun run typecheck && bun run knip && bun run test:coverage" + "preflight": "bun run build-site-data && bun run build-injection-patterns && bun run check && bun run typecheck && bun run knip && bun run test:coverage" }, "dependencies": { "abort-utils": "^3.0.0", diff --git a/extension/scripts/build-rule-defaults.ts b/extension/scripts/build-rule-defaults.ts deleted file mode 100644 index 62df9ee..0000000 --- a/extension/scripts/build-rule-defaults.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2026 PixieBrix, Inc. -// Licensed under PolyForm Shield 1.0.0 — see LICENSE. - -// Compiles extension/data/rule-defaults.json into a TypeScript module -// consumed by `src/lib/storage.ts` and `src/rules/index.ts`. Same pattern as -// build-site-data.ts and build-injection-patterns.ts — generated output is -// committed and bundled statically, the runtime never reads JSON at startup. -// -// The JSON is the single source of truth for which rules ship on by default -// in the prebuilt extension AND for the canonical rule id set (`RuleId` is -// derived from the keys here). Drift between the rule registry and these -// keys is caught at runtime by `__tests__/catalog.test.ts`, which compares -// `RULES.map(r => r.id)` to `RULE_IDS`. -// -// This module has no transitive import of `src/rules` — that was the path -// pulling DOM-touching rule files into the service-worker bundle. - -import { readFileSync, writeFileSync } from "node:fs"; -import { join, relative } from "node:path"; -import { RuleDefaultsSchema } from "../data/rule-defaults.schema"; - -const ROOT = join(import.meta.dir, ".."); -const INPUT = join(ROOT, "data", "rule-defaults.json"); -const OUTPUT = join(ROOT, "src", "rules", "rule-defaults.generated.ts"); - -function buildOutput(defaults: Record): string { - const ids = Object.keys(defaults); - const lines: string[] = [ - "// AUTO-GENERATED — do not edit by hand.", - "// Source: extension/data/rule-defaults.json", - "// Regenerate with `bun run build-rule-defaults`.", - "", - "// Source of truth for `RuleId` and `RULE_IDS`. Lives outside `rules/index.ts`", - "// so service-worker code (`lib/storage.ts`, `background.ts`) can import the", - "// id set without pulling in any rule file's top-level DOM access.", - "", - "export const RULE_DEFAULTS = {", - ]; - for (const id of ids) { - lines.push(` ${JSON.stringify(id)}: ${defaults[id]},`); - } - lines.push( - "} as const satisfies Readonly>;", - "", - "export type RuleId = keyof typeof RULE_DEFAULTS;", - "", - "export const RULE_IDS = Object.keys(RULE_DEFAULTS) as readonly RuleId[];", - "", - ); - return lines.join("\n"); -} - -export function generateRuleDefaults(): void { - const raw = readFileSync(INPUT, "utf8"); - let parsed: unknown; - try { - parsed = JSON.parse(raw); - } catch (error) { - throw new Error( - `${relative(ROOT, INPUT)}: JSON parse error — ${(error as Error).message}`, - { cause: error }, - ); - } - const result = RuleDefaultsSchema.safeParse(parsed); - if (!result.success) { - const issues = result.error.issues - .map((issue) => `${issue.path.join(".") || "(root)"} — ${issue.message}`) - .join("\n - "); - throw new Error( - `${relative(ROOT, INPUT)}: schema validation failed:\n - ${issues}`, - ); - } - - const declared = result.data.defaults; - writeFileSync(OUTPUT, buildOutput(declared)); - console.log( - `Generated ${relative(ROOT, OUTPUT)} from ${Object.keys(declared).length} rules.`, - ); -} - -if (import.meta.main) { - generateRuleDefaults(); -} diff --git a/extension/scripts/check-background-purity.ts b/extension/scripts/check-background-purity.ts index c9cf00e..ccd75c7 100644 --- a/extension/scripts/check-background-purity.ts +++ b/extension/scripts/check-background-purity.ts @@ -98,7 +98,7 @@ export function checkBackgroundPurity(): void { `${relative(ROOT, BUNDLE)} leaks ${leaks.length} rule implementation file(s):\n${sample}${tail}\n\n` + "The background service worker must not import rule files. Check that " + "any new lib/* code in the background's import graph uses " + - "`rules/rule-defaults.generated` for RuleId/RULE_IDS rather than " + + "`rules/rule-metadata` for RuleId/RULE_IDS rather than " + "`rules/index.ts`.", ); } diff --git a/extension/src/background.ts b/extension/src/background.ts index 567fdd7..0c92b89 100644 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -3,6 +3,7 @@ import { startCheckoutCheckboxDefenseRegistration } from "./lib/checkout-checkbox-defense-registration"; import { installCheckoutCheckboxDefense } from "./lib/checkout-checkbox-defense-source"; +import { debugTraceStorage } from "./lib/debug-trace"; import { appendEvent as appendDebugTraceEvent, clearTab as clearDebugTraceTab, @@ -27,8 +28,8 @@ import { installShadowRootProbe } from "./lib/shadow-root-probe-source"; import { ruleStatesStorage } from "./lib/storage"; import { startWebdriverProbeRegistration } from "./lib/webdriver-probe-registration"; import { installProbe } from "./lib/webdriver-probe-source"; -import type { RuleId } from "./rules/rule-defaults.generated"; -import { RULE_IDS } from "./rules/rule-defaults.generated"; +import type { RuleId } from "./rules/rule-metadata"; +import { RULE_IDS } from "./rules/rule-metadata"; // Per-tab, per-frame, per-rule footprint counts. Each content script reports // its own frame's tally grouped by rule id; the badge shows the cross-frame @@ -189,13 +190,31 @@ chrome.tabs.onRemoved.addListener((tabId) => { // On a top-level navigation, drop stale per-frame counts so the new document // starts from zero. The content script will report fresh numbers as rules run. -chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { - if (changeInfo.status === "loading") { - clearTab(tabId); - void clearDebugTraceTab(tabId).catch(() => { - // noop - }); +// The debug trace is *not* cleared — instead a `navigation` entry is appended +// so a single export can span multiple page loads in the same tab. Gated on +// the same toggle that gates content-script emission so the trace stays empty +// when the toggle is off. +chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (changeInfo.status !== "loading") { + return; } + clearTab(tabId); + void (async () => { + try { + if (!(await debugTraceStorage.get())) { + return; + } + const entry: DebugTraceEntry = { + type: "navigation", + url: tab.url ?? null, + timestamp: Date.now(), + }; + // Frame id 0 — top-level navigation is always the main frame. + await appendDebugTraceEvent(tabId, 0, entry); + } catch { + // noop — storage read or IDB write rejection shouldn't surface. + } + })(); }); // Re-render every tab's badge when enforcement is toggled. When disabled, the @@ -293,7 +312,7 @@ chrome.runtime.onMessage.addListener( // disallowed) reject here. The primary registration silently // skips these origins via match-pattern filtering; the fallback // has to swallow the rejection explicitly. - log("inject-webdriver-probe executeScript failed", { error }); + log.error("inject-webdriver-probe executeScript failed", { error }); }); return undefined; } @@ -320,7 +339,7 @@ chrome.runtime.onMessage.addListener( func: installCheckoutCheckboxDefense, }) .catch((error: unknown) => { - log("inject-checkout-checkbox-defense executeScript failed", { + log.error("inject-checkout-checkbox-defense executeScript failed", { error, }); }); @@ -349,7 +368,7 @@ chrome.runtime.onMessage.addListener( func: installShadowRootProbe, }) .catch((error: unknown) => { - log("inject-shadow-root-probe executeScript failed", { error }); + log.error("inject-shadow-root-probe executeScript failed", { error }); }); return undefined; } @@ -438,7 +457,7 @@ chrome.runtime.onMessage.addListener( frameId, entry as DebugTraceEntry, ).catch((error: unknown) => { - log("debug-trace IDB write failed", { error }); + log.error("debug-trace IDB write failed", { error }); }); } return undefined; diff --git a/extension/src/content.ts b/extension/src/content.ts index bc02c21..4f3b0fb 100644 --- a/extension/src/content.ts +++ b/extension/src/content.ts @@ -2,6 +2,7 @@ // Licensed under PolyForm Shield 1.0.0 — see LICENSE. import { isTopFrame } from "./lib/frame"; +import { log } from "./lib/log"; import { startOptionsBadge } from "./lib/options-badge"; import { startRuleCountReporter } from "./lib/rule-count"; import { start } from "./lib/rule-engine"; @@ -23,7 +24,7 @@ installShadowRootHook(); // content-script entry, so a promise chain is required. // eslint-disable-next-line unicorn/prefer-top-level-await start().catch((error: unknown) => { - console.error("[abs] failed to start rule engine", error); + log.error("failed to start rule engine", error); }); // Per-frame, per-rule footprint reporter — background aggregates across diff --git a/extension/src/lib/__tests__/debug-trace-store.test.ts b/extension/src/lib/__tests__/debug-trace-store.test.ts index 736c6ca..435336e 100644 --- a/extension/src/lib/__tests__/debug-trace-store.test.ts +++ b/extension/src/lib/__tests__/debug-trace-store.test.ts @@ -41,6 +41,14 @@ function appEntry(segmentId: number): DebugTraceEntry { }; } +function navEntry(url: string | null): DebugTraceEntry { + return { + type: "navigation", + url, + timestamp: Date.now(), + }; +} + beforeEach(async () => { await __clearAllForTesting(); __resetDebugTraceStoreForTesting(); @@ -101,4 +109,24 @@ describe("debug-trace store", () => { expect(tab1.byteSize).toBeGreaterThan(tab2.byteSize); expect(empty.byteSize).toBe(0); }); + + it("navigation entries persist and contribute to byteSize but not eventCount", async () => { + await appendEvent(1, 0, navEntry("https://example.com/a")); + await appendEvent(1, 0, appEntry(1)); + await appendEvent(1, 0, navEntry("https://example.com/b")); + + const stored = await getEventsForTab(1); + expect(stored.map((record) => record.entry.type)).toEqual([ + "navigation", + "rule-application", + "navigation", + ]); + + const stats = await getTabStats(1); + // Only the rule-application contributes to eventCount; navigation + // markers are bookkeeping like segment markers. + expect(stats.eventCount).toBe(1); + // All three entries contribute to the on-disk footprint readout. + expect(stats.byteSize).toBeGreaterThan(JSON.stringify(appEntry(1)).length); + }); }); diff --git a/extension/src/lib/__tests__/storage.test.ts b/extension/src/lib/__tests__/storage.test.ts index 23dcf26..a19eda5 100644 --- a/extension/src/lib/__tests__/storage.test.ts +++ b/extension/src/lib/__tests__/storage.test.ts @@ -11,7 +11,7 @@ // the freshly-derived defaults instead of any stale stored value from a // previous case. -import { RULE_DEFAULTS as RAW_RULE_DEFAULTS } from "../../rules/rule-defaults.generated"; +import { RULE_DEFAULTS as RAW_RULE_DEFAULTS } from "../../rules/rule-metadata"; import type * as Storage from "../storage"; type StorageModule = typeof Storage; diff --git a/extension/src/lib/checkout-checkbox-defense-registration.ts b/extension/src/lib/checkout-checkbox-defense-registration.ts index 6bdda3c..c71ceff 100644 --- a/extension/src/lib/checkout-checkbox-defense-registration.ts +++ b/extension/src/lib/checkout-checkbox-defense-registration.ts @@ -50,7 +50,7 @@ async function isRegistered(): Promise { // getRegisteredContentScripts throws if no script with the id exists // in some Chrome versions; treat that as "not registered" rather than // a failure mode that prevents registration. - log("checkout-checkbox-defense registration: getRegistered threw", { + log.warn("checkout-checkbox-defense registration: getRegistered threw", { error, }); return false; @@ -76,20 +76,22 @@ async function register(): Promise { persistAcrossSessions: true, }, ]); - log("checkout-checkbox-defense registered at document_start (main world)"); + log.info( + "checkout-checkbox-defense registered at document_start (main world)", + ); } catch (error) { - log("checkout-checkbox-defense registration failed", { error }); + log.error("checkout-checkbox-defense registration failed", { error }); } } async function unregister(): Promise { try { await chrome.scripting.unregisterContentScripts({ ids: [SCRIPT_ID] }); - log("checkout-checkbox-defense unregistered"); + log.info("checkout-checkbox-defense unregistered"); } catch (error) { // Unregister fails if the script wasn't registered to begin with; // that's a benign state, not a problem. - log("checkout-checkbox-defense unregister no-op", { error }); + log.debug("checkout-checkbox-defense unregister no-op", { error }); } } diff --git a/extension/src/lib/debug-trace.ts b/extension/src/lib/debug-trace.ts index 757d939..a68052f 100644 --- a/extension/src/lib/debug-trace.ts +++ b/extension/src/lib/debug-trace.ts @@ -7,6 +7,11 @@ // the outerHTML before/after the swap so a false-positive report can be // reproduced offline. // +// The same toggle also unmasks `log.debug` console output (see +// `lib/log.ts`) — one switch silences both surfaces. Use this module +// for structured before/after HTML capture; use `log.debug` for +// unstructured human-readable diagnostic lines. +// // The toggle gates emission at the source: when off, `recordRuleApplication` // and `recordSegment` are no-ops. Callers that route through `traceMutation` // skip the outerHTML serialization entirely on the off path; direct callers diff --git a/extension/src/lib/detection-messages.ts b/extension/src/lib/detection-messages.ts index 24d088e..a32f256 100644 --- a/extension/src/lib/detection-messages.ts +++ b/extension/src/lib/detection-messages.ts @@ -10,7 +10,7 @@ // the popup — keep this file dependency-free so it stays trivially // bundleable into each of those three entry points. -import type { RuleId } from "../rules/rule-defaults.generated"; +import type { RuleId } from "../rules/rule-metadata"; import type { RoachMotelDifficulty } from "../rules/site-data.generated"; export type DetectionKind = @@ -167,9 +167,26 @@ export interface RuleApplicationEvent { cssOnly?: boolean; } +// Top-level navigation marker emitted by the background service worker on +// every `chrome.tabs.onUpdated` loading transition. Lets the trace span +// multiple page loads in the same tab — useful when a single user flow +// crosses documents and the developer wants to see all of it in one +// export. Timestamped in the background, so a single tab's events stay +// chronologically ordered alongside content-script-emitted entries even +// without a shared clock. +export interface NavigationEvent { + // Tab URL at the moment the loading transition fired. May be the + // previous URL on a same-page reload, or the new URL on a cross-page + // navigation — Chrome updates `tab.url` lazily relative to `status`. + // Null when the tab has no URL yet (initial new-tab navigation). + url: string | null; + timestamp: number; +} + export type DebugTraceEntry = | ({ type: "segment" } & SegmentMarker) - | ({ type: "rule-application" } & RuleApplicationEvent); + | ({ type: "rule-application" } & RuleApplicationEvent) + | ({ type: "navigation" } & NavigationEvent); export interface DebugTraceEventMessage { type: "debug-trace-event"; diff --git a/extension/src/lib/log.ts b/extension/src/lib/log.ts index 4068385..0f4c450 100644 --- a/extension/src/lib/log.ts +++ b/extension/src/lib/log.ts @@ -1,17 +1,75 @@ // Copyright (c) 2026 PixieBrix, Inc. // Licensed under PolyForm Shield 1.0.0 — see LICENSE. -// Lightweight debug logging used to trace rule application and placeholder -// reveal interactions. Logs are unconditional so they show up in the page's -// devtools console without needing a verbose-level toggle — adjust here if -// they get too noisy. +// Leveled console logger. `info` / `warn` / `error` always emit; `debug` +// is gated on the same `debugTraceStorage` toggle that controls the +// structured trace buffer (see `lib/debug-trace.ts`) so one switch +// silences both surfaces. Verbose `debug` calls in the ~10ms before the +// toggle's in-memory cache hydrates may be missed — that's acceptable, +// since `debug` is advisory. +// +// `createRuleLogger(ruleId)` returns the same shape but auto-prefixes +// `[abs:rule-id]` so devtools console filters can pick out one rule's +// output. +// +// For DOM-mutation events, use `traceMutation` (`lib/trace-mutation.ts`) +// instead — it captures before/after HTML lazily and routes through the +// debug-trace store for offline analysis. -const PREFIX = "[abs]"; +import { isDebugTraceEnabled } from "./debug-trace"; -export function log(message: string, details?: unknown): void { +const DEFAULT_PREFIX = "[abs]"; + +type Level = "debug" | "info" | "warn" | "error"; + +function emit( + level: Level, + prefix: string, + message: string, + details?: unknown, +): void { + if (level === "debug" && !isDebugTraceEnabled()) { + return; + } + const sink = + level === "warn" + ? console.warn + : level === "error" + ? console.error + : console.log; if (details === undefined) { - console.log(PREFIX, message); + sink(prefix, message); } else { - console.log(PREFIX, message, details); + sink(prefix, message, details); } } + +export interface Logger { + debug: (message: string, details?: unknown) => void; + info: (message: string, details?: unknown) => void; + warn: (message: string, details?: unknown) => void; + error: (message: string, details?: unknown) => void; +} + +function build(prefix: string): Logger { + return { + debug: (m, d) => { + emit("debug", prefix, m, d); + }, + info: (m, d) => { + emit("info", prefix, m, d); + }, + warn: (m, d) => { + emit("warn", prefix, m, d); + }, + error: (m, d) => { + emit("error", prefix, m, d); + }, + }; +} + +export const log: Logger = build(DEFAULT_PREFIX); + +export function createRuleLogger(ruleId: string): Logger { + return build(`[abs:${ruleId}]`); +} diff --git a/extension/src/lib/options-badge.ts b/extension/src/lib/options-badge.ts index 5be3b1f..80d1b40 100644 --- a/extension/src/lib/options-badge.ts +++ b/extension/src/lib/options-badge.ts @@ -1,6 +1,7 @@ // Copyright (c) 2026 PixieBrix, Inc. // Licensed under PolyForm Shield 1.0.0 — see LICENSE. +import { log } from "./log"; import { optionsButtonStorage } from "./options-button-toggle"; const BADGE_SELECTOR = "data-abs"; @@ -54,7 +55,7 @@ function injectBadge(): void { chrome.runtime .sendMessage({ type: "open-options" }) .catch((error: unknown) => { - console.error("[abs] failed to open options page", error); + log.error("failed to open options page", error); }); }); diff --git a/extension/src/lib/placeholder.ts b/extension/src/lib/placeholder.ts index 4e42b62..482632e 100644 --- a/extension/src/lib/placeholder.ts +++ b/extension/src/lib/placeholder.ts @@ -3,7 +3,7 @@ import { isDebugTraceEnabled, recordRuleApplication } from "./debug-trace"; import { REVEALED_ATTR, RULE_ATTR } from "./dom-markers"; -import { log } from "./log"; +import { createRuleLogger, log } from "./log"; import type { RuleId } from "./storage"; import { traceMutation } from "./trace-mutation"; @@ -32,10 +32,11 @@ function describeNode(node: Node): Record { function attachReveal(container: HTMLElement, original: Node): void { const ruleId = container.getAttribute(RULE_ATTR); + const ruleLog = ruleId ? createRuleLogger(ruleId) : log; let revealed = false; const reveal = (event: Event) => { const target = event.target as Element | null; - log("reveal click received", { + ruleLog.info("reveal click received", { ruleId, eventType: event.type, isTrusted: event.isTrusted, @@ -54,7 +55,7 @@ function attachReveal(container: HTMLElement, original: Node): void { (original as Element).setAttribute(REVEALED_ATTR, ruleId); } container.replaceWith(original); - log("reveal complete — original restored", { + ruleLog.info("reveal complete — original restored", { ruleId, restored: describeNode(original), }); @@ -167,7 +168,7 @@ export function replaceWithBlockPlaceholder( element.replaceWith(placeholder); }, ); - log("block placeholder created", { + createRuleLogger(ruleId).info("block placeholder created", { ruleId, label, hidden: describeNode(element), @@ -225,7 +226,7 @@ function createInlinePlaceholder( placeholder.setAttribute(RULE_ATTR, ruleId); placeholder.textContent = label; attachReveal(placeholder, document.createTextNode(originalText)); - log("inline placeholder created", { + createRuleLogger(ruleId).info("inline placeholder created", { ruleId, label, hiddenLength: originalText.length, @@ -439,7 +440,10 @@ export function revealAll(ruleId: RuleId): void { const placeholders = document.querySelectorAll( `[${RULE_ATTR}="${ruleId}"]`, ); - log("revealAll invoked", { ruleId, count: placeholders.length }); + createRuleLogger(ruleId).info("revealAll invoked", { + ruleId, + count: placeholders.length, + }); for (const placeholder of placeholders) { placeholder.dispatchEvent(new MouseEvent("click")); } diff --git a/extension/src/lib/route-change.ts b/extension/src/lib/route-change.ts index 67db33e..c3ec0f3 100644 --- a/extension/src/lib/route-change.ts +++ b/extension/src/lib/route-change.ts @@ -38,7 +38,7 @@ function emit(): void { if (url === lastUrl) { return; } - log("route change", { from: lastUrl, to: url }); + log.info("route change", { from: lastUrl, to: url }); lastUrl = url; for (const listener of listeners) { listener(); diff --git a/extension/src/lib/rule-engine.ts b/extension/src/lib/rule-engine.ts index 9b34eec..0ce4995 100644 --- a/extension/src/lib/rule-engine.ts +++ b/extension/src/lib/rule-engine.ts @@ -15,7 +15,7 @@ import { subscribeEnforcementEnabled, } from "./enforcement"; import { isTopFrame } from "./frame"; -import { log } from "./log"; +import { createRuleLogger, log } from "./log"; import { LABEL_CLASS, LABEL_ICON_CLASS, @@ -159,7 +159,7 @@ function applyEnabled( // TS lib types `document.body` as non-null; reality disagrees in iframes. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!document.body) { - log("rule engine skipping — no document.body", { + log.info("rule engine skipping — no document.body", { url: globalThis.location.href, }); return; @@ -167,7 +167,7 @@ function applyEnabled( const enabled = RULES.filter( (rule) => isApplicableHere(rule, topFrame, availability) && states[rule.id], ).map((r) => r.id); - log("initial rule application", { + log.info("initial rule application", { enabled, url: globalThis.location.href, topFrame, @@ -177,7 +177,7 @@ function applyEnabled( continue; } if (states[rule.id]) { - log("applying rule", { ruleId: rule.id }); + createRuleLogger(rule.id).info("applying rule"); rule.apply(document.body); } } @@ -226,11 +226,12 @@ function reconcile( if (isApplied === wasApplied) { continue; } + const ruleLog = createRuleLogger(rule.id); if (isApplied) { - log("rule enabled — applying", { ruleId: rule.id }); + ruleLog.info("rule enabled — applying"); rule.apply(document.body); } else { - log("rule disabled — revealing and tearing down", { ruleId: rule.id }); + ruleLog.info("rule disabled — revealing and tearing down"); revealAll(rule.id); rule.teardown?.(); } @@ -251,7 +252,7 @@ function mask(states: RuleStates, enforcementEnabled: boolean): RuleStates { export async function start(): Promise { const topFrame = isTopFrame(); - log("rule engine starting", { url: globalThis.location.href, topFrame }); + log.info("rule engine starting", { url: globalThis.location.href, topFrame }); injectStyles(); // Set the default synchronously so any placeholder created before storage // resolves gets the right CSS scoping. @@ -305,11 +306,11 @@ export async function start(): Promise { applyChange(next, enforcementCurrent, availabilityCurrent); }); subscribeEnforcementEnabled((enabled) => { - log("enforcement toggle changed", { enabled }); + log.info("enforcement toggle changed", { enabled }); applyChange(rawCurrent, enabled, availabilityCurrent); }); subscribeRuleAvailability((next) => { - log("rule availability changed", { + log.info("rule availability changed", { snapshot: Object.fromEntries( Object.entries(next).map(([id, snap]) => [id, snap.available]), ), diff --git a/extension/src/lib/shadow-root-probe-registration.ts b/extension/src/lib/shadow-root-probe-registration.ts index 8e6d902..e8918d6 100644 --- a/extension/src/lib/shadow-root-probe-registration.ts +++ b/extension/src/lib/shadow-root-probe-registration.ts @@ -55,7 +55,7 @@ async function isRegistered(): Promise { // getRegisteredContentScripts throws if no script with the id exists // in some Chrome versions; treat that as "not registered" rather than // a failure mode that prevents registration. - log("shadow-root-probe registration: getRegistered threw", { error }); + log.warn("shadow-root-probe registration: getRegistered threw", { error }); return false; } } @@ -77,20 +77,20 @@ async function register(): Promise { persistAcrossSessions: true, }, ]); - log("shadow-root-probe registered at document_start (main world)"); + log.info("shadow-root-probe registered at document_start (main world)"); } catch (error) { - log("shadow-root-probe registration failed", { error }); + log.error("shadow-root-probe registration failed", { error }); } } async function unregister(): Promise { try { await chrome.scripting.unregisterContentScripts({ ids: [SCRIPT_ID] }); - log("shadow-root-probe unregistered"); + log.info("shadow-root-probe unregistered"); } catch (error) { // Unregister fails if the script wasn't registered to begin with; // that's a benign state, not a problem. - log("shadow-root-probe unregister no-op", { error }); + log.debug("shadow-root-probe unregister no-op", { error }); } } diff --git a/extension/src/lib/storage.ts b/extension/src/lib/storage.ts index ee613cc..e9c7b7f 100644 --- a/extension/src/lib/storage.ts +++ b/extension/src/lib/storage.ts @@ -5,17 +5,17 @@ // JS has no dependency on the rule catalog — the rules index pulls in every // rule implementation file, some of which touch DOM constructors at top // level, which would crash the service-worker bundle. Runtime values -// (RULE_IDS, RULE_DEFAULTS) come from the generated defaults module, which -// is pure data. The post-build purity check -// (`scripts/check-background-purity.ts`) guards against regressions. +// (RULE_IDS, RULE_DEFAULTS) come from `rules/rule-metadata`, which is pure +// data. The post-build purity check (`scripts/check-background-purity.ts`) +// guards against regressions. import type { RuleId } from "../rules"; -import { RULE_DEFAULTS, RULE_IDS } from "../rules/rule-defaults.generated"; +import { RULE_DEFAULTS, RULE_IDS } from "../rules/rule-metadata"; import { createChromeStorageValue } from "./chrome-storage-value"; export type RuleStates = Record; -// Defaults come from extension/data/rule-defaults.json via codegen — one -// scannable file lists every rule's initial state. Availability is resolved +// Defaults come from `rules/rule-metadata.ts` — one scannable file lists +// every rule's initial state. Availability is resolved // separately via `lib/availability.ts` (which supports reactive accessors), // and the rule engine gates application on it at apply time. We deliberately // don't mask unavailable rules' stored state here: if availability is @@ -96,4 +96,4 @@ export async function setAllRuleStates( } export type { RuleId } from "../rules"; -export { RULE_IDS } from "../rules/rule-defaults.generated"; +export { RULE_IDS } from "../rules/rule-metadata"; diff --git a/extension/src/lib/webdriver-probe-registration.ts b/extension/src/lib/webdriver-probe-registration.ts index 438fbaf..01a9618 100644 --- a/extension/src/lib/webdriver-probe-registration.ts +++ b/extension/src/lib/webdriver-probe-registration.ts @@ -48,7 +48,7 @@ async function isRegistered(): Promise { // getRegisteredContentScripts throws if no script with the id exists // in some Chrome versions; treat that as "not registered" rather than // a failure mode that prevents registration. - log("webdriver-probe registration: getRegistered threw", { error }); + log.warn("webdriver-probe registration: getRegistered threw", { error }); return false; } } @@ -70,20 +70,20 @@ async function register(): Promise { persistAcrossSessions: true, }, ]); - log("webdriver-probe registered at document_start (main world)"); + log.info("webdriver-probe registered at document_start (main world)"); } catch (error) { - log("webdriver-probe registration failed", { error }); + log.error("webdriver-probe registration failed", { error }); } } async function unregister(): Promise { try { await chrome.scripting.unregisterContentScripts({ ids: [SCRIPT_ID] }); - log("webdriver-probe unregistered"); + log.info("webdriver-probe unregistered"); } catch (error) { // Unregister fails if the script wasn't registered to begin with; // that's a benign state, not a problem. - log("webdriver-probe unregister no-op", { error }); + log.debug("webdriver-probe unregister no-op", { error }); } } diff --git a/extension/src/options/__tests__/parse-config.test.ts b/extension/src/options/__tests__/parse-config.test.ts index eadb96f..e9e8d24 100644 --- a/extension/src/options/__tests__/parse-config.test.ts +++ b/extension/src/options/__tests__/parse-config.test.ts @@ -2,11 +2,11 @@ // Licensed under PolyForm Shield 1.0.0 — see LICENSE. // parse-config.ts imports `RULE_IDS` from `lib/storage`, which re-exports it -// from `rules/rule-defaults.generated`. Mock the generated module so the test -// runs against a small id set (and so storage.ts's transitive -// `rules/index.ts` import doesn't pull in every rule file — some of which -// reach for ESM-only deps that ts-jest's CJS transform can't handle). -jest.mock("../../rules/rule-defaults.generated", () => ({ +// from `rules/rule-metadata`. Mock the metadata module so the test runs +// against a small id set (and so storage.ts's transitive `rules/index.ts` +// import doesn't pull in every rule file — some of which reach for ESM-only +// deps that ts-jest's CJS transform can't handle). +jest.mock("../../rules/rule-metadata", () => ({ RULE_DEFAULTS: { "rule-a": true, "rule-b": false }, RULE_IDS: ["rule-a", "rule-b"], })); diff --git a/extension/src/popup/Popup.tsx b/extension/src/popup/Popup.tsx index 3910270..ff9232c 100644 --- a/extension/src/popup/Popup.tsx +++ b/extension/src/popup/Popup.tsx @@ -85,8 +85,8 @@ export function Popup() { Debug trace - Captures DOM snippets of removed content for debugging. Stored only - in this browser. + Captures DOM snippets of removed content and verbose console logs + for debugging. Stored only in this browser. diff --git a/extension/src/popup/rule-labels.ts b/extension/src/popup/rule-labels.ts index a2bfee8..2468713 100644 --- a/extension/src/popup/rule-labels.ts +++ b/extension/src/popup/rule-labels.ts @@ -11,7 +11,7 @@ // `rules/__tests__/catalog.test.ts`: adding a rule without adding its // label here fails the suite. -import type { RuleId } from "../rules/rule-defaults.generated"; +import type { RuleId } from "../rules/rule-metadata"; // Exported as `Record` (rather than the narrow literal- // keyed `as const` shape) so callers can look up via any `RuleId` value diff --git a/extension/src/rules/__tests__/catalog.test.ts b/extension/src/rules/__tests__/catalog.test.ts index e90545b..58ede1d 100644 --- a/extension/src/rules/__tests__/catalog.test.ts +++ b/extension/src/rules/__tests__/catalog.test.ts @@ -27,7 +27,7 @@ jest.mock("abort-utils", () => ({ import { RULE_GROUPS } from "../../lib/rule-groups"; import { RULE_LABELS } from "../../popup/rule-labels"; import { RULE_IDS, RULES } from ".."; -import { RULE_DEFAULTS } from "../rule-defaults.generated"; +import { RULE_DEFAULTS } from "../rule-metadata"; describe("rule catalog invariants", () => { it("ships at least one rule", () => { @@ -56,10 +56,9 @@ describe("rule catalog invariants", () => { expect(typeof rule.apply).toBe("function"); }); - // Defaults live in extension/data/rule-defaults.json and flow through - // codegen into RULE_DEFAULTS. Codegen rejects mismatches at build time; - // this test is a belt-and-suspenders so adding a rule without picking a - // default fails fast in `bun run test` too. + // Defaults live in `rules/rule-metadata.ts` and are hand-edited. This + // test catches the case where a rule is registered in `rules/index.ts` + // without a corresponding metadata entry (or vice versa). it("declares a default for every rule and no extras", () => { const defaultsKeys = Object.keys(RULE_DEFAULTS).toSorted(); expect(defaultsKeys).toEqual([...RULE_IDS].toSorted()); diff --git a/extension/src/rules/cart-addon-annotate.ts b/extension/src/rules/cart-addon-annotate.ts index 35c94a1..75048fd 100644 --- a/extension/src/rules/cart-addon-annotate.ts +++ b/extension/src/rules/cart-addon-annotate.ts @@ -32,12 +32,13 @@ import { RULE_ATTR, } from "../lib/dom-markers"; import { findInnermostMatches } from "../lib/dom-utils"; -import { log } from "../lib/log"; +import { createRuleLogger } from "../lib/log"; import { createSubtreeWatcher } from "../lib/subtree-watcher"; import { traceMutation } from "../lib/trace-mutation"; import type { Rule } from "./types"; const RULE_ID = "cart-addon-annotate" as const; +const log = createRuleLogger(RULE_ID); const FLAG_CLASS = "abs-cart-addon-annotate"; @@ -189,7 +190,7 @@ function scanAndFlag(root: ParentNode): void { for (const candidate of candidates) { flag(candidate); } - log("cart add-ons flagged", { + log.info("cart add-ons flagged", { count: candidates.length, labels: candidates.map((c) => c.label), url: globalThis.location.href, diff --git a/extension/src/rules/checkout-checkbox-sanitize.ts b/extension/src/rules/checkout-checkbox-sanitize.ts index d866488..9304e5f 100644 --- a/extension/src/rules/checkout-checkbox-sanitize.ts +++ b/extension/src/rules/checkout-checkbox-sanitize.ts @@ -26,12 +26,13 @@ import { isCheckoutUrl } from "../lib/checkout-url"; import { CHECKOUT_CHECKBOX_CLEARED_ATTR as CLEARED_ATTR } from "../lib/dom-markers"; -import { log } from "../lib/log"; +import { createRuleLogger } from "../lib/log"; import { createSubtreeWatcher } from "../lib/subtree-watcher"; import { traceMutation } from "../lib/trace-mutation"; import type { Rule } from "./types"; const RULE_ID = "checkout-checkbox-sanitize" as const; +const log = createRuleLogger(RULE_ID); const INJECT_DEFENSE_MESSAGE = { type: "inject-checkout-checkbox-defense", @@ -96,7 +97,7 @@ function scanAndClear(root: ParentNode): void { } if (cleared > 0) { - log("checkout checkboxes cleared", { + log.info("checkout checkboxes cleared", { count: cleared, url: globalThis.location.href, }); diff --git a/extension/src/rules/closed-shadow-root-annotate.ts b/extension/src/rules/closed-shadow-root-annotate.ts index ad4e16f..d5283a9 100644 --- a/extension/src/rules/closed-shadow-root-annotate.ts +++ b/extension/src/rules/closed-shadow-root-annotate.ts @@ -87,13 +87,14 @@ import type { RuleDetectionMessage } from "../lib/detection-messages"; import { RULE_ATTR } from "../lib/dom-markers"; -import { log } from "../lib/log"; +import { createRuleLogger } from "../lib/log"; import { SR_ONLY_INLINE_STYLE } from "../lib/sr-only"; import { createSubtreeWatcher } from "../lib/subtree-watcher"; import { traceMutation } from "../lib/trace-mutation"; import type { Rule } from "./types"; const RULE_ID = "closed-shadow-root-annotate" as const; +const log = createRuleLogger(RULE_ID); const LANDMARK_SELECTOR = `section[${RULE_ATTR}="${RULE_ID}"]`; @@ -192,7 +193,7 @@ function ensureLandmark(): void { document.body.prepend(buildLandmark()); }, ); - log("closed-shadow-root-annotate landmark added", { + log.info("closed-shadow-root-annotate landmark added", { host: globalThis.location.hostname, }); // Per-document dedupe: the landmark short-circuit above ensures we only diff --git a/extension/src/rules/confirmshame-sanitize.ts b/extension/src/rules/confirmshame-sanitize.ts index 5aac753..6a87f67 100644 --- a/extension/src/rules/confirmshame-sanitize.ts +++ b/extension/src/rules/confirmshame-sanitize.ts @@ -37,12 +37,13 @@ import { CONFIRMSHAME_ORIGINAL_TITLE_ATTR as ORIGINAL_TITLE_ATTR, CONFIRMSHAME_ORIGINAL_VALUE_ATTR as ORIGINAL_VALUE_ATTR, } from "../lib/dom-markers"; -import { log } from "../lib/log"; +import { createRuleLogger } from "../lib/log"; import { createSubtreeWatcher } from "../lib/subtree-watcher"; import { traceMutation } from "../lib/trace-mutation"; import type { Rule } from "./types"; const RULE_ID = "confirmshame-sanitize" as const; +const log = createRuleLogger(RULE_ID); // Decline label that replaces confirmshame copy. "No thanks" reads as a // plain refusal in every context this rule fires in (modals, signup forms, @@ -267,7 +268,7 @@ function scanAndNeutralize(root: ParentNode): void { } } if (count > 0) { - log("confirmshame buttons neutralized", { count }); + log.info("confirmshame buttons neutralized", { count }); } } diff --git a/extension/src/rules/disguised-ad-flag.ts b/extension/src/rules/disguised-ad-flag.ts index 2755b1e..699f61e 100644 --- a/extension/src/rules/disguised-ad-flag.ts +++ b/extension/src/rules/disguised-ad-flag.ts @@ -24,12 +24,13 @@ import { findInnermostMatches, isInsidePlaceholder, } from "../lib/dom-utils"; -import { log } from "../lib/log"; +import { createRuleLogger } from "../lib/log"; import { replaceWithBlockPlaceholder } from "../lib/placeholder"; import { createSubtreeWatcher } from "../lib/subtree-watcher"; import type { Rule } from "./types"; const RULE_ID = "disguised-ad-flag" as const; +const log = createRuleLogger(RULE_ID); // Cap on the trimmed text length of the *label* element. Real disclosure // labels are short — "Sponsored", "Paid for and presented by Acme", a @@ -398,7 +399,7 @@ function scanAndHide(root: ParentNode): void { for (const candidate of candidates) { hideCandidate(candidate); } - log("disguised ad placeholders inserted", { + log.info("disguised ad placeholders inserted", { count: candidates.length, phrases: candidates.map((c) => c.phrase), }); diff --git a/extension/src/rules/form-prefill-annotate.ts b/extension/src/rules/form-prefill-annotate.ts index 0b28df5..bcd302c 100644 --- a/extension/src/rules/form-prefill-annotate.ts +++ b/extension/src/rules/form-prefill-annotate.ts @@ -50,12 +50,13 @@ import { FORM_PREFILL_ANNOTATED_ATTR as FLAGGED_ATTR, RULE_ATTR, } from "../lib/dom-markers"; -import { log } from "../lib/log"; +import { createRuleLogger } from "../lib/log"; import { createSubtreeWatcher } from "../lib/subtree-watcher"; import { traceMutation } from "../lib/trace-mutation"; import type { Rule } from "./types"; const RULE_ID = "form-prefill-annotate" as const; +const log = createRuleLogger(RULE_ID); const FLAG_CLASS = "abs-form-prefill-annotate"; @@ -608,7 +609,7 @@ function scanAndFlag(root: ParentNode): void { flag(candidate); count++; } - log("form prefills flagged", { + log.info("form prefills flagged", { count, kinds: candidates.map((c) => c.kind), url: globalThis.location.href, diff --git a/extension/src/rules/hidden-affiliate-sanitize.ts b/extension/src/rules/hidden-affiliate-sanitize.ts index 319338b..282555e 100644 --- a/extension/src/rules/hidden-affiliate-sanitize.ts +++ b/extension/src/rules/hidden-affiliate-sanitize.ts @@ -46,12 +46,13 @@ import { isCheckoutUrl } from "../lib/checkout-url"; import { HIDDEN_AFFILIATE_CLEARED_ATTR as CLEARED_ATTR } from "../lib/dom-markers"; -import { log } from "../lib/log"; +import { createRuleLogger } from "../lib/log"; import { createSubtreeWatcher } from "../lib/subtree-watcher"; import { traceMutation } from "../lib/trace-mutation"; import type { Rule } from "./types"; const RULE_ID = "hidden-affiliate-sanitize" as const; +const log = createRuleLogger(RULE_ID); // Curated attribution name patterns. Whole-name regex match against // `input.name` (case-insensitive). Each entry pulls double duty in the @@ -315,7 +316,7 @@ function scanAndClear(root: ParentNode): void { tryClearInput(input, outcome); } if (outcome.cleared > 0) { - log("hidden affiliate values cleared", { + log.info("hidden affiliate values cleared", { count: outcome.cleared, names: outcome.names, url: globalThis.location.href, diff --git a/extension/src/rules/hidden-fee-annotate.ts b/extension/src/rules/hidden-fee-annotate.ts index d49ad94..c24fbba 100644 --- a/extension/src/rules/hidden-fee-annotate.ts +++ b/extension/src/rules/hidden-fee-annotate.ts @@ -42,12 +42,13 @@ import { RULE_ATTR, } from "../lib/dom-markers"; import { findInnermostMatches } from "../lib/dom-utils"; -import { log } from "../lib/log"; +import { createRuleLogger } from "../lib/log"; import { createSubtreeWatcher } from "../lib/subtree-watcher"; import { traceMutation } from "../lib/trace-mutation"; import type { Rule } from "./types"; const RULE_ID = "hidden-fee-annotate" as const; +const log = createRuleLogger(RULE_ID); const FLAG_CLASS = "abs-hidden-fee-annotate"; @@ -521,7 +522,7 @@ function scanAndFlag(root: ParentNode): void { for (const candidate of candidates) { flag(candidate); } - log("hidden fees flagged", { + log.info("hidden fees flagged", { count: candidates.length, phrases: candidates.map((c) => c.phrase), url: globalThis.location.href, diff --git a/extension/src/rules/hidden-text-strip.ts b/extension/src/rules/hidden-text-strip.ts index 64e2536..8b249aa 100644 --- a/extension/src/rules/hidden-text-strip.ts +++ b/extension/src/rules/hidden-text-strip.ts @@ -71,13 +71,14 @@ import { NON_CONTENT_TAGS, visibleTextContent, } from "../lib/dom-utils"; -import { log } from "../lib/log"; +import { createRuleLogger } from "../lib/log"; import { SR_ONLY_CLASS_NAMES, SR_ONLY_MAX_SIZE_PX } from "../lib/sr-only"; import { createSubtreeWatcher } from "../lib/subtree-watcher"; import { traceMutation } from "../lib/trace-mutation"; import type { Rule } from "./types"; const RULE_ID = "hidden-text-strip" as const; +const log = createRuleLogger(RULE_ID); const OFFSCREEN_THRESHOLD_PX = -9999; @@ -892,7 +893,7 @@ function scanAndStrip(root: ParentNode): void { continue; } const visible = visibleTextContent(element); - log("hidden text scrubbed", { + log.info("hidden text scrubbed", { ruleId: RULE_ID, reason, details, diff --git a/extension/src/rules/index.ts b/extension/src/rules/index.ts index 616440a..cd1656b 100644 --- a/extension/src/rules/index.ts +++ b/extension/src/rules/index.ts @@ -43,11 +43,11 @@ import { unicodeInvisiblesStripRule } from "./unicode-invisibles-strip"; import { webdriverProbeAnnotateRule } from "./webdriver-probe-annotate"; // Catalog of every rule's runtime (apply/teardown). Adding a rule: create the -// file, add the import above, append below, then update -// `data/rule-defaults.json` and rerun `bun run build-rule-defaults`. +// file, add the import above, append below, then add an entry to +// `rule-metadata.ts`. // // `RuleId` and `RULE_IDS` are the canonical id set, and they live in -// `rule-defaults.generated.ts` so service-worker consumers (`lib/storage.ts`, +// `rule-metadata.ts` so service-worker consumers (`lib/storage.ts`, // `background.ts`) can import them without pulling any rule file's top-level // DOM access into the worker bundle. The catalog invariants test verifies // `RULES.map(r => r.id)` matches `RULE_IDS` exactly. diff --git a/extension/src/rules/irrelevant-sections-redact.ts b/extension/src/rules/irrelevant-sections-redact.ts index 387b3f3..d40ad74 100644 --- a/extension/src/rules/irrelevant-sections-redact.ts +++ b/extension/src/rules/irrelevant-sections-redact.ts @@ -21,13 +21,14 @@ import { import { createApiKeyAvailability } from "../lib/availability"; import { isInsidePlaceholder } from "../lib/dom-utils"; import { classifyIrrelevantSections } from "../lib/llm-client"; -import { log } from "../lib/log"; +import { createRuleLogger } from "../lib/log"; import { getPageTree } from "../lib/page-tree"; import { replaceWithBlockPlaceholder } from "../lib/placeholder"; import { waitForSettle } from "../lib/wait-for-settle"; import type { Rule } from "./types"; const RULE_ID = "irrelevant-sections-redact"; +const log = createRuleLogger(RULE_ID); const SCROLL_DEBOUNCE_MS = 600; const SETTLE_TIMEOUT_MS = 3000; const SETTLE_QUIET_MS = 500; @@ -148,14 +149,14 @@ function runClassification(): void { pruneReferences(); const pageTree = serializePageTree(); if (!pageTree) { - log("irrelevant-sections-redact skipped — empty page tree"); + log.debug("irrelevant-sections-redact skipped — empty page tree"); return; } const signal = lifecycleController.signal; classifyInFlight = true; - log("irrelevant-sections-redact classify start", { + log.debug("irrelevant-sections-redact classify start", { url: location.href, pageTreeBytes: pageTree.length, }); @@ -166,7 +167,7 @@ function runClassification(): void { return; } - log("irrelevant-sections-redact classify response", { + log.debug("irrelevant-sections-redact classify response", { count: response.irrelevant.length, entries: response.irrelevant, }); @@ -179,7 +180,7 @@ function runClassification(): void { }> = []; for (const entry of response.irrelevant) { if (seenRefs.has(entry.ref)) { - log("irrelevant-sections-redact ref skipped — duplicate", { + log.debug("irrelevant-sections-redact ref skipped — duplicate", { ref: entry.ref, }); continue; @@ -187,7 +188,7 @@ function runClassification(): void { seenRefs.add(entry.ref); const element = resolveReference(entry.ref); if (!element) { - log("irrelevant-sections-redact ref skipped — unresolved", { + log.debug("irrelevant-sections-redact ref skipped — unresolved", { ref: entry.ref, summary: entry.summary, }); @@ -195,7 +196,7 @@ function runClassification(): void { } const skipReason = checkHideable(element); if (skipReason !== null) { - log("irrelevant-sections-redact ref skipped — not hideable", { + log.debug("irrelevant-sections-redact ref skipped — not hideable", { ref: entry.ref, summary: entry.summary, reason: skipReason, @@ -203,7 +204,7 @@ function runClassification(): void { }); continue; } - log("irrelevant-sections-redact ref accepted as candidate", { + log.debug("irrelevant-sections-redact ref accepted as candidate", { ref: entry.ref, summary: entry.summary, element: describeElement(element), @@ -220,20 +221,26 @@ function runClassification(): void { (candidate) => !outermost.includes(candidate), ); for (const dropped of droppedByDedupe) { - log("irrelevant-sections-redact ref dropped by dedupe — has ancestor", { - ref: dropped.ref, - summary: dropped.summary, - element: describeElement(dropped.element), - }); + log.debug( + "irrelevant-sections-redact ref dropped by dedupe — has ancestor", + { + ref: dropped.ref, + summary: dropped.summary, + element: describeElement(dropped.element), + }, + ); } let hiddenCount = 0; for (const { element, summary, ref } of outermost) { if (!element.isConnected) { - log("irrelevant-sections-redact ref skipped at hide — detached", { - ref, - summary, - }); + log.debug( + "irrelevant-sections-redact ref skipped at hide — detached", + { + ref, + summary, + }, + ); continue; } const description = describeElement(element); @@ -250,14 +257,14 @@ function runClassification(): void { placeholder.style.maxHeight = `${MAX_PLACEHOLDER_HEIGHT_PX}px`; placeholder.style.overflow = "hidden"; hiddenCount++; - log("irrelevant-sections-redact ref hidden", { + log.debug("irrelevant-sections-redact ref hidden", { ref, summary, element: description, }); } - log("irrelevant-sections-redact classify done", { + log.info("irrelevant-sections-redact classify done", { returned: response.irrelevant.length, candidates: candidates.length, deduped: outermost.length, @@ -269,7 +276,9 @@ function runClassification(): void { return; } const message = error instanceof Error ? error.message : String(error); - log("irrelevant-sections-redact classify failed", { error: message }); + log.error("irrelevant-sections-redact classify failed", { + error: message, + }); }) .finally(() => { classifyInFlight = false; @@ -301,7 +310,7 @@ function stopScrollWatcher(): void { } function apply(_root: ParentNode): void { - log("irrelevant-sections-redact apply", { url: location.href }); + log.info("irrelevant-sections-redact apply", { url: location.href }); const signal = lifecycleController.signal; void waitForSettle({ @@ -310,10 +319,10 @@ function apply(_root: ParentNode): void { signal, }).then(() => { if (signal.aborted) { - log("irrelevant-sections-redact aborted before classify"); + log.debug("irrelevant-sections-redact aborted before classify"); return; } - log("irrelevant-sections-redact page settled — classifying"); + log.info("irrelevant-sections-redact page settled — classifying"); runClassification(); startScrollWatcher(); }); diff --git a/extension/src/rules/link-spoof-annotate.ts b/extension/src/rules/link-spoof-annotate.ts index 28cc8ef..ebfdaae 100644 --- a/extension/src/rules/link-spoof-annotate.ts +++ b/extension/src/rules/link-spoof-annotate.ts @@ -45,12 +45,13 @@ import { RULE_ATTR, } from "../lib/dom-markers"; import { registrableDomain } from "../lib/domain-trust"; -import { log } from "../lib/log"; +import { createRuleLogger } from "../lib/log"; import { createSubtreeWatcher } from "../lib/subtree-watcher"; import { traceMutation } from "../lib/trace-mutation"; import type { Rule } from "./types"; const RULE_ID = "link-spoof-annotate" as const; +const log = createRuleLogger(RULE_ID); const FLAG_CLASS = "abs-link-spoof-annotate"; // Adjacent letters from different scripts inside a single word. Covers @@ -230,7 +231,7 @@ function scanAndFlag(root: ParentNode): void { } } if (count > 0) { - log("link spoofs flagged", { count }); + log.info("link spoofs flagged", { count }); } } diff --git a/extension/src/rules/roach-motel-annotate.ts b/extension/src/rules/roach-motel-annotate.ts index f4dbd05..48accfc 100644 --- a/extension/src/rules/roach-motel-annotate.ts +++ b/extension/src/rules/roach-motel-annotate.ts @@ -29,7 +29,7 @@ import { URLPattern } from "urlpattern-polyfill"; import type { RuleDetectionMessage } from "../lib/detection-messages"; import { RULE_ATTR } from "../lib/dom-markers"; -import { log } from "../lib/log"; +import { createRuleLogger } from "../lib/log"; import { SR_ONLY_INLINE_STYLE } from "../lib/sr-only"; import { traceMutation } from "../lib/trace-mutation"; import type { JustDeleteMeEntry } from "./justdeleteme.generated"; @@ -39,6 +39,7 @@ import { ROACH_MOTEL_WARNINGS } from "./site-data.generated"; import type { Rule } from "./types"; const RULE_ID = "roach-motel-annotate" as const; +const log = createRuleLogger(RULE_ID); const LANDMARK_SELECTOR = `section[${RULE_ATTR}="${RULE_ID}"]`; @@ -208,7 +209,7 @@ function apply(_root: ParentNode): void { ); }, ); - log("roach-motel-annotate applied", { + log.info("roach-motel-annotate applied", { host: globalThis.location.hostname, difficulty: warning.difficulty, }); diff --git a/extension/src/rules/rule-defaults.generated.ts b/extension/src/rules/rule-metadata.ts similarity index 70% rename from extension/src/rules/rule-defaults.generated.ts rename to extension/src/rules/rule-metadata.ts index f9be2cf..f3b7205 100644 --- a/extension/src/rules/rule-defaults.generated.ts +++ b/extension/src/rules/rule-metadata.ts @@ -1,10 +1,15 @@ -// AUTO-GENERATED — do not edit by hand. -// Source: extension/data/rule-defaults.json -// Regenerate with `bun run build-rule-defaults`. +// Copyright (c) 2026 PixieBrix, Inc. +// Licensed under PolyForm Shield 1.0.0 — see LICENSE. -// Source of truth for `RuleId` and `RULE_IDS`. Lives outside `rules/index.ts` -// so service-worker code (`lib/storage.ts`, `background.ts`) can import the -// id set without pulling in any rule file's top-level DOM access. +// Source of truth for `RuleId`, `RULE_IDS`, and ship-default state. +// Hand-edited. Adding a rule: append an entry here AND register the +// runtime in `rules/index.ts`. The catalog test in +// `rules/__tests__/catalog.test.ts` enforces that the two stay in sync. +// +// Kept out of `rules/index.ts` so service-worker code (`lib/storage.ts`, +// `background.ts`) can import id metadata without pulling rule files' +// DOM access into the worker bundle (guarded by +// `scripts/check-background-purity.ts`). export const RULE_DEFAULTS = { "pii-redact": true, diff --git a/extension/src/rules/search-url-helper.ts b/extension/src/rules/search-url-helper.ts index 2e0be24..ca68cf1 100644 --- a/extension/src/rules/search-url-helper.ts +++ b/extension/src/rules/search-url-helper.ts @@ -16,13 +16,14 @@ // by `bun run build-site-data`. import { RULE_ATTR } from "../lib/dom-markers"; -import { log } from "../lib/log"; +import { createRuleLogger } from "../lib/log"; import { SR_ONLY_INLINE_STYLE } from "../lib/sr-only"; import { traceMutation } from "../lib/trace-mutation"; import { SEARCH_URL_HELPER_RECIPES } from "./site-data.generated"; import type { Rule } from "./types"; const RULE_ID = "search-url-helper" as const; +const log = createRuleLogger(RULE_ID); const LANDMARK_SELECTOR = `section[${RULE_ATTR}="${RULE_ID}"]`; @@ -73,7 +74,7 @@ function apply(_root: ParentNode): void { document.body.prepend(buildLandmark(recipe)); }, ); - log("search-url-helper applied", { + log.info("search-url-helper applied", { host: globalThis.location.hostname, recipeLength: recipe.length, }); diff --git a/extension/src/rules/trust-badge-annotate.ts b/extension/src/rules/trust-badge-annotate.ts index 3684a2e..5e149bc 100644 --- a/extension/src/rules/trust-badge-annotate.ts +++ b/extension/src/rules/trust-badge-annotate.ts @@ -29,12 +29,13 @@ import { RULE_ATTR, } from "../lib/dom-markers"; import { registrableDomain } from "../lib/domain-trust"; -import { log } from "../lib/log"; +import { createRuleLogger } from "../lib/log"; import { createSubtreeWatcher } from "../lib/subtree-watcher"; import { traceMutation } from "../lib/trace-mutation"; import type { Rule } from "./types"; const RULE_ID = "trust-badge-annotate" as const; +const log = createRuleLogger(RULE_ID); const CHIP_CLASS = "abs-trust-badge-annotate"; // Hard cap on the accessible-name length we'll consider. Real badges carry @@ -293,7 +294,7 @@ function scanAndAnnotate(root: ParentNode): void { } } if (count > 0) { - log("trust badges annotated", { count }); + log.info("trust badges annotated", { count }); } } diff --git a/extension/src/rules/webdriver-probe-annotate.ts b/extension/src/rules/webdriver-probe-annotate.ts index b50543c..08750e1 100644 --- a/extension/src/rules/webdriver-probe-annotate.ts +++ b/extension/src/rules/webdriver-probe-annotate.ts @@ -45,12 +45,13 @@ import type { RuleDetectionMessage } from "../lib/detection-messages"; import { RULE_ATTR } from "../lib/dom-markers"; -import { log } from "../lib/log"; +import { createRuleLogger } from "../lib/log"; import { SR_ONLY_INLINE_STYLE } from "../lib/sr-only"; import { traceMutation } from "../lib/trace-mutation"; import type { Rule } from "./types"; const RULE_ID = "webdriver-probe-annotate" as const; +const log = createRuleLogger(RULE_ID); const EVENT_NAME = "abs:webdriver-probed"; @@ -95,7 +96,7 @@ function ensureLandmark(): void { document.body.prepend(buildLandmark()); }, ); - log("webdriver-probe-annotate landmark added", { + log.info("webdriver-probe-annotate landmark added", { host: globalThis.location.hostname, }); // Notify the background so the popup can show a human-visible entry. diff --git a/scripts/compare_scenarios.py b/scripts/compare_scenarios.py index aef3366..9baced9 100755 --- a/scripts/compare_scenarios.py +++ b/scripts/compare_scenarios.py @@ -205,7 +205,7 @@ def mint_run_id() -> str: def rebuild_extension() -> None: # Codegen + bundle + zip together take <2s on a clean tree; cheap enough # to do every run so source edits in rules/, data/sites/, or - # data/rule-defaults.json can't silently miss this comparison the way + # src/rules/rule-metadata.ts can't silently miss this comparison the way # they would with a stale zip on disk. ext_dir = REPO_ROOT / "extension" print("rebuilding extension (bun run build && bun run package)...") diff --git a/skills/agent-browser-shield-autoresearch/SKILL.md b/skills/agent-browser-shield-autoresearch/SKILL.md index 194347e..2da62d5 100644 --- a/skills/agent-browser-shield-autoresearch/SKILL.md +++ b/skills/agent-browser-shield-autoresearch/SKILL.md @@ -32,8 +32,8 @@ If the user already has a run in hand and just wants the *why*, hand off to - `output/agent-browser-shield-extension.zip` is auto-rebuilt by `compare_scenarios.py` before each run (codegen + bundle + zip, \<2s on a clean tree), so source edits in `extension/src/rules/`, - `extension/data/sites/`, or `extension/data/rule-defaults.json` take effect on - the next comparison. Pass `--no-rebuild-extension` to opt out (useful when + `extension/data/sites/`, or `extension/src/rules/rule-metadata.ts` take effect + on the next comparison. Pass `--no-rebuild-extension` to opt out (useful when `--extension-zip` is pinned to a release artifact). - `OPENAI_API_KEY` is in `.env` (the judge always calls OpenAI directly, regardless of the agent model). @@ -174,7 +174,7 @@ artificially attractive? Map what you observed to a concrete change. Pick one of: -- **Tighten a built-in rule default** — `extension/data/rule-defaults.json`. +- **Tighten a built-in rule default** — `extension/src/rules/rule-metadata.ts`. Disable a rule that's hurting more than it helps, or enable one that's off-by-default but would help. - **Add a site-specific rule** — `extension/data/sites/.yaml`. Hand off to @@ -205,13 +205,11 @@ with no manual rebuild step. Pass `--no-rebuild-extension` only when ### Where each change lives -- **Default rule toggles** — `extension/data/rule-defaults.json`. Flat map of - `` to boolean. The codegen - (`extension/scripts/build-rule-defaults.ts`) validates that every registered - id has a default and rejects unknown ids; do not edit the generated - `extension/src/rules/rule-defaults.generated.ts`. To flip - `irrelevant-sections-redact` on for one experiment, change `false` to `true` - here and rebuild. +- **Default rule toggles** — `extension/src/rules/rule-metadata.ts`. Hand- + edited flat map of `` to boolean. The catalog test in + `extension/src/rules/__tests__/catalog.test.ts` enforces that every registered + id has a default and rejects unknown ids. To flip `irrelevant-sections-redact` + on for one experiment, change `false` to `true` here and rebuild. - **Site-specific rules** — `extension/data/sites/.yaml`. Codegen (`extension/scripts/build-site-data.ts`) validates each YAML against @@ -230,9 +228,9 @@ with no manual rebuild step. Pass `--no-rebuild-extension` only when is anchored. - **New defense rule** — create `extension/src/rules/.ts`, import + add - it to the tuple in `extension/src/rules/index.ts`, add a default to - `extension/data/rule-defaults.json`. See the `agent-browser-shield` skill for - the rule contract. + it to the tuple in `extension/src/rules/index.ts`, add an entry to + `extension/src/rules/rule-metadata.ts`. See the `agent-browser-shield` skill + for the rule contract. Rule IDs are `-`. Pick the verb by what the rule does to the DOM, not the threat: @@ -267,8 +265,8 @@ invokes all three codegens automatically. Before drawing conclusions from the re-run, confirm the change is actually in the bundle: -- For default toggles or new rules: grep the rebuilt - `extension/src/rules/rule-defaults.generated.ts` for the id. +- For default toggles or new rules: grep `extension/src/rules/rule-metadata.ts` + for the id. - For site rules: grep `extension/src/rules/site-data.generated.ts` for the host or selector. - For built-in rule code edits: `bun run test -- ` (or @@ -341,8 +339,8 @@ verify: > agent followed in rep 2. Footer stripping accounts for the other -1,400 bytes > per article-page tree. > -> **Proposal**: keep `search-url-helper` enabled (`rule-defaults.json` shows -> it's already true). Optionally add more Wikipedia URL templates to +> **Proposal**: keep `search-url-helper` enabled (`rule-metadata.ts` shows it's +> already true). Optionally add more Wikipedia URL templates to > `extension/data/sites/wikipedia.yaml` if other reps had been searching from > non-Main_Page entry points. Evidence: `output/results/cmp_/cost_diff.md`, > `output/results/cmp_/traces/.../steps.json` step 4. diff --git a/skills/agent-browser-shield-install/SKILL.md b/skills/agent-browser-shield-install/SKILL.md index 59ca6c3..ae0cdb2 100644 --- a/skills/agent-browser-shield-install/SKILL.md +++ b/skills/agent-browser-shield-install/SKILL.md @@ -179,7 +179,8 @@ JSON override file instead of using the hosted ZIP. 1. Write a JSON file mapping rule ids to booleans, plus any of the reserved non-rule keys below. Rules not listed keep their committed default from - `extension/data/rule-defaults.json`: + `extension/src/rules/rule-metadata.ts`. A starting template lives at + `extension/data/defaults-overrides.example.json`: ```json {