Skip to content

Commit 1fd3af5

Browse files
ion05claude
andauthored
Open-source readiness: review fixes, README, licensing and docs (#12)
* Fix two shipped examples that told people to type the wrong keyword WolframAlpha's example read `wa 42 miles in km`, but its only keyword is `wolfram` and `wa` belongs to WhatsApp. The browse list prints the example verbatim under each row, so the page was telling people to type a keyword that opens somebody else's site. WhatsApp had the mirror of the same slip, showing `whatsapp` when its keyword is `wa`. A test now collects every example whose first word is not one of that command's own keywords, so the next one fails the build. It is one test rather than one per command: a failure should name every row that drifted, not just the first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Rewrite the README as the front door of a public repository It opened with install instructions and buried what the thing is. A stranger now gets two sentences and an example first, then the not-affiliated-with-Meta line, then install, then the one design decision that explains the rest: the first word of a query is always a command when it matches a keyword, and the escape hatch that makes that liveable. Corrections found while checking the text against the code: the GitHub username setting is read only by `gh me`, not by `pr` and `iss`; the fallback engine has five presets, not two; the shipped table was missing Goodreads and the meta keywords; the first run has a Skip button the text did not mention. Adds CI and licence badges, the pinned Node and pnpm versions, and a marked placeholder for the three screenshots the repository still needs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Null-prototype every lookup table an argument can index A security pass found `gh facebook/react constructor` building `https://github.com/facebook/react/function Object() { [native code] }`. `GITHUB_TABS` was a plain object literal, so a lowercase Object.prototype key answered truthy and was interpolated into the path. Nothing escaped the origin and nothing was exploitable: the value always lands after the encoded repo path and carries no ? or #, so the worst case was a 404 from a query nobody types. But this is exactly the shape AGENTS.md invariant 17 exists for, in a module the invariant does not name, so all five tables that an argument or a user-edited URL can index are null-prototype now: the two GitHub ones, the AI aliases, the Drive app types and the meta route parameters. `normalizeTemplates` in storage.ts joins them. It was the one override map the parser built on a plain object, and a string assigned to `__proto__` there is swallowed by the inherited setter rather than stored, so a key could go missing without the parser saying so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Make the privacy policy exhaustive about where data is kept It enumerated two stores and the extension uses three. The options page also writes `bunnylol.collapsed` to its own `localStorage`, holding the list of folded shortcut groups. The substance of the policy was never in doubt, since that value is per-machine view state that never leaves the machine, but a document whose whole worth is that it is complete cannot omit a store a reader will find by grepping for the API. Also corrects the tab attribution. Opening a tab was credited to the popup alone; the omnibox and the install-time welcome tab do it too. All three use only create and update, so the load-bearing claim, that the `tabs` permission is not requested and no page can be read, is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Drop the internal handover doc and keep the facts worth keeping docs/handoff.md was written for the next agent working on the release, not for anybody who will read this repository. It cited eleven paths under a gitignored directory that no clone will have, described a branch and a pull request as in flight, listed manual browser checks as still owed, and said of itself that it was probably deleted before the merge. It was not. Publishing it advertises unfinished QA and explains nothing a contributor needs. The one durable section was its list of non-obvious facts that bit somebody during development. The seven that were not already recorded move into the AGENTS.md rules, which is where a future reader will look: the relative meta URLs, the preview substituting at the registry index, re-minted ids having to be rewritten in disabled and deleted, the vitest css flag that stops the token assertions passing vacuously, the flat-hex accent the icon generator parses, the harness class that must not reach the shipped sheet, and what hasOnboarded means on a profile that never answered the picker. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Correct the changelog where it contradicted itself and the code The 1.1.0 entry described the status pill twice and incompatibly: once under Changed as saying "Shortcuts active", once under Removed as having lost that state. The build produces neither string on a healthy profile, because the pill is silent. The two entries are now one that says what the release actually did. The Goodreads line claimed the same release both dropped and restored it, which is true of the development history and useless to a reader. Every link at the foot of the file pointed at a tag that does not exist, so all three 404 on the page a stranger scrolls to. The 1.0.0 link is gone with a note that the version was never published, and the 1.1.0 link now points at a release tag rather than a comparison. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Disclaim the Meta affiliation on the screen that claims it AGENTS.md requires the README and any listing copy to say the project is not affiliated with Meta. Both did. The extension itself did not, and the welcome screen opened with "A rebuild of an internal tool at Meta", which is the strongest affiliation claim anywhere in the project and the only one a user or a store reviewer meets at runtime. The sentence now says an independent rebuild of the command bar used inside Meta, and a quieter line under it disclaims affiliation, endorsement and sponsorship outright. The rule in AGENTS.md is widened to cover the extension's own UI, since that is the place it was just broken. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Finish the repository for a first-time visitor Six things a stranger or a store reviewer would hit. The bug report form required the rule-status text and gave an example string this build cannot produce. On the commonest bug, a healthy profile sending a shortcut to the wrong place, there is no pill at all, so the field could not be answered honestly. It is optional now and says that nothing shown is a fine answer. The shipped zip carried the font licence but not this project's own. LICENSE moves into public/ so the packer picks it up, which is what MIT asks for when the software is redistributed. store/listing.md holds the Web Store copy. The two paragraphs that are compliance text rather than marketing, the search-behaviour disclosure and the non-affiliation line, are quoted from the submission crib verbatim so neither can be softened while somebody is pasting fields into a dashboard under time pressure. The crib itself named the rule-status pill in its suggested screenshot set, which is now a shot of nothing, and told the reader to find a published URL for the privacy policy when the dashboard accepts the file's own GitHub URL. CONTRIBUTING gains what a contributor cannot infer: one maintainer, a week for a reply, what a major and a minor mean here given that the stored state format is the compatibility surface, and the release steps including building the zip fresh rather than trusting one left in release/. Dependabot watches the GitHub Actions pins only. Those are third-party code running against this repository and nobody notices when one goes stale. npm is left out on purpose: four devDependencies that move rarely are the policy, not an oversight. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fix three ways the Shortcuts page lost the user's place or its state A review of the Hidden shortcuts work found these, none of which any test could see: the repo has no DOM environment, so every view in it is untested by construction. Start over left the Hidden shortcuts group unfolded. The collapse store records ids whose fold differs from the default, and `expandAll` adds every default-folded id on purpose, because the Expand all button has to be able to open that group. `forgetCollapsed` is not that button, it is the reset that puts a profile back to how it was installed, so a user who declined two packs landed on the page of dead rows the folded default exists to prevent. `CollapseState` gains `reset`. Flipping any switch dropped keyboard focus to the body. `move` restored focus before `applyFilter` had decided visibility, so on the default path, with the hidden group folded, the row was inside a `display: none` subtree where focus is a silent no-op. `move` now returns the element and the caller focuses it after `applyFilter`, which is the order `turnOn` already used. Deleting a row had the same hole with nothing to catch it; focus goes to the filter box. The "omnibox only" badge went stale. It was computed once per render for rows that rendered enabled, so switching a row on never added one and a row kept its badge under Hidden shortcuts. `applyFilter` writes it now, alongside the counts it already owns, off a memoized keyword set. Also corrects two comments that claimed `turnOn` empties the list it walks. It does not: `move` reassigns the array rather than mutating it. A confidently wrong comment is what makes a reviewer skip the bug under it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Route the exempt-keyword field through the one validation boundary AGENTS.md invariant 6 says every alias check goes through validate.ts and that a new surface adds a call site rather than a local rule. The Exempt keywords field re-derived one of the three rules by hand, testing only for whitespace. So `\gh` and `=npm` were accepted and stored, where they can never match anything, because resolve strips that prefix before the key map is consulted. The user got a permanent chip that did nothing, and an arbitrarily long paste was stored too. Also from the same review: `commitSettings` swallowed its failures while its two siblings reject, and it did not roll back the state it had already applied optimistically, so a failed write left the page and every render after it showing a value that was not in storage. It now rejects like the others and restores the previous settings slice, not a whole snapshot, so a write that landed while this one was in flight is not undone by its failure. Dead code from the cards and the pill that went away: the `ok` status tone, which nothing can produce now that a healthy sync is silent, and `editedFields`, whose only consumer was its own test. `countShortcuts` moves from a view into lib/text.ts beside `countShipped`, where the helpers every surface shares live. `dispatchToast` keeps its name, since renaming the stored field would read as off for every profile written so far, and gains a comment saying so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Split storage.ts into the two families it always had 917 lines and 42 functions with no dividers, holding two complete parallel families plus the chrome I/O. They differ in the one way that matters, which is what they do with bad input, and nothing in the file said which one a reader was in. `storage/normalize.ts` is the lenient reader: any blob in, a usable state out, and it never throws. `storage/parse-import.ts` is the strict parser: it refuses a bad file with a message naming what is wrong. `storage/shared.ts` holds what both need. `storage.ts` keeps the I/O, the export and the public surface, so every importer is unchanged and the test file needed no edit at all, not even an import path. Each module docstring now opens with its own bad-input behaviour, since invariant 17 turns on the difference between them. Function bodies and their comments travelled verbatim. Verified by diffing the original against the four files: the only changed lines are the fourteen declarations that gained an export keyword. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Split dnr.ts along its three concerns 818 lines holding rule construction, the serialized sync state machine, and the RE2 fitting and sharding, with `buildRules` at the top and `fitPlan` five hundred lines below it. Nothing showed that those two are entry points into one set of constructors rather than two copies of them. `dnr/rules.ts` is now the only module that mints a rule, and it has exactly two consumers, both visible from its import graph: `buildRules` beside it, which only tests call, and `fitPlan` in `dnr/fit.ts`, which `syncRules` calls. `fit.ts` defines no rule of its own, so what a `buildRules` test omits is precisely the fitting step, which the docstrings now say outright. `dnr/keywords.ts` holds the ranking, the alternation order and the sharding. Every invariant comment travelled with its own code: the priority tiers, the RE2 pattern that has to swallow the whole URL remainder, the two orders one list rule, the fail-closed precondition, and the trailing slot the serialized rebuild consults before the in-flight one. Verified line by line against the original; the only additions are export keywords. The public surface is unchanged and no test needed an edit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Split the browse route so its one rule can be checked by grep 842 lines, of which `renderBrowse` was 531 holding eight mutually referencing closures over several mutable locals. The rule that `applyFilter` is the only writer of visibility, of every count and of the badge could only be verified by reading all of it, and two shipped bugs lived there for exactly that reason. `browse-row.ts` takes the row, which closed over nothing. `browse-groups.ts` takes the group headings, the runs and the refiling of a row between them. Neither writes anything that is on screen: the headings and the bulk buttons are built empty, and every function that changes what a group holds takes the repaint as a callback. So the rule is now a grep over two short modules. `browse.ts` is 500 lines and no longer imports the storage writers at all. `browse-groups.ts` is plain functions over their arguments rather than the factory the review suggested. A factory closed over the collapse state and a repaint callback would have to be constructed before `applyFilter` exists and then be read by it, which puts a mutable slot on the one seam this contract lives on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Give the browse view a DOM test suite, on jsdom Around 3,400 lines of view code were untested by construction: vitest runs under `environment: node` and there was no alternative, so two of the three bugs the last review found were in code no test could reach. jsdom joins the dev tooling, and exactly one suite opts into it with a `// @vitest-environment jsdom` docblock. The global default stays `node`, which is what keeps the rule that lib and model import cleanly without a DOM able to fail. The suite covers the Hidden shortcuts state machine, the newest code in the repo: a switch moves the row between groups and repaints both headings, a section survives losing its last live row, the group leaves the page when its last row is switched on, a bulk action is one write for the whole run, delete drops the row from every total, and the filter force-expands the folded group to reveal a hidden row. Two assertions were mutation-checked: moving the commit inside the bulk loop fails the one-write test, and a bare collapse read fails the force-expand test. The docstring is honest about the ceiling. jsdom does no layout, so `focus()` inside a hidden subtree succeeds there and fails in Chrome, which is precisely the bug this file cannot catch, and the CSS order reordering is invisible to it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Delete 39 test cases that were covering something else's ground The suite was 1401 cases from 776 written blocks. These four were duplicates, and each was checked against the code before it went. Three form-builder cases: the form's `buildCommand` is a four-line wrapper whose only addition is narrowing the category, and the three cases removed were asserting the wrapped builder's own behaviour, which its own test file already covers more strictly. Thirty-five self-interception cases: every sweep in that file ran twice, once over the test-only `buildRules` and once over the rules the sync path registers. Both sets were built over the shipped registry and compared rule by rule: 36 rules each, identical in priority, action and condition, differing only in id. The mirror was catching nothing. Invariant 1 is untouched, and is now derived only from the rules that actually ship, which is the stronger of the two. One manifest-floor case asserted from the token tests what the manifest test asserts more strictly. Its reasoning moved into the surviving comment rather than being lost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Add Prettier and ESLint, and put lint first in the gate CONTRIBUTING stated the style rules by hand and nothing checked them, so the first outside pull request was going to be a style negotiation in review comments. Prettier is configured from what the repo already does rather than from its defaults. Every setting was measured: print width 100 is the value that touches the fewest files and the fewest lines out of eleven candidates, and trailing commas and arrow parens were chosen the same way. Both stylesheets came through the first run unchanged, which is a fair signal the config matches the house style. design/ is excluded because it is review-gated and its hand-aligned contrast-ratio comments do not survive a formatter, go.html because its inline style block is deliberately minified on the one page whose job is to redirect before it paints, and Markdown because a formatter has nothing to offer prose that is already hand-wrapped. ESLint is flat config and type-aware, which costs about two seconds, so it runs first in CI as the fastest signal. It enforces the two rules a typechecker cannot see and CONTRIBUTING already asked for: no default exports, and `import type` for type-only imports. Every rule turned off is a convention rather than a dodge, and each carries its reason in the config. The largest is the unsafe-assignment rule, which fires on exactly the null-prototype construction that exists to satisfy invariant 17. The one real suggestion it made is fixed here: the import parser now attaches the underlying error as `cause` when it rethrows a JSON parse failure, so the stack survives. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Sweep the dead exports and the comments that describe old approaches Several passes of feature removal and three file splits landed in the last day, each cleaning up after itself and each missing something. This is the sweep, plus the repo-wide Prettier pass, which touched about four and a half percent of the lines and changed no behaviour. Fifteen exports dropped where nothing outside the file called them, so `noUnusedLocals` keeps them honest from now on. No code deleted and nothing renamed. Deliberate test-only seams were left alone, and so was every exported type that names a public signature. The comments were the important half. A confidently wrong comment is worse than none, and this codebase comments heavily. Five referred to "the monolith", a file layout that has not existed for a long time and that a stranger has no way to look up; each now states the constraint directly. Two pointed at a per-shortcut Restore that was removed. Two named `weather`, `gimg` and `gsite` as the commands at risk of self-interception, and none of those three still exists, so both were rederived by running the current registry through the current rules. Four pointed at modules the splits moved code out of. Two quoted keyword and shard counts that were off by a factor. Four entries in the token tests still described the dispatch toast and its dismiss button, and those strings are interpolated into live test names. No dead CSS was found. All 126 class selectors in the two sheets are still rendered, checked in both directions. One thing left as a finding rather than fixed: a sync-rules test named for the loop URL that used to bounce forever now drives `weather boston` through the fallback-engine path, because `weather` is not a command any more. It still passes and still covers something, but not what its name says. Pointing it at a command that does resolve onto an intercepted engine is a behaviour decision, not a comment fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Turn on noUncheckedIndexedAccess and answer what it asks The code indexes arrays constantly and guarded inconsistently: some sites checked the first keyword before reading it, others read a run or an escape plan by index with nothing checking the length. Every one was correct by construction, and nothing was verifying that. Nineteen sites under src/, and not one of them is a non-null assertion. This codebase has no `any`, no ignores and no `!` anywhere in src/, and that is worth more than the shortcut. The fixes are real: a length check written in the form the compiler reads, a regex capture tested instead of its match, a total API in place of an index, and two parallel arrays restructured so the pairing cannot come apart at all. Three sites were correct only because of a fact stated in another file, and each is now local and commented. The sharpest is the escape-rule precondition in the rule fitter: a missing escape rule would have thrown inside the fitter, escaped into the sync, and left the fail-closed branch tearing down the whole dynamic rule table. It now treats that engine as refused, which is what invariant 2 already prescribes. Five branches had to be chosen for cases that cannot arise today. Each takes what the surrounding code does for the nearest case that can: a keyless member drops out of a pack's sample rather than rendering undefined into it, a missing row removes the active-descendant attribute as the no-selection path does, and a character outside the escape map is kept rather than dropped. Under tests/, one helper that throws on a missing element, used sixty-eight times. It never substitutes a default, so a test still fails for the reason it was written to fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Cut the test suite to the 150 cases that would catch a broken build 1369 cases across 27 files, down to 150 across 20, running in half a second. The question asked of every survivor was whether a user would notice if it vanished and the code broke. What went. Every design and token assertion, so nothing now enforces the no-literal-hex and type-scale conventions but discipline. Every test that asserted a removed feature stayed removed, which tests history rather than behaviour. The view tests that were DOM assembly rather than decisions, including the jsdom suite added yesterday: the decisions under it are pure and still covered, and the parts that really break need a browser that jsdom cannot be. jsdom leaves package.json with it, since a dependency carrying no tests is worse than neither. The source-text tests, which asserted what the code looked like rather than what it did. And the sweeps: assertions run once per registry row are now single property tests that name every row that drifted. The seventeen invariants were the hard call, because AGENTS.md says their regression tests must never be deleted and that now conflicts with the instruction. Resolved by keeping, for each invariant whose failure a user would meet, the one smallest test that goes red when the bug comes back, with the comment saying which bug that is. Fifteen have one test. Invariant 16 keeps two, because the edit path and the storage boundary are different code and fixing one leaves the other red. Two have none, and AGENTS.md now says so rather than implying cover it never had. The surviving suite was checked by breaking the source three times: the self-interception marker, the pattern that has to swallow Chrome's appended parameters, and the edit that must copy named fields rather than spread. Three, three and five failures respectively. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: refresh README and add UI screenshots * Remove the Web Store submission material The crib and the store directory go, along with the listing copy and the padded listing icon. Four things referenced them and are updated rather than left dangling. The README logo now points at the toolbar icon, which is tracked and shipped, so the header still renders; it is the full-bleed art rather than the padded tile, which is the one visible difference. The install section no longer sends a reader to a file that is not there. The release steps in CONTRIBUTING end at uploading the zip. The architecture map drops both entries. The icon generator no longer emits the padded 128px tile, since nothing consumes it now, and the drift check in CI watches only public/icons. Re-running the generator leaves the tracked icons byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Drop the lead-in sentence above the README examples The three examples read on their own, and the arrows already say what the sentence was announcing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 70e10fa commit 1fd3af5

100 files changed

Lines changed: 4786 additions & 10250 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitattributes‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
* text=auto eol=lf
22
*.png binary
33
*.woff2 binary
4+
5+
# Exported artboards, not hand-written source. Marking them keeps GitHub's
6+
# language bar and its diffs about the code somebody actually maintains.
7+
design/canvas/*.dc.html linguist-generated=true

‎.github/ISSUE_TEMPLATE/bug_report.yml‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ body:
3535
attributes:
3636
label: Rule status
3737
description: >-
38-
The rule-status text at the top of the options page, copied verbatim
39-
(e.g. "Intercepting 148 keywords · 2 exempted by you")
38+
The rule-status text at the top of the options page, copied verbatim,
39+
if any is shown. A healthy profile shows nothing there, which is a
40+
fine answer.
4041
validations:
41-
required: true
42+
required: false
4243
- type: textarea
4344
id: console
4445
attributes:

‎.github/dependabot.yml‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# GitHub Actions only, deliberately.
2+
#
3+
# The npm ecosystem is left out because this project's dependency policy is the
4+
# point: four devDependencies, no runtime dependencies, and nothing from
5+
# node_modules reaches the shipped extension. A weekly stream of npm bumps would
6+
# be noise against a lockfile that is meant to move rarely and on purpose.
7+
#
8+
# The action pins are the opposite case. They are `@v4` major tags on somebody
9+
# else's repository, they are the only third-party code that runs with access to
10+
# this repository, and nobody notices when one goes stale.
11+
version: 2
12+
updates:
13+
- package-ecosystem: github-actions
14+
directory: /
15+
schedule:
16+
interval: weekly
17+
commit-message:
18+
prefix: 'ci:'

‎.github/workflows/ci.yml‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ permissions:
1010

1111
jobs:
1212
check:
13-
name: typecheck + test + build
13+
name: lint + typecheck + test + build
1414
runs-on: ubuntu-latest
1515
steps:
1616
- uses: actions/checkout@v4
@@ -25,14 +25,19 @@ jobs:
2525
cache: pnpm
2626

2727
- run: pnpm install --frozen-lockfile
28+
29+
# First, because it is the fastest signal: eslint and prettier --check
30+
# together run in a couple of seconds, well under the typecheck.
31+
- run: pnpm lint
32+
2833
- run: pnpm typecheck
2934
- run: pnpm test
3035
- run: pnpm build
3136

3237
# `pnpm build` runs scripts/gen-icons.mjs, so a change to the generator or
3338
# to --accent repaints these. Committed PNGs that the generator no longer
3439
# produces are a silent drift no other step can see.
35-
- run: git diff --exit-code -- public/icons store
40+
- run: git diff --exit-code -- public/icons
3641

3742
# The packer has no test, it writes a binary nothing else reads, so the
3843
# only cheap guard is that it still runs over a real build. `release/` is

‎.prettierignore‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Prettier already skips file types it has no parser for, so this lists only
2+
# what it WOULD format and should not.
3+
4+
# Build output and packaging artefacts. Nothing here is edited by hand.
5+
dist/
6+
release/
7+
8+
# Machine-written, and pnpm owns the formatting of its own lockfile.
9+
pnpm-lock.yaml
10+
11+
# The approved design bundle. AGENTS.md: change it through a design review, not
12+
# in passing. `design/canvas/*.dc.html` are exported artboards (.gitattributes
13+
# already marks them linguist-generated), and `design/tokens.css` is parsed as
14+
# text by scripts/gen-icons.mjs, which throws if the accent declarations move.
15+
# Its trailing contrast-ratio comments are aligned by hand and carry the audit.
16+
design/
17+
18+
# The dispatch page's inline stylesheet is deliberately minified: this page's
19+
# whole job is to redirect before it paints, so it fetches no font and loads no
20+
# sheet, and the values are copied by hand from design/tokens.css rather than
21+
# substituted at build time.
22+
go.html
23+
24+
# Prose is hand-wrapped at about 100 columns and uses *emphasis*. Prettier
25+
# rewrites that to _emphasis_ and reflows paragraphs, which is churn on text no
26+
# formatter can improve. The wrap width is a review convention, not a build rule.
27+
*.md

‎.prettierrc.json‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"printWidth": 100,
3+
"tabWidth": 2,
4+
"useTabs": false,
5+
"semi": true,
6+
"singleQuote": true,
7+
"trailingComma": "all",
8+
"arrowParens": "always",
9+
"endOfLine": "lf"
10+
}

‎AGENTS.md‎

Lines changed: 158 additions & 49 deletions
Large diffs are not rendered by default.

‎CHANGELOG.md‎

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Added
1313

14-
- `gr` (also `goodreads`): search books and reviews on Goodreads. It shipped
15-
until v1.1.0 dropped the `media` category it was filed under; it is back, in
14+
- `gr` (also `goodreads`): search books and reviews on Goodreads, filed under
1615
Search.
1716
- `track <number>` (also `pkg`): one keyword for any parcel. BunnyLol reads
1817
the carrier (UPS, USPS, FedEx or DHL) off the shape of the number and opens
@@ -83,20 +82,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8382
it is about to open, and offers an Open button, which holds the focus, and
8483
the escape search. The 1.2 second toast it replaces navigated on its own,
8584
which is a delay rather than a confirmation.
86-
- The rule-status pill says **Shortcuts active** instead of counting
87-
keywords, and **Some keywords not intercepted** when coverage is partial.
88-
The count moved every time a shortcut was switched on or off, and nobody
89-
acted on it. The numbers that do matter, what you exempted and what Chrome
90-
refused, are still on the line under it and on the Settings coverage line.
9185
- `web_accessible_resources` is narrowed to `go.html`. `go.js` and `assets/*`
9286
are same-origin subresources of an extension page and never needed an
9387
entry. Listing them exposed them, and the shipped sourcemaps, to the search
9488
engines.
9589

9690
### Removed
97-
- The green *Shortcuts active* pill. The rule-status pill in the topbar now
98-
appears only when there is something to act on: partial coverage, a failed
99-
sync, or interception switched off. A healthy profile shows nothing.
91+
- The always-on rule-status pill. It now appears only when there is something
92+
to act on: partial coverage, a failed sync, or interception switched off. A
93+
healthy profile shows nothing, and neither does one whose only shortfall is a
94+
keyword you exempted yourself. When it does appear it says **Some keywords
95+
not intercepted** rather than counting keywords, since the count moved every
96+
time a shortcut was switched on or off and nobody acted on it. The numbers
97+
that do matter, what you exempted and what Chrome refused, are on the line
98+
under it and on the Settings coverage line.
10099
- The `?` shortcut and the **Default AI** setting it read (`settings.defaultAi`).
101100
Pick the assistant with its own keyword instead: `c`, `gpt`, `gem` or `cc`.
102101
- The **AI prompt templates** card. `settings.aiTemplates` still overrides a
@@ -117,12 +116,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
117116

118117
## [1.0.0] - 2026-09-01
119118

119+
Never published anywhere. Recorded as the baseline the 1.1.0 entries are
120+
written against, which is why it has no link below.
121+
120122
### Added
121123

122124
- First release: keyword shortcuts for the address bar via
123125
`declarativeNetRequest`, a shortcut manager (options page), a toolbar
124126
popup, and an omnibox keyword (`bl`).
125127

126128
[Unreleased]: https://github.com/ion05/bunnylol/compare/v1.1.0...HEAD
127-
[1.1.0]: https://github.com/ion05/bunnylol/compare/v1.0.0...v1.1.0
128-
[1.0.0]: https://github.com/ion05/bunnylol/releases/tag/v1.0.0
129+
[1.1.0]: https://github.com/ion05/bunnylol/releases/tag/v1.1.0

‎CONTRIBUTING.md‎

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
Bug reports, new shortcuts and fixes are all welcome. Before you start:
44

55
- **[AGENTS.md](AGENTS.md) is the architecture note**, and its "Invariants that were violated during
6-
development" section is not decoration. Every entry is a bug that already shipped once. Every one
7-
has a regression test. And every one looks like reasonable code, which is why they came back. Read
8-
it before you touch routing, validation or the override layer.
9-
- **No new dependencies**, devDependencies included. The whole project runs on a handful of build
10-
tools. If you need a helper, inline it.
6+
development" section is not decoration. Every entry is a bug that already shipped once, and every
7+
one looks like reasonable code, which is why they came back. Most carry one regression test, named
8+
in the entry; two carry none and say so. Read it before you touch routing, validation or the
9+
override layer.
10+
- **No new dependencies in what ships.** Nothing is bundled into the extension but this repo's own
11+
source and one font. If you need a helper, inline it. Dev tooling is judged on its own merits and
12+
is currently prettier and eslint on top of typescript, vite and vitest. Adding to that list is a
13+
decision somebody makes on purpose.
1114

1215
## Setup
1316

@@ -26,11 +29,12 @@ extension card after every build.
2629
## The gate
2730

2831
```bash
29-
pnpm typecheck && pnpm test && pnpm build
32+
pnpm lint && pnpm typecheck && pnpm test && pnpm build
3033
```
3134

32-
All three, green, on **every** commit, not just at the end of a branch. CI runs exactly this on pull
33-
requests, plus `git diff --exit-code -- public/icons store`. `pnpm build` regenerates the icons from
35+
All four, green, on **every** commit, not just at the end of a branch. CI runs exactly this on pull
36+
requests, plus `git diff --exit-code -- public/icons store`. `pnpm lint` goes first because it is
37+
the fastest of the four and the cheapest to fix. `pnpm build` regenerates the icons from
3438
`design/tokens.css`. So if you change the generator or the accent colour and do not commit the
3539
result, it shows up as a dirty tree.
3640

@@ -46,6 +50,23 @@ the matcher. Then load the extension and try it.
4650
When you add a test, make sure it fails when the thing it guards is broken. Break the code, watch it
4751
go red, put it back.
4852

53+
## The test suite
54+
55+
20 files, about 150 cases, under a second. It is small on purpose. Before you add a test, answer
56+
this: **if it vanished and the code broke, would a user notice?**
57+
58+
It covers `resolve()` turning a typed query into a destination and honouring the `\` and `=` escape,
59+
one or two shapes per smart handler, the redirect rules against real Chrome-generated search URLs,
60+
import and export, the override layer, and a few property tests over the shipped registry (which is
61+
why adding a command usually needs no new test).
62+
63+
It deliberately does not cover stylesheets or design tokens, the DOM a view assembles (the decisions
64+
behind it are pure, in `src/options/model/*.ts`, and those are testable), that a removed feature
65+
stayed removed, or the same rule twice through a wrapper. A table that runs one assertion over every
66+
row of the registry is one property test, not 96 cases: that is how a suite gets to four figures
67+
without covering anything new. Views are verified in a real browser, which is the only place layout
68+
and focus behaviour are visible anyway.
69+
4970
## Adding or changing a command
5071

5172
Commands are plain data in `src/lib/commands.ts`.
@@ -75,15 +96,24 @@ A shortcut only *you* need does not need a PR at all. Make it in the options pag
7596
- Vanilla TS and CSS in the UI. No framework.
7697
- Colours, sizes and spacing in the UI stylesheets come from `design/tokens.css`. No literal hex, no
7798
raw `font-size: Npx`, and never `color: var(--accent)`, because the sand accent is a fill and
78-
fails contrast as text. `tests/tokens.test.ts` enforces all of it.
99+
fails contrast as text. Reviewed by hand: the 72-case suite that enforced it went with the rest
100+
of the design tests.
79101
- **Comment only where the reason is non-obvious.** Do not restate the code. A comment that says
80102
*why this and not the obvious alternative* is worth more than five that narrate what the next line
81103
does.
82104
- User text reaches the DOM only through `textContent` and `createElement`. A shortcut name is
83105
untrusted input.
84106

85-
There is no linter or formatter, and that is deliberate: one fewer dependency, and one fewer config
86-
to argue with. Match the surrounding code.
107+
`import type`, the indent, the quotes, the semicolons and the ban on default exports are all
108+
enforced now. `pnpm lint` runs eslint and `prettier --check`; `pnpm format` rewrites the files.
109+
Prettier is set to the style already here rather than the other way round, so running it over a
110+
clean tree changes nothing.
111+
112+
Both configs are short and commented. Every rule eslint has switched off names the convention it was
113+
fighting, so if a rule is in your way, read why it is off before turning it back on. Four things are
114+
outside the formatter on purpose: `design/` is the approved design bundle and changes through a
115+
design review, `go.html` carries a deliberately minified inline stylesheet the dispatch page needs
116+
to paint without one, Markdown is hand-wrapped prose, and `pnpm-lock.yaml` belongs to pnpm.
87117

88118
## Pull requests
89119

@@ -105,3 +135,25 @@ auto-closes the PR that targets it.
105135

106136
Do not open a public issue for a vulnerability. [SECURITY.md](SECURITY.md) has the private reporting
107137
route.
138+
139+
## Maintenance and releases
140+
141+
This project is maintained by one person, [@ion05](https://github.com/ion05). Issues and pull
142+
requests are read, but a reply may take a week. That is the honest expectation rather than a
143+
promise of anything faster.
144+
145+
Versions follow [semantic versioning](https://semver.org), and the stored state format is the
146+
compatibility surface. A new field that older builds ignore is a minor. A change that makes an
147+
older export unreadable is a major. Adding or removing a shipped shortcut is a minor, since a
148+
profile that never touched it still resolves.
149+
150+
A release is:
151+
152+
1. Bump `version` in `package.json` and `public/manifest.json` in the same commit. They are checked
153+
against each other by `tests/manifest.test.ts`, so they cannot drift.
154+
2. Add the section to [CHANGELOG.md](CHANGELOG.md) and the link at the foot of that file.
155+
3. Run the gate, then `pnpm package`, which rebuilds and writes `release/bunnylol-<version>.zip`.
156+
Build fresh rather than trusting a zip already sitting in `release/`: the Web Store enforces
157+
monotonic versions, so uploading a stale build under a new version costs you the next one too.
158+
4. Tag `vX.Y.Z`, push the tag, and attach that zip to a GitHub release.
159+
5. Upload the same zip to the Web Store.

‎PRIVACY.md‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Privacy Policy
22

3-
Last updated: 2026-09-01
3+
Last updated: 2026-09-03
44

55
## Summary
66

@@ -20,6 +20,12 @@ The extension also caches its rule-registration status under
2020
until the browser closes and never reaches disk. It holds counts and, when
2121
Chrome rejects a pattern, the affected keywords.
2222

23+
The options page keeps one more value, `bunnylol.collapsed`, in the ordinary
24+
`localStorage` of its own extension page (`COLLAPSE_KEY` in
25+
`src/options/model/collapse.ts`). It is the list of shortcut groups you have
26+
folded on that page, and nothing else. It is per-machine view state rather
27+
than settings, which is why it is not in the exported file.
28+
2329
## What happens when you type in the address bar
2430

2531
BunnyLol registers local `declarativeNetRequest` redirect rules for
@@ -34,9 +40,12 @@ do not match are left untouched and go to the search engine as normal.
3440
## What the extension cannot see
3541

3642
BunnyLol has no content scripts, reads no page content, and has no access to
37-
your browsing history. It does not request the `tabs` permission. The popup
38-
uses only `chrome.tabs.create` and `chrome.tabs.update`
39-
(`src/popup/popup.ts`), which do not require it.
43+
your browsing history. It does not request the `tabs` permission. Three places
44+
open a tab, and all of them use only `chrome.tabs.create` and
45+
`chrome.tabs.update`, which do not require that permission: the toolbar popup
46+
(`src/popup/popup.ts`), the omnibox keyword (`src/background.ts`), and the
47+
welcome tab shown once on install (`src/lib/install.ts`). Neither call can
48+
read a tab, only point one at a URL.
4049

4150
## Third parties
4251

0 commit comments

Comments
 (0)