One skill, every app in the category.
- Chromium JSON -
- ChromeBraveEdgeArcChromiumVivaldi +
- ChromeBraveEdgeArcChromiumVivaldiOpera
- Safari plist diff --git a/tests/test_skills.py b/tests/test_skills.py index 7eacee3..a551f7e 100644 --- a/tests/test_skills.py +++ b/tests/test_skills.py @@ -210,3 +210,38 @@ def test_marketplace_source_points_at_this_repo(self): if __name__ == "__main__": unittest.main() + + +class TestPublishedCountsMatchCode(unittest.TestCase): + """The README and the site make a numeric claim. Claims drift; tests do not. + + Opera was added to the code and both the site and the README kept + advertising the old number until this test existed. + """ + + def setUp(self): + import importlib.util + spec = importlib.util.spec_from_file_location( + "platforms", os.path.join(ROOT, "scripts", "platforms.py")) + self.platforms = importlib.util.module_from_spec(spec) + spec.loader.exec_module(self.platforms) + self.expected = (len(self.platforms.CHROMIUM) + + 1 # Safari + + len(self.platforms.FIREFOX)) # Firefox and Tor + + def test_site_browser_count_matches_code(self): + site = read(os.path.join(ROOT, "site", "index.html")) + m = re.search(r'class="stat">(\d+)<', site) + self.assertIsNotNone(m, "site no longer states a browser count") + self.assertEqual(int(m.group(1)), self.expected) + + def test_readme_browser_count_matches_code(self): + words = {8: "Eight", 9: "Nine", 10: "Ten", 11: "Eleven", 12: "Twelve"} + readme = read(os.path.join(ROOT, "README.md")) + self.assertIn(f"**{words[self.expected]} browsers, one markdown file.**", readme) + + def test_every_chromium_app_appears_on_the_site(self): + site = read(os.path.join(ROOT, "site", "index.html")) + for app in self.platforms.CHROMIUM: + with self.subTest(app=app): + self.assertIn(app.capitalize(), site) From 5bd86b21be30c6e532e46f02dc80ece5a4431700 Mon Sep 17 00:00:00 2001 From: Vamil Gandhi <13998000+vamgan@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:54:38 -0400 Subject: [PATCH 2/7] docs: correct the claim about adding a browser The site, the README, and the contributor guide all said adding Vivaldi was one line in a table. It is three lines across two files: the reference table, the family paths, and PROCESS_NAMES. The omitted third line is the one whose absence caused the Opera bug. A path without a process name means running() returns None, a skill cannot tell the browser is open, and the write is clobbered on quit. The copy was understating precisely the part that destroys data, and the contributor guide was the worst place for it to be wrong. The guide now lists all three registration points and explains why a partial addition is dangerous, since the test that enforces the pairing only helps someone who knows why it exists. --- README.md | 6 +++--- docs/ADDING-A-SKILL.md | 22 +++++++++++++++++----- site/index.html | 2 +- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9f36031..d48c67c 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ skill covers every app that shares a storage format: | Safari plist | Safari | | `places.sqlite` | Firefox · Tor Browser | -**Ten browsers, one markdown file.** Adding Vivaldi was one line in a table. +**Ten browsers, one markdown file.** Adding Vivaldi took three lines and no new skill. The same holds across operating systems. Chrome keeps identical bookmark JSON on Windows, macOS, and Linux, so only the path changes. @@ -150,8 +150,8 @@ Read `references/safe-mutation-rules.md` and follow that workflow. Open a pull request. See [ADDING-A-SKILL.md](docs/ADDING-A-SKILL.md). -Already-supported format? Then it's a **table row**, and every existing skill in that -category covers your app immediately. +Already-supported format? Then it's **three lines** across two files, and every +existing skill in that category covers your app immediately. Want a skill for an app that isn't here? [Open an issue](../../issues/new?template=app_request.md). diff --git a/docs/ADDING-A-SKILL.md b/docs/ADDING-A-SKILL.md index eb0aa67..67a9ffa 100644 --- a/docs/ADDING-A-SKILL.md +++ b/docs/ADDING-A-SKILL.md @@ -6,14 +6,26 @@ Two kinds of contribution. Pick the one that matches what you want. ## Adding an app to a category that already exists -**This is usually a table row.** If your app stores its data in a format that is -already supported, add it to `references/app-data-locations.md` and every existing -skill in that category covers it immediately. +**Three lines, across two files.** If your app stores its data in a format that is +already supported, register it in all three places below and every existing skill in +that category covers it immediately. No new skill, no new code. + +| Where | What to add | +|---|---| +| `references/app-data-locations.md` | the table row, so a human can read it | +| `scripts/platforms.py`, the family table | where the data lives, on each platform | +| `scripts/platforms.py`, `PROCESS_NAMES` | what the running process is called | + +**All three, or none.** Registering a path without a process name is the dangerous +half-addition: `running()` returns `None`, so a skill cannot tell the app is open, and +a browser that rewrites its store on exit will clobber the change. Opera shipped in +exactly that state until a test caught it. A test now enforces the pairing, but the +test only helps if you know why it is there. Vivaldi was added this way. It is a Chromium browser, so it keeps bookmarks in the -same JSON format as Chrome — the entire change was one line in a table. +same JSON format as Chrome, and it needed no new skill at all. -Include: +For the table row, include: | Field | Why it matters | |---|---| diff --git a/site/index.html b/site/index.html index 8cd4cc0..df602e5 100644 --- a/site/index.html +++ b/site/index.html @@ -347,7 +347,7 @@
- FirefoxTor Browser
One skill, every app in the category.
Adding Vivaldi was one line in a table.
+Adding Vivaldi took three lines and no new skill.
The same holds across operating systems. Chrome keeps identical bookmark JSON on Windows, macOS, and Linux, so only the path changes. macOS is the strictest of the three and the one that needs a permission grant.