Morning discovery¶
Morning discovery surfaces up to three surprising connections — pairs of notes that share concepts but aren't linked yet — each one click from being related. The value of a slip-box shows up in unexpected links, not the backlinks you already knew about.
Where you see it¶
On Home, in its own section. Running "Show discoveries" from the command palette still works and opens Home — since #504 the Discovery surface is gone, because Home already rendered these pairs from the same function and a door onto something that lives elsewhere is not a surface.
Each row is one pair, and either name opens its note. Linking them is an act you perform in the note (or through Cultivate's connect), never something a surface does for you (constitution §XII).
How pairs are found¶
The engine is graph-structural, offline, and read-only — the same heuristic as find related (#154), applied to pairs:
score(a, b) = 2·|notes linking to both| + 1·|notes both link to| — co-citation weighted above bibliographic coupling. Only pairs that share context score above zero, already-linked pairs are excluded (either direction), and the top-scoring unlinked pairs are shown (canonical order, ties broken by path). Candidate pairs are generated once per node (every pair among a note's neighbours), so it stays efficient. No text similarity, no embeddings, no AI.
Accepting, and saying no¶
Accepting needs no memory. Link the two notes and the pair stops being a gap by construction: the engine excludes already-linked pairs in either direction, so once the model re-indexes it is simply not there any more. The expands relation is the neutral "this note connects to that idea" choice — there is no generic related type in the semantic vocabulary (#147) — and every write goes through FrontmatterService (deduplicated, add-only, never removes anything).
Saying no is the half that needed recording, and since #534 (epic #529) it is. Every gap row on Home carries not related: one click records a judgement — origin: "derived", verdict: "rejected", subject: "gap:<the other note's path>" — and writes nothing to the vault. Not a link, not a property, not a line.
Why a click and not a formula: on a real vault the strongest gaps are often the vault's own scaffolding — folder index notes co-cited by their parent — and the epic tried to rank them down with arithmetic twice, failing both times with numbers. Degree damping left the ranking unchanged (the offenders have degree 3–7; they are not hubs) and a score floor deleted the real headline seam before it touched the noise. No graph statistic separates a filing convention from a thought. A person does it in one click, which is §XII exactly: the machine observes, the human rules, and the verdict is data.
Three properties of the verdict, each load-bearing:
- Both directions. The record canonicalises the pair (lower path, higher subject), so a verdict given either way round removes the pair either way round.
- Idempotent. Clicking twice records once — and the guard is the pair already being ruled out, not the recorder's repeat check, which compares timestamps and would let a second click through.
- Every reader, at once. Home's list, the 3D map's gap lens, the seams, the dashboard's connections count, what to do next and both script doors read one filtered pass, so a surface cannot forget the verdict. A guardrail test pins the callers of the raw projections to a closed list.
The action is absent when the judgement record is turned off, never present-but-inert, and the setting's own description says so.
Continuous discovery (#365, D5)¶
Discovery is not only a pane you open — it is continuous. The Home surface registers vault listeners and, on every change (a note created, saved, renamed or deleted), re-runs the same heuristic discovery and recommendation projections. So a connection that becomes possible the moment you save a note is already waiting the next time you glance at Home — you never have to go looking.
Two invariants hold, and a guardrail test pins them:
- Never AI. The continuous path is
findDiscoveries/deriveRecommendations/buildHome— pure, offline model queries. No completion is requested in the background; AI stays a deliberate, foreground Action (#337, constitution §XII). A test asserts the whole Home path imports no AI provider and calls nothing that reaches one. - Never a silent write. Continuous discovery only proposes — it surfaces connections and next moves; committing any of them stays the same judgement-gated act described above.
There is deliberately no parallel background engine, no badge, and no on/off toggle: continuous discovery is a property of the Home surface refreshing, not a new feature bolted on (design by subtraction).
What can be a gap¶
Both endpoints are notes that exist (#538). The knowledge model records a link's target whether or not it resolves — that is deliberate, and it is what makes a note's degree honest — so before this rule two broken links in one note proposed a connection between the two notes that were never written. The tally now indexes the model's own notes and nothing else, so an unresolved target has no index and a pair touching it can never be scored.
The fix sits in the shared pass, not in each reader: Home's list, the seams, the dashboard's connections count, the recommendation and the map's gap lens all stopped seeing them at once. Nothing reports the broken links themselves — KnowledgeDebt already has a dangling category for that.
Architecture¶
gapTally(model) (pure, Obsidian-free, memoised on the model alone)
→ every unlinked pair of **existing notes** that shares context: a count, and a walk of them
topGaps(model, limit) (a bounded linear selection over that walk, never a sort)
→ the strongest `limit` pairs, score desc then a asc then b asc
findDiscoveries(model, { limit }) (the same, bound to the default of three -- public on zf)
read by: Home's section · the dashboard's count (the whole tally) · deriveRecommendations
linking a pair → the create-semantic-relation action writes an `expands` relation
open → workspace.openLinkText
One pass, several readers: memoise keys on a projection's arguments, so a reader wanting three pairs and a reader wanting sixty would each have paid for the tally behind them. The tally takes no arguments, which is what lets them share it — see one tally, many readers. At ten thousand notes it holds over 1.2 million pairs, so for the limits the product uses it is never copied into an array and never sorted — measured A/B on one warm tally, 1,393 ms of sorting became 553 ms of selecting (budgets).