pi-weave 0.1.22 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,24 +11,56 @@
11
11
  <a href="./LICENSE"><img alt="license: MIT" src="https://img.shields.io/npm/l/pi-weave?color=green"></a>
12
12
  </p>
13
13
 
14
- **A local knowledge workspace you can talk to.**
14
+ **Your notes. Your agent's notes. One searchable knowledge workspace.**
15
15
 
16
- pi-weave is an extension for [pi](https://github.com/earendil-works/pi). Ask Pi to take notes while you think out loud, keep your exact words alongside a useful summary, and explore everything in `/weave-view`.
16
+ I've been chasing the right note-taking workflow for years. Notion, OneNote, Obsidian, and plenty of others.
17
17
 
18
- It also understands the repository you are working in. Personal notes live in a Markdown vault; repository knowledge lives in a disposable `.okf` index. Both are plain files that humans and agents can read.
18
+ With pi-weave, I've made one that finally answers the way I want to work today especially now that AI agents are part of that work.
19
19
 
20
- ## Core capabilities
20
+ The idea is simple:
21
21
 
22
- - **Conversational note-taking.** Create and update notes through natural-language requests such as “start a note”, “add this”, or “remember that”.
23
- - **Verbatim narration with structured summaries.** During dictation, Pi preserves each spoken passage in an append-only `## Raw` section while maintaining an organized summary above it.
24
- - **Knowledge retrieval.** Pi searches existing notes when answering questions about previous decisions, people, projects, or meetings.
25
- - **Unified visual workspace.** `/weave-view` presents notes, links, repository structure, and provenance in a live browser interface.
26
- - **Optional session memory.** `/weave-scan sessions` turns changed pi transcripts into searchable notes with reusable technical takeaways.
27
- - **Repository exploration.** A lightweight, git-aware index gives Pi a structural overview of the current codebase before it reads files.
22
+ **You and your agent should share the same knowledge.**
28
23
 
29
- Nothing is captured silently. pi-weave creates or extends a personal note only when you ask it to.
24
+ Talk naturally and let your thoughts become structured notes. Write notes yourself. Let your agent capture decisions, discoveries, and
25
+ useful context. Bring important knowledge forward from previous sessions.
30
26
 
31
- ## Install
27
+ Then find it again — by searching the workspace yourself, or simply asking your agent.
28
+
29
+ Everything is local. Everything is readable. Your notes are ordinary files, not knowledge trapped inside another chat.
30
+
31
+ ## Think out loud
32
+
33
+ Dictation is one of my favorite ways to use AI.
34
+
35
+ Instead of stopping to organize every thought while I'm having it, I can just talk:
36
+
37
+ > Start a note about pi-weave positioning. I'm going to think out loud.
38
+
39
+ pi-weave keeps what I actually said while Pi continuously turns it into a useful, structured note.
40
+
41
+ The raw thought stays yours. The organization is handled for you.
42
+
43
+ And it doesn't end with human notes.
44
+
45
+ ## Your knowledge + your agent's knowledge
46
+
47
+ pi-weave gives humans and agents one workspace.
48
+
49
+ A note you dictate, something you write manually, a useful discovery made by an agent, or knowledge recovered from an earlier session can
50
+ all live together.
51
+
52
+ pi-weave keeps track of where knowledge came from, while making the whole workspace searchable by both you and your agent.
53
+
54
+ **The conversation can end. The knowledge doesn't have to.**
55
+
56
+ ## Built on Pi
57
+
58
+ pi-weave is built as an extension for [Pi](https://github.com/earendil-works/pi).
59
+
60
+ I've grown to love Pi precisely because it is such a lightweight agent harness. It gives the model tools and context without trying to
61
+ become the product itself, and its extensibility makes projects like pi-weave possible.
62
+
63
+ Install pi-weave:
32
64
 
33
65
  ```bash
34
66
  pi install npm:pi-weave
@@ -43,6 +75,8 @@ pi install /path/to/pi-weave
43
75
 
44
76
  Requires Node **20.13 or newer**.
45
77
 
78
+ Then just talk to Pi.
79
+
46
80
  ## Start taking notes
47
81
 
48
82
  Talk to Pi normally:
@@ -71,7 +105,8 @@ For each chunk, Pi:
71
105
  2. refreshes the structured summary above it;
72
106
  3. leaves the raw record untouched.
73
107
 
74
- This makes the note readable during the conversation without replacing your words with an AI reconstruction. Notes based on your dictation remain marked `source: human`; notes drafted by Pi are marked `source: agent`.
108
+ This makes the note readable during the conversation without replacing your words with an AI reconstruction. Notes based on your dictation
109
+ remain marked `source: human`; notes drafted by Pi are marked `source: agent`.
75
110
 
76
111
  Useful requests include:
77
112
 
@@ -82,6 +117,24 @@ Useful requests include:
82
117
  | “Clean up” or “finalize this note” | Reorganizes the readable body and preserves the raw tail |
83
118
  | “What did we decide about…?” | Searches the vault, then reads the relevant notes |
84
119
  | “Remember that…” | Stores durable knowledge for a future session |
120
+ | “Fix the broken links” | Audits and repairs stale `[[wiki-links]]` deterministically |
121
+
122
+ ## Keeping the vault connected
123
+
124
+ A wiki-link resolves to nothing when it is written as a bare title — `[[Quarterly Roadmap]]` when the note lives at `planning/roadmap-2026`
125
+ — or when it points at a note that was never written. Pi repairs those in one deterministic pass instead of rereading the vault and
126
+ guessing.
127
+
128
+ ```text
129
+ You: Check the links in my notes.
130
+ You: Fix the ones you're sure about.
131
+ ```
132
+
133
+ A target resolves by exact slug, then unique basename, then unique title — each rule requiring **exactly one** candidate. Ambiguous links
134
+ are reported with their candidates and never guessed; links to notes that were never written are listed, never invented. Repairs preserve
135
+ the alias, so the visible text does not change, and never touch the `## Raw` tail or code fences.
136
+
137
+ Renaming or moving a note rewrites its inbound links automatically, so its backlinks stay intact.
85
138
 
86
139
  ## `/weave-view`
87
140
 
@@ -94,11 +147,13 @@ Useful requests include:
94
147
  The browser workspace has four connected views:
95
148
 
96
149
  - **Tree** — notes, folders, and repository structure, with text and provenance filters.
97
- - **Note** — rendered Markdown with clickable `[[wikilinks]]`, link previews, tags, authorship, and an action to open the source in `$EDITOR`.
150
+ - **Note** — rendered Markdown with clickable `[[wikilinks]]`, link previews, tags, authorship, and an action to open the source in
151
+ `$EDITOR`.
98
152
  - **Graph** — a navigable map of notes, links, mentions, modules, and repository relationships. Selecting something updates every view.
99
153
  - **Context** — links, backlinks, tags, and code mentions for the current selection.
100
154
 
101
- Search with `⌘K` / `Ctrl K`. Press `?` for all shortcuts. The workspace updates as notes change on disk, so a note written by Pi appears without a reload. It follows the system theme by default and can be switched between light and dark.
155
+ Search with `⌘K` / `Ctrl K`. Press `?` for all shortcuts. The workspace updates as notes change on disk, so a note written by Pi appears
156
+ without a reload. It follows the system theme by default and can be switched between light and dark.
102
157
 
103
158
  The browser is read-only. Edit with `$EDITOR`, Obsidian, or the `weave_note` tool; unknown front-matter fields remain preserved.
104
159
 
@@ -111,7 +166,10 @@ The browser is read-only. Edit with `$EDITOR`, Obsidian, or the `weave_note` too
111
166
  /weave-scan sessions /path/to/history # explicit history root
112
167
  ```
113
168
 
114
- This opt-in scan treats a supplied file—or every bounded text file under a supplied directory—as opaque session material for the active model to interpret, then writes generated notes under `~/.okf/notes/sessions/`. That makes it usable with Claude Code, opencode, Codex, or exported history trees without requiring their schema or file extension. It skips unchanged files, captures outcomes plus reusable technical takeaways, works outside Git repositories, and can be stopped with `/weave-scan-cancel`.
169
+ This opt-in scan treats a supplied file—or every bounded text file under a supplied directory—as opaque session material for the active
170
+ model to interpret, then writes generated notes under `~/.okf/notes/sessions/`. That makes it usable with Claude Code, opencode, Codex, or
171
+ exported history trees without requiring their schema or file extension. It skips unchanged files, captures outcomes plus reusable technical
172
+ takeaways, works outside Git repositories, and can be stopped with `/weave-scan-cancel`.
115
173
 
116
174
  ## Repository knowledge
117
175
 
@@ -122,9 +180,11 @@ Inside a Git repository, pi-weave detects whether `<repo>/.okf/` is missing, fre
122
180
  /weave-scan deep # also summarize changed files with the active model
123
181
  ```
124
182
 
125
- The light index covers languages, packages, modules, entry points, and Git state. A deep scan adds short per-file summaries and only revisits files whose content changed.
183
+ The light index covers languages, packages, modules, entry points, and Git state. A deep scan adds short per-file summaries and only
184
+ revisits files whose content changed.
126
185
 
127
- The repository index is a cache, not a source of truth. Delete `.okf`, scan again, and nothing important is lost. pi-weave excludes it locally from Git by default.
186
+ The repository index is a cache, not a source of truth. Delete `.okf`, scan again, and nothing important is lost. pi-weave excludes it
187
+ locally from Git by default.
128
188
 
129
189
  ## Commands and tools
130
190
 
@@ -181,7 +241,9 @@ We probably want OIDC next quarter…
181
241
 
182
242
  Set `PI_WEAVE_VAULT` to use a different vault location.
183
243
 
184
- Reading, writing, searching, and viewing notes are local operations. Deep repository scans and session summaries send bounded input to whichever model you configured in pi. The browser workspace binds only to loopback, uses a per-session token, and shuts down with the pi session.
244
+ Reading, writing, searching, and viewing notes are local operations. Deep repository scans and session summaries send bounded input to
245
+ whichever model you configured in pi. The browser workspace binds only to loopback, uses a per-session token, and shuts down with the pi
246
+ session.
185
247
 
186
248
  The vault format, repository index, and skills are intentionally harness-agnostic. `src/core` contains no pi-specific imports.
187
249
 
@@ -201,7 +263,8 @@ npm run coverage
201
263
  npm run build:web
202
264
  ```
203
265
 
204
- Coverage must remain at or above **95%** for lines, branches, functions, and statements. If browser source changes, rebuild and commit `src/web/client/dist/app.js`.
266
+ Coverage must remain at or above **95%** for lines, branches, functions, and statements. If browser source changes, rebuild and commit
267
+ `src/web/client/dist/app.js`.
205
268
 
206
269
  Read [AGENTS.md](AGENTS.md) before contributing. Work on a feature branch; do not commit directly to `main`.
207
270
 
@@ -209,6 +272,7 @@ Read [AGENTS.md](AGENTS.md) before contributing. Work on a feature branch; do no
209
272
 
210
273
  - [Design](docs/design.md) — product and architecture
211
274
  - [Notepad skill](skills/weave-notepad/SKILL.md) — capture, narration, and provenance behavior
275
+ - [Link repair reference](skills/weave-notepad/references/link-repair.md) — how stale links resolve, and what repair will not do
212
276
  - [Historical browser workspace notes](docs/weave-workspace.md) — superseded implementation record
213
277
  - [Repository exploration skill](skills/weave-explore/SKILL.md) — how Pi uses the index
214
278
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-weave",
3
- "version": "0.1.22",
3
+ "version": "0.2.0",
4
4
  "description": "An agent-native knowledge workspace for your life and your code. Smart notepad + repository exploration, readable by humans and agents alike.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: weave-notepad
3
- description: "Take and retrieve durable notes in the pi-weave vault. Use when the user asks to remember something or to start/add to a note (aliases: notes, ai note, note-taking, note-taker), or when answering questions about past decisions, people, or projects. Also handles interview note-taking where raw dictations are appended AND expanded."
3
+ description: "Take and retrieve durable notes in the pi-weave vault. Use when the user asks to remember something or to start/add to a note (aliases: notes, ai note, note-taking, note-taker), or when answering questions about past decisions, people, or projects. Also handles interview note-taking where raw dictations are appended AND expanded, and deterministic repair of stale [[wiki-links]] between notes."
4
4
  ---
5
5
 
6
6
  # Weave Notepad
@@ -14,7 +14,7 @@ In pi, use the `weave_note` tool. In other harnesses (or when the tool is not av
14
14
 
15
15
  - **Notes** live at `~/.okf/notes/<slug>.md` (vault root overridable via `PI_WEAVE_VAULT`).
16
16
  - Each note has YAML front matter: `title`, `created`, `updated` (ISO-8601), `tags: [..]`, and `source: human | agent | generated`.
17
- - `weave_note` actions: `list`, `get`, `add`, `append`, `finalize`, `search`. `finalize` restructures the body *above* the `## Raw` tail and
17
+ - `weave_note` actions: `list`, `get`, `add`, `append`, `finalize`, `search`, `links`, `suggest`. `finalize` restructures the body *above* the `## Raw` tail and
18
18
  preserves the tail verbatim — a body with no tail yet is preserved **in full** as a newly created tail, so finalization never destroys
19
19
  dictation.
20
20
  - **Dictation appends**: use `append` with `raw: true` — the tool appends the text verbatim into the `## Raw` tail as a dated fenced block,
@@ -79,19 +79,26 @@ down". Never promote conversation into a note on your own initiative — capture
79
79
 
80
80
  ## How to write a good note
81
81
 
82
- 1. **Search first** (`weave_note` action=search): if a note exists, `append` to it rather than creating a duplicate.
82
+ 0. **Go straight to the tool.** A note is one `weave_note` call. Do not `list` the vault, inspect the repository, or run `git` none of that
83
+ informs what to write, and on a large vault `list` alone floods the context. When the user gives you the content ("note that says X"),
84
+ `add` it and report the slug.
85
+ 1. **Search first when adding to existing knowledge** (`weave_note` action=search, with the note's key terms): if a note on the subject
86
+ exists, `append` to it rather than creating a duplicate. Skip this when the user is clearly starting something new — one targeted search,
87
+ never a vault listing.
83
88
  2. Title: short noun phrase ("Auth boundary decision", not "Notes").
84
- 3. **Scribble in, verbatim.** When the user is dictating, append their words to the note as rough, verbatim scribbles no silent rewording.
89
+ 3. **Ask only when the content is genuinely missing.** A vague request ("write a note weave") needs one short question, not exploration
90
+ searching the vault or the repo will not reveal what the user meant.
91
+ 4. **Scribble in, verbatim.** When the user is dictating, append their words to the note as rough, verbatim scribbles — no silent rewording.
85
92
  Append with `raw: true` so they land under the `## Raw` tail at the end of the note (the tail is created automatically if missing).
86
- 4. **Compile continuously during dictation.** After *every* interactive append in dictation mode, immediately finalize the body *above* the
93
+ 5. **Compile continuously during dictation.** After *every* interactive append in dictation mode, immediately finalize the body *above* the
87
94
  raw tail so the compiled doc stays current (see [Dictation mode](#dictation-mode-continuous-compile)). Outside dictation mode,
88
95
  compilation stays on request.
89
- 5. **Finalize on request.** When the user says "finalize this" / "clean this up", restructure the body *above* the raw tail: front-loaded
96
+ 6. **Finalize on request.** When the user says "finalize this" / "clean this up", restructure the body *above* the raw tail: front-loaded
90
97
  summary, sections, entities, links. Use `weave_note` action=finalize (or edit the file directly in other harnesses). Move nothing out of
91
98
  `## Raw` — it is append-only and never rewritten. A note with no `## Raw` tail yet gets its entire pre-finalize body preserved as a new
92
99
  raw tail: finalization is editorial, never destructive.
93
- 6. Tags: 1–4 lowercase tags; reuse existing tags when possible.
94
- 7. Provenance: notes the user scribbled stay `source: human` (finalization is editorial, not authorship) — pass `source: "human"` to `add`
100
+ 7. Tags: 1–4 lowercase tags; reuse existing tags when possible.
101
+ 8. Provenance: notes the user scribbled stay `source: human` (finalization is editorial, not authorship) — pass `source: "human"` to `add`
95
102
  for user-scribbled notes. Notes you draft from scratch are `source: agent` (the default). Never overwrite a `source: human` note's
96
103
  meaning; append with a dated "Agent addendum" section instead.
97
104
 
@@ -109,3 +116,30 @@ The scan is opt-in and never runs on its own. Suggest it when the user asks why
109
116
 
110
117
  Use `weave_note` action=search with the user's key terms, then `get` the best hits. When a note and the repository index disagree, trust the
111
118
  repository for facts about code and flag the discrepancy — the note may be stale intent.
119
+
120
+ ## Repairing stale links
121
+
122
+ A link written as a bare title or basename — `[[Quarterly Roadmap]]` when the note is `planning/roadmap-2026` — resolves to nothing.
123
+ **Never reconnect a vault by reading every note and guessing which ones relate.** Run the deterministic pass instead:
124
+
125
+ ```jsonc
126
+ weave_note { "action": "links" } // report: fixable / ambiguous / unresolvable
127
+ weave_note { "action": "links", "fix": true } // apply only the unambiguous repairs
128
+ ```
129
+
130
+ It resolves by exact slug, then unique basename, then unique title — each requiring exactly one candidate. Ambiguous links are reported
131
+ with their candidates and never guessed; unresolvable ones point at notes that were never written. Aliases are preserved, the `## Raw`
132
+ tail and code fences are never touched, and `updated` is not bumped. Report first, apply after the user sees it.
133
+
134
+ To find connections that were **never written** — two notes that belong together but have never referenced each other — use `suggest`:
135
+
136
+ ```jsonc
137
+ weave_note { "action": "suggest" } // strongest unlinked pairs
138
+ weave_note { "action": "suggest", "slug": "some/note" } // what relates to one note
139
+ ```
140
+
141
+ It ranks pairs by how much *rare* vocabulary they share, and cites the shared terms as evidence. **`suggest` never writes** — there is no `fix`.
142
+ A similarity score is a soft signal and a `[[link]]` is a hard claim, so propose the worthwhile pairs to the user and add links only to those they
143
+ confirm.
144
+
145
+ See [references/link-repair.md](references/link-repair.md) for the full rules, guarantees, and when to run each.
@@ -0,0 +1,90 @@
1
+ # Link repair — keeping the vault connected
2
+
3
+ A wiki-link resolves to nothing when it is written as a bare title or basename — `[[Quarterly Roadmap]]` while the note lives at
4
+ `planning/roadmap-2026` — or when it points at a note that was never written. The graph then shows an isolated note that is in fact well
5
+ connected.
6
+
7
+ **Never reconnect a vault by reading it.** Do not search the vault note by note, infer which notes "feel related", and hand-write links.
8
+ That is slow, costs tokens, and is not reproducible — two runs give two different answers. There is a deterministic pass that does it in
9
+ one shot.
10
+
11
+ ## The tool
12
+
13
+ ```jsonc
14
+ weave_note { "action": "links" } // read-only report
15
+ weave_note { "action": "links", "fix": true } // apply the unambiguous repairs
16
+ ```
17
+
18
+ In other harnesses, call `repairVaultLinks(vaultRoot, { apply })` from `pi-weave/core`.
19
+
20
+ Always run the report first, read it, then apply. The report is cheap (one pass over the vault, no model calls).
21
+
22
+ ## How targets resolve
23
+
24
+ Three rules, tried in order. A rule fires only when it yields **exactly one** candidate:
25
+
26
+ | # | Rule | Example |
27
+ |---|------|---------|
28
+ | 1 | exact slug | `[[planning/roadmap-2026]]` — already correct, left alone |
29
+ | 2 | unique basename | `[[roadmap-2026]]` → `planning/roadmap-2026` |
30
+ | 3 | unique slugified title | `[[Quarterly Roadmap]]` → `planning/roadmap-2026` |
31
+
32
+ Anything else is reported, never guessed:
33
+
34
+ - **ambiguous** — several notes match (two `plan.md` in different folders). The report lists the candidates; a human picks one, or you
35
+ ask. Do not choose on their behalf.
36
+ - **unresolvable** — no note matches. The link points at something never written. Offer to create the note or drop the link; **never
37
+ invent content to satisfy a link.**
38
+
39
+ ## What a repair does and does not do
40
+
41
+ - Rewrites `[[Quarterly Roadmap]]` → `[[planning/roadmap-2026|Quarterly Roadmap]]`. The **alias preserves the visible text**, so the rendered prose is unchanged —
42
+ only the target moves.
43
+ - **Does not touch the `## Raw` tail.** A link inside dictation is the user's words, quoted. Off limits, always.
44
+ - **Does not touch fenced code blocks.** `[[…]]` in a code sample is a string literal.
45
+ - **Does not bump `updated`.** A repair is bookkeeping, not an edit; bumping it would reorder the whole vault by recency.
46
+ - **Is idempotent.** A second run finds nothing.
47
+
48
+ ## Renames repair themselves
49
+
50
+ `renameNote`, `moveNote` and `renameFolder` rewrite inbound links automatically, so renaming or moving a note keeps its backlinks intact.
51
+ The repair pass is for links that were *written* stale — typed as a bare title, or pointing somewhere that never existed.
52
+
53
+ ## Finding connections that were never made
54
+
55
+ Repair fixes links that point wrong. It cannot find links that were **never written** — two notes that belong together but have never referenced each
56
+ other. That is `suggest`:
57
+
58
+ ```jsonc
59
+ weave_note { "action": "suggest" } // strongest pairs vault-wide
60
+ weave_note { "action": "suggest", "slug": "some/note" } // what relates to this note
61
+ weave_note { "action": "suggest", "limit": 40 }
62
+ ```
63
+
64
+ It ranks unlinked pairs by IDF-weighted cosine over every term a note carries — title, tags and body in one bag — weighting each term by how rare
65
+ it is *in this vault*. Vocabulary shared by most notes (`sprint`, `meeting`, a tag on half the vault) scores near zero and connects nothing;
66
+ a ticket id or an unusual name on a handful of notes scores high. Nothing is domain-specific: the vault's own frequencies decide.
67
+
68
+ Every suggestion cites the shared terms that earned it. **Read the evidence, not the score** — a list like `shared: acme-1234, release-pipeline`
69
+ is checkable, `0.16` is not.
70
+
71
+ ### suggest never writes
72
+
73
+ This is the rule that matters. `suggest` only reports; there is no `fix`. A similarity score is a soft signal and a `[[link]]` is a hard claim —
74
+ once written into a body it is indistinguishable from one the user wrote deliberately. Good scores here are around 0.1–0.3, not 0.9, so treat the
75
+ output as a shortlist for a human:
76
+
77
+ 1. Run `suggest`, read the shared terms.
78
+ 2. Propose the worthwhile pairs **to the user**.
79
+ 3. Add `[[wikilinks]]` only to those they confirm.
80
+
81
+ Never bulk-apply suggestions, and never present one as an established connection.
82
+
83
+ ## When to run it
84
+
85
+ - The user asks to "fix the links in" their notes — `links`.
86
+ - The health panel or `/weave` reports dangling links — `links`.
87
+ - After bulk-importing or reorganising notes outside the tool — `links`.
88
+ - The user asks what a note "relates to", or to "connect" / "link up" the vault — `suggest`, then confirm before writing.
89
+
90
+ Do not run `fix: true` unprompted on a vault you did not just change — show the report and let the user approve. Reporting is always safe.
@@ -29,7 +29,7 @@
29
29
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
30
30
  import { gitSpawnCount } from "../git";
31
31
  import { NOTES_DIR } from "../paths";
32
- import { buildGraph, DEFAULT_MAX_NOTES, type BuildGraphInput } from "../graph/build";
32
+ import { buildGraph, type BuildGraphInput } from "../graph/build";
33
33
  import type { GraphModel } from "../graph/model";
34
34
  import { readRepositorySide } from "../graph/current";
35
35
  import { withMutationQueue } from "../mutex";
@@ -41,16 +41,14 @@ import type { HtmlArtifact, Note } from "../types";
41
41
  *
42
42
  * The two travel together because a caller deriving anything per-note — the
43
43
  * tag index (§4.3) is the motivating case — must use *the same* list the
44
- * graph used, including the `DEFAULT_MAX_NOTES` truncation. Reading the notes
45
- * from a second call would let the cap fall between them and produce a tag
46
- * pointing at a slug the graph has no node for.
44
+ * graph used. Reading the notes from a second call could race a vault change
45
+ * and produce a tag pointing at a slug the graph has no node for.
47
46
  */
48
47
  export interface WorkspaceSnapshot {
49
48
  model: GraphModel;
50
49
  /**
51
- * Exactly the notes `buildGraph` saw: newest-updated first and already
52
- * truncated to the cap. Frozen — this is the cache's own array and a
53
- * caller mutating it would corrupt the next build.
50
+ * Exactly the notes `buildGraph` saw, newest-updated first. Frozen — this
51
+ * is the cache's own array and a caller mutating it would corrupt the next build.
54
52
  */
55
53
  notes: readonly Note[];
56
54
  }
@@ -383,10 +381,6 @@ export class WorkspaceCache {
383
381
  return this.lastSnapshot;
384
382
  }
385
383
 
386
- // Truncated once, here, and then handed to *both* the builder and the
387
- // snapshot — so a caller deriving per-note data cannot see a note the
388
- // graph has no node for (§4.3).
389
- const kept = notes.slice(0, DEFAULT_MAX_NOTES);
390
384
  const input: BuildGraphInput = {
391
385
  vault: {
392
386
  root: this.vaultRoot,
@@ -395,7 +389,7 @@ export class WorkspaceCache {
395
389
  ...(this.folders.length > 0 ? { folders: this.folders } : {}),
396
390
  ...(this.artifactCount > 0 ? { artifactCount: this.artifactCount } : {}),
397
391
  },
398
- notes: kept,
392
+ notes,
399
393
  artifacts: refreshed.artifacts,
400
394
  repository: repo?.repository ?? null,
401
395
  };
@@ -405,7 +399,7 @@ export class WorkspaceCache {
405
399
  this.builtAt = this.now().toISOString();
406
400
  const snapshot: WorkspaceSnapshot = {
407
401
  model: buildGraph(input),
408
- notes: Object.freeze(kept),
402
+ notes: Object.freeze(notes),
409
403
  };
410
404
  this.lastSnapshot = snapshot;
411
405
  return snapshot;
@@ -15,9 +15,6 @@ import type { EdgeKind, GraphEdge, GraphModel, GraphNode } from "./model";
15
15
  import { buildPathIndex, resolveMentions, type PathIndex } from "./mentions";
16
16
  import { extractWikilinks } from "./wikilinks";
17
17
 
18
- /** Hard cap on note nodes so a large vault cannot overwhelm the viewer. */
19
- export const DEFAULT_MAX_NOTES = 500;
20
-
21
18
  export interface BuildGraphInput {
22
19
  vault: VaultStatus;
23
20
  /** Full notes including bodies (for wiki-link extraction). */
@@ -118,15 +115,11 @@ function parseRemote(raw: string): { label: string; url: string } {
118
115
 
119
116
  function buildVaultSide(
120
117
  input: BuildGraphInput,
121
- maxNotes: number,
122
118
  nodes: GraphNode[],
123
119
  edges: GraphEdge[],
124
120
  danglingLinks: Record<string, string[]>,
125
121
  paths: PathIndex,
126
- ): string[] {
127
- const truncated = input.notes.length > maxNotes;
128
- const kept = input.notes.slice(0, maxNotes);
129
-
122
+ ): void {
130
123
  const vaultDetail: Record<string, string> = {
131
124
  root: input.vault.root,
132
125
  notes: String(input.vault.noteCount),
@@ -135,12 +128,9 @@ function buildVaultSide(
135
128
  if ((input.vault.artifactCount ?? artifacts.length) > 0) {
136
129
  vaultDetail.artifacts = String(input.vault.artifactCount ?? artifacts.length);
137
130
  }
138
- if (truncated) {
139
- vaultDetail.warning = `Graph shows the ${maxNotes} most recent notes — the vault holds ${input.vault.noteCount}. Wiki-links to older notes are omitted.`;
140
- }
141
131
  nodes.push({ id: "vault", kind: "vault", label: "Vault", provenance: null, detail: vaultDetail });
142
132
 
143
- const keptSlugs = new Set(kept.map((n) => n.slug));
133
+ const allSlugs = new Set(input.notes.map((n) => n.slug));
144
134
  const artifactSlugs = new Set(artifacts.map((a) => a.slug));
145
135
  const artifactLinks = new Map<string, number>();
146
136
 
@@ -153,23 +143,17 @@ function buildVaultSide(
153
143
  const noteDirs = [
154
144
  ...new Set([
155
145
  ...(input.vault.folders ?? []),
156
- ...kept.map((n) => n.slug.split("/").slice(0, -1).join("/")),
146
+ ...input.notes.map((n) => n.slug.split("/").slice(0, -1).join("/")),
157
147
  ...artifacts.map((a) => a.slug.split("/").slice(0, -1).join("/")),
158
148
  ]),
159
149
  ]
160
150
  .filter((d) => d.length > 0)
161
151
  .sort();
162
- const notesIn = (dir: string): number => kept.filter((n) => n.slug.startsWith(`${dir}/`)).length;
163
- const totalNotesIn = (dir: string): number => input.notes.filter((n) => n.slug.startsWith(`${dir}/`)).length;
152
+ const notesIn = (dir: string): number => input.notes.filter((n) => n.slug.startsWith(`${dir}/`)).length;
164
153
  for (const dir of noteDirs) {
165
154
  const id = `vfolder:${dir}`;
166
155
  folderIds.set(dir, id);
167
- const keptCount = notesIn(dir);
168
- const totalCount = totalNotesIn(dir);
169
- const folderDetail: Record<string, string> = { path: dir, notes: String(keptCount) };
170
- if (totalCount > keptCount) {
171
- folderDetail.warning = `${totalCount - keptCount} older note(s) in this folder omitted by note limit`;
172
- }
156
+ const folderDetail: Record<string, string> = { path: dir, notes: String(notesIn(dir)) };
173
157
  nodes.push({
174
158
  id,
175
159
  kind: "module",
@@ -186,9 +170,9 @@ function buildVaultSide(
186
170
  return (dir.length > 0 && folderIds.get(dir)) || "vault";
187
171
  };
188
172
 
189
- for (const note of kept) {
173
+ for (const note of input.notes) {
190
174
  const links = extractWikilinks(note.body);
191
- const resolved = links.filter((slug) => keptSlugs.has(slug));
175
+ const resolved = links.filter((slug) => allSlugs.has(slug));
192
176
  const detail: Record<string, string> = {
193
177
  slug: note.slug,
194
178
  source: note.source,
@@ -198,7 +182,7 @@ function buildVaultSide(
198
182
  // The names, not just the count (§4.2). `detail` keeps carrying the count
199
183
  // because it is what the TUI's side panel prints; the structured targets
200
184
  // go on the model, where a UI can turn them into ghost nodes.
201
- const dangling = links.filter((slug) => !keptSlugs.has(slug) && !artifactSlugs.has(slug));
185
+ const dangling = links.filter((slug) => !allSlugs.has(slug) && !artifactSlugs.has(slug));
202
186
  if (dangling.length > 0) {
203
187
  detail["dangling links"] = String(dangling.length);
204
188
  danglingLinks[note.slug] = dangling;
@@ -238,7 +222,6 @@ function buildVaultSide(
238
222
  nodes.push({ id: `artifact:${artifact.slug}`, kind: "file", label: artifact.title, provenance: null, detail });
239
223
  edges.push({ source: parent, target: `artifact:${artifact.slug}`, kind: "contains" });
240
224
  }
241
- return [...keptSlugs];
242
225
  }
243
226
 
244
227
  function buildRepositorySide(
@@ -355,12 +338,8 @@ function buildRepositorySide(
355
338
  }
356
339
  }
357
340
 
358
- /**
359
- * Build the graph model for the viewer. Notes are capped at `maxNotes`
360
- * (docs/design.md §11); the vault node carries a warning when truncated.
361
- */
362
- export function buildGraph(input: BuildGraphInput, options: { maxNotes?: number } = {}): GraphModel {
363
- const maxNotes = options.maxNotes ?? DEFAULT_MAX_NOTES;
341
+ /** Build the complete graph model for the viewer. */
342
+ export function buildGraph(input: BuildGraphInput): GraphModel {
364
343
  const nodes: GraphNode[] = [];
365
344
  const edges: GraphEdge[] = [];
366
345
  const danglingLinks: Record<string, string[]> = {};
@@ -372,7 +351,7 @@ export function buildGraph(input: BuildGraphInput, options: { maxNotes?: number
372
351
  const paths = input.repository === null
373
352
  ? (new Map<string, string>() as PathIndex)
374
353
  : buildPathIndex(input.repository.index.structure);
375
- buildVaultSide(input, maxNotes, nodes, edges, danglingLinks, paths);
354
+ buildVaultSide(input, nodes, edges, danglingLinks, paths);
376
355
  if (input.repository !== null) {
377
356
  buildRepositorySide(input.repository, input.summaries, nodes, edges);
378
357
  }
@@ -382,9 +361,8 @@ export function buildGraph(input: BuildGraphInput, options: { maxNotes?: number
382
361
  nodes,
383
362
  edges,
384
363
  danglingLinks,
385
- // The same slice the vault side kept, so the digest describes exactly
386
- // the notes that have nodes. Slug-ordered inside, hence order-stable.
387
- contentDigest: noteContentDigest(input.notes.slice(0, maxNotes)),
364
+ // Slug-ordered inside, hence order-stable.
365
+ contentDigest: noteContentDigest(input.notes),
388
366
  };
389
367
  }
390
368
 
@@ -19,7 +19,7 @@ import { assessStaleness, readRepoIndex } from "../repoIndex";
19
19
  import { readSummaryMap } from "../summaries";
20
20
  import type { Note } from "../types";
21
21
  import { getNote, readVault, resolveNotePath } from "../vault";
22
- import { buildGraph, DEFAULT_MAX_NOTES, type BuildGraphInput } from "./build";
22
+ import { buildGraph, type BuildGraphInput } from "./build";
23
23
  import type { GraphModel } from "./model";
24
24
 
25
25
  /** A note read for the viewers (read-only; never cached). Mirrors the vault `Note` shape. */
@@ -91,9 +91,8 @@ export async function readRepositorySide(
91
91
 
92
92
  /**
93
93
  * Assemble the fresh graph from disk. Called on every viewer fetch
94
- * (no caching). Reads the vault (capped at
95
- * DEFAULT_MAX_NOTES) and, when cwd is an indexed git repository, the repo
96
- * index + deep-scan summary sidecars. Degrades to a vault-only graph when
94
+ * (no caching). Reads the complete vault and, when cwd is an indexed git
95
+ * repository, the repo index + deep-scan summary sidecars. Degrades to a vault-only graph when
97
96
  * the repo has no index or the index is corrupt.
98
97
  *
99
98
  * One read per note: `readVault` returns bodies *and* the file count, so the
@@ -111,7 +110,7 @@ export async function buildCurrentGraph(cwd: string, vaultRoot: string = resolve
111
110
  ...(folders ? { folders } : {}),
112
111
  ...(artifactCount ? { artifactCount } : {}),
113
112
  },
114
- notes: notes.slice(0, DEFAULT_MAX_NOTES),
113
+ notes,
115
114
  ...(artifacts ? { artifacts } : {}),
116
115
  repository: null,
117
116
  };
package/src/core/index.ts CHANGED
@@ -53,12 +53,30 @@ export {
53
53
  moveNote,
54
54
  parseHtmlArtifact,
55
55
  renameFolder,
56
+ readVault,
56
57
  renameNote,
58
+ repairVaultLinks,
57
59
  resolveHtmlPath,
58
60
  resolveNotePath,
59
61
  searchNotes,
60
62
  upsertNote,
63
+ type LinkRepairResult,
61
64
  } from "./vault";
65
+ export {
66
+ suggestLinks,
67
+ type LinkSuggestion,
68
+ type SuggestOptions,
69
+ type SuggestionReport,
70
+ } from "./links/similar";
71
+ export {
72
+ auditLinks,
73
+ rewriteLinks,
74
+ scanLinks,
75
+ type AmbiguousLink,
76
+ type LinkAudit,
77
+ type LinkFix,
78
+ type UnresolvableLink,
79
+ } from "./links/repair";
62
80
  export type { HtmlArtifact } from "./types";
63
81
  export { withMutationQueue } from "./mutex";
64
82
  export { formatDashboard, formatStatusLine, getWorkspaceStatus } from "./workspace";