Releases: 88lin/facetmark
Release list
v2.0.0 — Local search page, full-stack pagination, and a complete UI rebuild
108 commits, 152 files changed (+34,563 / −2,702), 8 PRs merged (#7–#11, #15–#18) since v1.6.1.
facetmark 2.0 is the "it just works" release. The biggest gap in 1.x was that facetmark serve started a server with no page to open — the extension needed a manual npm run build + load, docs/landing/ was read-only, and /docs was Swagger. For someone who doesn't write code, the project had no entry point after install. 2.0 fills that gap, and moves retrieval pagination, configuration, and administration onto HTTP at the same time.
✨ New Features
1. Local search page — GET /app
facetmark serve now ships a complete search UI — one HTML shell, one stylesheet, a handful of ES modules. No bundler, no Node build step, no new Python dependency.
- Two views: search (using #11's
offset/depthpagination, withdepthpinned from the first page) and a library overview (/stats, answering "is my index built yet?"). - Bilingual (EN/ZH) + light/dark theme: all strings live in
static/strings.json; theme follows the system, manually toggleable, persisted tolocalStorage. GET /app/boot: the sole endpoint for the pairing token. Returns the token only when the TCP peer is a loopback address and theHostheader is a loopback literal — the second check blocks DNS rebinding. Falls back to manual paste otherwise.GET /app/static/*: StarletteStaticFilesmount; ETag and 304 come for free.- The startup banner now prints
open the search page: .../app.
2. Full-stack pagination (offset + depth)
"Only 20 results" was not one ceiling but four stacked: a page size, a missing offset, a candidate pool that conflated "how deep to retrieve" with "how many to return", and a reranker whose cost grew linearly with page size. 2.0 separates all four.
offsetanddepthplumbed through every surface:search()/quick_search()/ HTTP/search/quick/ MCPsearch_bookmarks/ CLI (-o/--offset,--depth) / browser extension / karakeep bridge.- Six new response fields:
limit,offset,depth,total,has_more,depth_capped— all report actual values, not echoes of the request. depthis an explicit parameter because RRF is only depth-stable with a single facet. Deepening the pool to reach page 2 lets page 2 disagree with page 1. The fix is to pin depth: the response reportsdepth, the client sends it back, and every page is a slice of the same ranking.- Browser extension moves from a hardcoded
api.search(q, 20)to "load more". db.in_chunks(): batchesIN (...)lookups in groups of 900 to avoid the 999-variable limit in distribution-built SQLite.
3. HTTP admin API
Import, index, and configure are all available over HTTP (new src/facetmark/admin.py). No CLI required — the browser settings page can import bookmarks, trigger indexing, and edit configuration.
4. File-based configuration
- Read and write settings from
<data_dir>/config.toml(newsrc/facetmark/configfile.py), with safe write-back. - Retrieval ceilings —
MAX_PAGE_SIZE(200),MAX_CANDIDATE_DEPTH(2000),RERANK_DEPTH(20) — moved from scatteredle=annotations into configuration. Over-limit is truncation, not rejection, and the truncated values are reported in the response.
5. Facet contribution reporting
Search responses now report each facet's contribution to the fused score, making "why this ranking" explainable.
🎨 UI & Documentation
Web UI — full rebuild
- Apple system fonts + a wisteria palette; browns, coffees, dark greens and near-blacks removed.
- Hand-drawn dashed visual language; all 8 views redrawn — white-card grids replaced with layering, hue, and dashed frames.
- New setup screen and settings screen; every ranking shows "why this result".
- Typographic scale pushed into the app UI, capsule navigation, macOS-style code windows.
color-schemesynced with the manual theme (light/darkpaired), fixing UA widgets rendering dark when the system is dark but the user picked light.
Site & README
- Site rebuilt as three bilingual pages (home / quickstart / guide) with a scroll-narrative landing page.
- README fully rewritten (EN + ZH): centered hero, tree-directory TOC, GitHub Admonitions, horizontal Mermaid flowcharts, Star History chart.
- New guide sections for Web UI and pagination; home page adds eight screenshots (search / library × EN/ZH × light/dark).
docs/addsconfig.html,guide.html,quickstart.html,webui.html,integrations.html,measured.html(EN + ZH each).
🐛 Fixes
- Empty results on chat-only libraries: when the embeddings endpoint 404s, search no longer returns an empty page for every query.
search()auto-degrades to lexical facets and reportsdegraded_fromwith the replaced facet names. synthesizeempty answers: when all source excerpts are empty (pages imported but not yet indexed), the model is no longer called — the call is guaranteed to fail and costs money. Returns an explicit gap: "none of the sources have any indexed text; runfacetmark index".- Unlabeled title-only summaries: pages whose body wasn't extracted get summaries inferred from title + URL, previously indistinguishable from real summaries. New
basiscolumn (body/title/karakeep); inferred summaries carry a badge, and/synthesizeannotates them in the prompt and gap. .gitignoreswallowing wheel contents: line 23's*.htmlatesrc/facetmark/web/index.html, so the build succeeded, the server started, and/app404'd. Added!src/facetmark/web/*.htmlnegation, backed by a wheel CI job.- Contrast: new
--ink-mutetoken; rank numbers, footers and small text are ≥ 4.5:1 on every background — measured, not asserted in a comment. - Settings page: domain lists are now stored, uploads no longer drop titles, env-locked fields can no longer be edited.
- Mobile: fifth view was unreachable, Escape didn't clear search — fixed.
⚠️ Behavior Changes
- Rerank depth is no longer tied to page size: capped by
RERANK_DEPTH(20), so per-page rerank cost is independent of page size. On reranking profiles (E,fused), pages beyond 20 items keep fusion order in the tail. This may affect relevance; the impact has not been measured. CANDIDATES_PER_FACETchanges from "pool size" to "pool floor": every retrieval is at least this deep, so a 5-row request still reports an honesttotal.- First-page depth changes from
max(3 × limit, 30)tomax(candidates_per_facet, 3 × window). - MCP
search_bookmarksdrops the staticleonlimit(default remains 10 — that's a context-window budget, not a recall ceiling). - karakeep bridge: retrieval window ceiling moves from literal
500toMAX_CANDIDATE_DEPTH;truncatednow reads the pipeline'shas_moredirectly.
🔒 Security
- Removed an accidentally committed personal bookmark export (
favorites_2026_8_4.html, 1,710HREF=entries with folders and timestamps) fromHEADand added a named.gitignoreentry. This changes HEAD only, not history — the file is still reachable in old commits. - DNS rebinding protection on
/app/boot(loopback address + loopbackHostdouble check).
🧪 Testing & CI
- Python tests 1188 → 1514 (+326), extension tests 16 → 28.
- CI adds a
webuijob (node --teston pure-function modules) and awheeljob (builds the wheel and assertsweb/index.htmland three other files are inside). scripts/browser_check.py: CI actually opens the page and looks — seven bugs were reintroduced one by one and all caught.- New
star-historyworkflow (updates the Star History chart every Monday). - Site adds structural parity tests between EN and ZH content, plus "committed HTML equals re-rendered output".
- Fixed a flaky Windows timing test (
test_requests_to_one_host_are_spaced_apart): lowered the per-gap floor from 0.02 s to 0.01 s to tolerate Windows' ~15 ms timer resolution.
📦 Install & Upgrade
pip install facetmark==2.0.0Or from source:
pip install -e '.[dev]'
facetmark import BOOKMARKS.html
facetmark index
facetmark serve # then open .../app in your browserFull Changelog: v1.6.1...v2.0.0
v1.6.1
What's Changed
- 1.6.1: the exception 1.6.0 swallowed was a dead-verdict factory that never fired by @88lin in #2
- v3 query set: protocols + dead pool + q_save_action by @88lin in #3
- v3 holdout: freeze 551 queries (dead pool + q_save_action) by @88lin in #4
- extension: one design system for popup + options, dark mode, empty state by @88lin in #5
Full Changelog: v1.6.0...v1.6.1
What's Changed
- 1.6.1: the exception 1.6.0 swallowed was a dead-verdict factory that never fired by @88lin in #2
- v3 query set: protocols + dead pool + q_save_action by @88lin in #3
- v3 holdout: freeze 551 queries (dead pool + q_save_action) by @88lin in #4
- extension: one design system for popup + options, dark mode, empty state by @88lin in #5
Full Changelog: v1.6.0...v1.6.1
What's Changed
- 1.6.1: the exception 1.6.0 swallowed was a dead-verdict factory that never fired by @88lin in #2
- v3 query set: protocols + dead pool + q_save_action by @88lin in #3
- v3 holdout: freeze 551 queries (dead pool + q_save_action) by @88lin in #4
- extension: one design system for popup + options, dark mode, empty state by @88lin in #5
- ci: datetime.UTC is 3.11+; the matrix starts at 3.10 by @88lin in #6
New Contributors
Full Changelog: v1.6.0...v1.6.1
v1.6.0
v1.5.0
v1.4.0
v1.3.0
v1.2.0
v1.1.0
v1.0.0
facetmark v0.1.0
First release, cut on a commit with the full matrix green (Ubuntu and Windows, Python 3.10 and 3.12). The unpacked extension build is attached. The service installs from source with pip install -e '.[dev]'; then facetmark import BOOKMARKS.html, facetmark index, facetmark serve, and pair the extension with the token from facetmark token.