01How to read this page
- Pre-registration. Criteria are written down before the run. A rung measured on the queries that motivated it is a hypothesis, not a result, and is labelled exploratory.
- Paired tests. Every A-versus-B claim is paired on the same queries, with a bootstrap confidence interval and a McNemar test on the discordant pairs. Wins and losses are reported separately, because a net zero from 0 changes and a net zero from 40 wins and 40 losses are different facts.
- Nothing is reopened. Once a query set is frozen and a verdict recorded, it stands. A new question needs a new query set.
- pp means percentage points. CI95 is a 95% bootstrap interval.
Every query set on this page was written by the author of the tool. Bootstrapping fixes sampling noise; it does nothing at all about the author knowing what the tool is good at. The most valuable contribution this project could receive is a query set written by somebody else.
02W1 · Four-facet fusion lost to one facet
default withdrawn 479 queries · one real 1,700-bookmark library · pre-registered
The premise of the whole project was that fusing four facets beats any one of them. Three criteria were registered before the run. All three failed.
| Rung | Facets | Recall@5 | Recall@1 | MRR@10 | p50 |
|---|---|---|---|---|---|
| A | content vector only | 0.643 | 0.505 | 0.564 | 148 ms |
| B | + two lexical | 0.589 | — | — | 189 ms |
| C | all four | 0.635 | — | — | 526 ms |
| D | + context + graph | 0.639 | — | — | 523 ms |
Fusion cost 5.4pp of Recall@5 and made queries 3.5× slower. Config A by query type: content-style 0.959, vague 0.706, episodic 0.279.
Why it lost
Flat-weight reciprocal rank fusion has no way to express confidence. Two weak facets that happen to agree score 0.0279; one strong facet that is certain scores 0.0164. The coincidence wins. That is not a tuning problem, it is what the formula does.
What survived the same run
| Survivor | Effect | Wins / losses | p | Cost |
|---|---|---|---|---|
| Graph expansion as a separate group | +2.09pp Recall@5 | 10 / 0 | 0.0019 | 9 ms |
| Reranker, on Recall@1 | +4.80pp CI95 [+1.46, +8.35] | 45 / 22 | 0.0067 | — |
Both shipped. Note that graph expansion only works as an addition — returned as its own group rather than merged into the ranking.
03W2/W3 · The episodic gate shipped, then lost
default reverted after shipping
The episodic gate detects “the thing I saved around the same time as X” and restricts retrieval to that saving window. On its 616-query holdout it won cleanly and was shipped.
| Query set | Comparison | ΔRecall@5 | CI95 | Wins / losses | p |
|---|---|---|---|---|---|
| 616-query holdout | A → A_gatedctx | +3.09pp | [1.79, 4.55] | 19 / 0 | 3.8e−6 |
| 361-query precision probe | A → A_gatedctx | −18.83pp | [−23.27, −14.68] | 3 / 71 | — |
The second row is the same feature, measured on a query set built afterwards to ask a different question: what does the gate do when it fires on a query it should not have? Recall@5 fell from 0.9058 to 0.7175 and Recall@1 from 0.801 to 0.363.
The stratification is the whole answer
| Stratum | n | ΔRecall@5 |
|---|---|---|
| The saving window contains the target | 57 | +0.00pp — exactly zero |
| The saving window misses the target | 304 | −22.37pp |
When the gate is right it adds nothing. When it is wrong it throws the answer away. Verdict gate_precision_unqualified; the default reverted to no gating.
A narrower gate scored +1.79pp on the original 616-query set and −10.52pp on the precision probes. Shipping on the first number while the second exists would have been choosing the query set that gave the answer we wanted. It was not shipped.
04The other side of the gate · no verdict
descriptive only · below the pre-registered sample floor
The precision probe asked what happens when the gate fires and should not have. This asks the opposite: how often does it fail to fire when it should? The protocol was pre-registered before the run, mirroring the precision protocol.
| Measure | Value |
|---|---|
| Probes available | 16 q_save_action rows of the frozen v3 holdout |
| Gate fired | 0 of 16 |
| Miss rate | 100.0%, Wilson CI95 [80.64, 100.00] |
| ΔRecall@5 (A_gatedctx − A) | +0.00pp, CI95 [0.00, 0.00] |
| McNemar | 0 gained, 0 lost, p = 1.0, 0 discordant pairs |
| Protocol self-check | pass — the untriggered subset must move exactly 0.00pp, and it did |
| Verdict | none. 16 < the pre-registered floor of 25 |
The gate never fired, so both arms ran identical code and produced identical per-query ranks. A Δ of exactly zero with zero discordant pairs is not evidence that the gate is harmless — it is evidence that nothing was tested. The minimum detectable effect is undefined here, because the formula divides by the number of discordant pairs and there were none.
All sixteen phrasings are ways of saying “the one I put away” — 之前收起来的那个, the link I set aside, 我塞进清单里的那篇. None of them contains a word in the gate's trigger vocabulary, which currently keys on 保存, 收藏, saved, bookmark and eleven others.
The obvious move — add these sixteen phrasings to the vocabulary — is exactly what the protocol forbids, because selecting a vocabulary on the probes that measure it is circular. Reaching a verdict requires generating at least 25 probes in a new round with new seeds, at frozen parameters, and then passing both the miss-rate bar and the 361-probe precision bar. Until then the vocabulary is unchanged.
05Five candidate fixes, five verdicts
After W1 killed fusion, five obvious repairs were each measured rather than argued about.
| Candidate | What was measured | Verdict |
|---|---|---|
| Drop the lexical facets entirely | 80.1% of content-style and 46.3% of vague queries need no vector at all — but 6.05% (29 of 479) are findable only lexically, above the pre-registered 5% line. | kept |
| Weight the facets instead of flat RRF | A coincidence on two weak facets scores 0.0279; certainty on one strong facet scores 0.0164. | explains the loss |
| Fix the trigram facet on Chinese | It matched 25 of 211 Chinese queries (11.85%). After the fix, 202 of 211 (95.73%). Overall Recall@5: unchanged. | fixed, no gain |
| Raise the boost ceiling | MAX_BOOST = 1.60 crosses 79.7% of the score range in config A but only 20.9% in C/D. Equal displacement power would need 6.03. 66.3% of candidates get exactly 1.0. | measured, not shipped |
| Turn on the intent facet | 19 of 50 generated intents (38%) were plausible, below the pre-registered 50% line. The information word is absent from the page 34.0% of the time overall and 62.4% on body-poor pages. | off |
The third row is the interesting one. A real bug was found and fixed — the trigram facet went from useless on Chinese to working — and end-to-end recall did not move. A fix that is genuinely a fix and changes no outcome is a normal result, and reporting it is the only thing that keeps the other four honest.
06karakeep round trip · unfaithful
roundtrip_unfaithful 2,376 bookmarks · 616 holdout queries · protocol frozen first
Question: if a library is pushed through the karakeep bridge and read back, is it the same library? Three criteria were registered first.
| Criterion | Bar | Measured | Verdict |
|---|---|---|---|
| Metric fidelity | |ΔRecall@5| ≤ 3pp with CI95 inside ±5pp | −0.81pp, CI95 [−2.44, +0.81] | pass |
| Rank fidelity | median overlap@5 ≥ 4 and top-1 agreement ≥ 80% | median 4.0, top-1 79.06% | fail by 0.94pp |
| Read-path equivalence | HTTP and native identical over 616×2 | 0 mismatches | pass |
The cause is fully attributed
- Bodies survive byte-identical: 1,876 of 1,876.
- Summaries survive: 2,375 of 2,375, 100%.
- Topics match 0% and entities 1.18% — because karakeep's tags are the browser's folder labels, not topics.
- The keyword line collapses from 19,016 distinct terms to 13; mean terms per page falls from 10.32 to 0.76; the most common tag is
未分类on 1,124 pages. - Vectors move by a median cosine of 0.9846 — small, and enough to reshuffle a top-5.
Grafting the source enrichment back produced 2,376 of 2,376 byte-identical embed texts, residual zero, which closes the attribution. Re-running facetmark index repairs it: 0 karakeep bodies needed re-fetching, all 2,376 rows re-enriched, and the graph came back matching except for 212 semantic edges (26,485 against 26,697).
Metric-level conclusions transfer to a karakeep-enriched library. Rank-level ones do not, until you re-index. If you run the bridge, run facetmark index afterwards.
07The decay layer, measured twice — the second run overturned the first
The decay layer demotes pages that look stale. Round one measured it and found exactly nothing:
| Round one | Value |
|---|---|
| ΔRecall@5 | 0.0000pp, CI95 [0.00, 0.00] |
| Cold pages | 8 of 2,376 |
| Cold pages among the 230 targets | 0 |
The health table had zero rows and open_count was 0 for all 2,376 pages. The layer could not fire because it had nothing to read. A clean zero from a correctly executed protocol, measuring nothing.
Round two ran the same bytes with a local health check first.
| Round two | Shipped (0.02) | Reachable (0.0) |
|---|---|---|
| Recall@5 | 0.5860 | 0.5714 |
| Recall@1 | 0.4237 | 0.4188 |
| Rescue valve open | 417 of 616 | 0 of 616 |
| Health rows | 2,376 (was 0) | 2,376 |
| Cold pages | 73 — 3.07% (was 8, 0.34%) | 73 |
| Cold ∩ the 230 targets | 8, across 19 queries | 8 |
ΔRecall@5 went from +0.0000pp in round one to −1.4610pp CI95 [−2.5974, −0.4870] in round two. The mechanism is countable: of 37 rank changes, 12 fell out of the top 20 entirely — 10 of those had been in the top 5 and 5 had been rank 1. Twenty-four rose, 21 of them by a single place, and exactly 1 crossed into the top 5. Net −10 + 1 = −9, and −9/616 = −1.4610pp.
Why the threshold still has not changed
Two bugs are cancelling, and the cancellation is load-bearing.
- Bug one: the cold-layer condition treats “the URL died” as “the saved copy is useless”. But facetmark stores the body. A dead URL is precisely when the local snapshot matters most, and
driftedis worse still, because then the snapshot is the only surviving record. - Bug two: with
rrf_k = 60, one unit-weight facet tops out at1/61 = 0.016393, which is below the rescue threshold of0.02. In the shipped single-facet profile the rescue valve is therefore always open and the demotion has never once executed. - Remove either one alone and results get measurably worse. Both are pinned by
tests/test_decay_reach.pyso neither can be quietly “cleaned up”.
What changed instead was the instrumentation. cold_census() now reports the three conditions separately, and facetmark stats and facetmark health --check name never_opened_selects_everything and health_never_checked out loud.
One more detail worth keeping: 4 of the 8 damaged targets have char_count = 0 and are still retrieved correctly, through title and lexical facets. Body loss is not the same as retrieval loss.
08One real library, end to end
Synthetic corpora hide integration failures. This is one actual browser export, imported and indexed with the shipped code path.
| Stage | Result |
|---|---|
| The file | favorites_2026_8_4.html, 1.7 MB, 96 folders, 4 levels deep |
| Import | parsed 1,710 → inserted 1,701, 9 duplicates merged, 1 non-indexable |
| Index (no page fetching) | 322 saving sessions, 9,132 edges, 1,386 distinct domains, 1,775 vectors |
| Median query latency | 2,265 ms |
The latency number is honest and unflattering: it is a cold, unfetched index on a laptop, and it is the number that would be quietly omitted from a launch post.
09What none of this measures
- Whether anyone else's queries look like these. Every query set was written by the author of the tool. This is the single largest threat to every number on this page and no amount of bootstrapping touches it.
- Whether the decay layer helps, because in the shipped profile it cannot fire at all.
- Whether the intent facet would help a different library. It was measured on this one, generated by one model, and it lost.
- Whether the karakeep bridge works against a live karakeep. The contract is pinned and replayed; a running instance has never been tested.
- Whether the reranker helps with a real cross-encoder. What ships offline is term overlap. An ablation run under that reranker measures the harness, not the idea, and must not be quoted as evidence that reranking works.
- Long-term behaviour. Every measurement is a snapshot. Nobody has run this for a year and watched what a growing library does to the session clustering.
Write 100 queries against your own library, with the target URL for each, as JSONL. Run facetmark eval --no-build --queries yours.jsonl --rungs A,C,full. Post the JSON. That single contribution is worth more than any feature request, and it is the one thing the author structurally cannot do.