pi-weave 0.1.11 → 0.1.13

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.
Files changed (75) hide show
  1. package/README.md +148 -132
  2. package/package.json +1 -2
  3. package/src/core/cache/workspace.ts +26 -9
  4. package/src/core/concurrency.ts +3 -6
  5. package/src/core/frontmatter.ts +0 -53
  6. package/src/core/graph/build.ts +12 -8
  7. package/src/core/graph/current.ts +4 -6
  8. package/src/core/graph/model.ts +1 -1
  9. package/src/core/graph/wikilinks.ts +3 -3
  10. package/src/core/index.ts +26 -27
  11. package/src/core/paths.ts +0 -7
  12. package/src/core/slug.ts +13 -0
  13. package/src/core/types.ts +1 -0
  14. package/src/core/vault.ts +45 -467
  15. package/src/core/view/detail.ts +1 -1
  16. package/src/core/view/health.ts +1 -1
  17. package/src/core/view/tree.ts +15 -3
  18. package/src/pi/index.ts +6 -85
  19. package/src/pi/summarize.ts +2 -2
  20. package/src/pi/viewer/tui/bodyStore.ts +4 -7
  21. package/src/pi/viewer/tui/branding.ts +7 -148
  22. package/src/pi/viewer/tui/run.ts +3 -17
  23. package/src/pi/viewer/tui/surface/base.ts +24 -3
  24. package/src/pi/viewer/tui/surface/explore.ts +41 -6
  25. package/src/pi/viewer/tui/workspace.ts +23 -351
  26. package/src/pi/viewer/tui/workspaceRoot.ts +31 -172
  27. package/src/pi/viewer/web/run.ts +7 -117
  28. package/src/web/client/api.dom.ts +2 -2
  29. package/src/web/client/api.ts +14 -176
  30. package/src/web/client/bootstrap.ts +5 -14
  31. package/src/web/client/context/context.model.ts +9 -11
  32. package/src/web/client/dist/app.js +77 -167
  33. package/src/web/client/graph/dynamics.ts +5 -65
  34. package/src/web/client/graph/renderer.dom.ts +7 -8
  35. package/src/web/client/graph/renderer.ts +9 -35
  36. package/src/web/client/main.tsx +1 -1
  37. package/src/web/client/note/Note.tsx +37 -60
  38. package/src/web/client/note/note.model.ts +23 -0
  39. package/src/web/client/search/SearchPalette.tsx +45 -36
  40. package/src/web/client/search/search.model.ts +33 -454
  41. package/src/web/client/shell/Columns.tsx +13 -81
  42. package/src/web/client/shell/Header.tsx +2 -10
  43. package/src/web/client/shell/Shell.tsx +50 -123
  44. package/src/web/client/shell/StatusBar.tsx +1 -4
  45. package/src/web/client/shell/icons.model.ts +4 -7
  46. package/src/web/client/shell/keys.model.ts +5 -42
  47. package/src/web/client/shell/keys.ts +2 -2
  48. package/src/web/client/shell/shell.model.ts +10 -133
  49. package/src/web/client/shell/theme.model.ts +2 -2
  50. package/src/web/client/shell/theme.ts +33 -121
  51. package/src/web/client/state.ts +9 -89
  52. package/src/web/client/tree/Tree.tsx +24 -97
  53. package/src/web/client/tree/tree.model.ts +8 -56
  54. package/src/web/client/workspace.ts +72 -242
  55. package/src/web/server/page.ts +8 -10
  56. package/src/web/server/routes.ts +30 -415
  57. package/src/web/server/server.ts +6 -145
  58. package/src/web/shared/layout.ts +72 -624
  59. package/src/web/shared/wire.ts +10 -178
  60. package/src/core/sessions.ts +0 -929
  61. package/src/pi/sessionScan.ts +0 -104
  62. package/src/pi/viewer/tui/explorer.ts +0 -586
  63. package/src/web/client/live.model.ts +0 -275
  64. package/src/web/client/live.ts +0 -151
  65. package/src/web/client/note/Editor.tsx +0 -109
  66. package/src/web/client/note/editor.controller.ts +0 -151
  67. package/src/web/client/note/editor.model.ts +0 -686
  68. package/src/web/client/search/search.ts +0 -107
  69. package/src/web/client/shell/Divider.tsx +0 -44
  70. package/src/web/client/shell/cssvars.ts +0 -70
  71. package/src/web/client/shell/drag.model.ts +0 -170
  72. package/src/web/client/shell/layout.model.ts +0 -500
  73. package/src/web/client/shell/viewport.ts +0 -29
  74. package/src/web/server/sse.ts +0 -321
  75. package/src/web/server/watcher.ts +0 -507
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # pi-weave
2
2
 
3
3
  <p align="center">
4
- <img src="https://raw.githubusercontent.com/EranYonai/pi-weave/main/docs/pi-weave-logo.png" alt="pi-weave — an agent-native knowledge workspace" width="220"/>
4
+ <img src="https://raw.githubusercontent.com/EranYonai/pi-weave/main/docs/pi-weave-logo.png" alt="pi-weave" width="220"/>
5
5
  </p>
6
6
 
7
7
  <p align="center">
@@ -11,190 +11,206 @@
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
- **An agent-native knowledge workspace for your life and your code.**
14
+ **A local knowledge workspace you can talk to.**
15
15
 
16
- pi-weave is a [pi](https://github.com/earendil-works/pi) extension with two faces that are secretly one:
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
17
+ words alongside a useful summary, and explore everything in `/weave-view`.
17
18
 
18
- 1. **A smart notepad.** A persistent vault of knowledge decisions, ideas, people, meetings stored as plain Markdown notes with YAML
19
- front matter under `~/.okf/notes/`. Your agent reads and writes it *with* you through the `weave_note` tool; everything stays editable by
20
- hand in any editor.
19
+ It also understands the repository you are working in. Personal notes live in a Markdown vault; repository knowledge lives in a disposable
20
+ `.okf` index. Both are plain files that humans and agents can read.
21
21
 
22
- 2. **A repository exploration engine.** A derived, git-aware knowledge index of the repo you are standing in, living at `<repo>/.okf/` —
23
- structure, languages, packages, modules, entry points, and staleness state. Built and read through the `weave_repo` tool. Rebuildable,
24
- disposable, never the source of truth.
22
+ ## Core capabilities
25
23
 
26
- One rule spans both: **everything is equally readable by humans and agents.** Markdown and JSON on disk, no opaque database, no lock-in
27
- format. Every generated artefact carries provenance (`human`, `agent` or `generated`), so agent-written content never masquerades as
28
- something you wrote.
24
+ - **Conversational note-taking.** Create and update notes through natural-language requests such as “start a note”, “add this”, or “remember
25
+ that”.
26
+ - **Verbatim narration with structured summaries.** During dictation, Pi preserves each spoken passage in an append-only `## Raw` section
27
+ while maintaining an organized summary above it.
28
+ - **Knowledge retrieval.** Pi searches existing notes when answering questions about previous decisions, people, projects, or meetings.
29
+ - **Unified visual workspace.** `/weave-view` presents notes, links, repository structure, and provenance in a live browser interface.
30
+ - **Repository exploration.** A lightweight, git-aware index gives Pi a structural overview of the current codebase before it reads files.
29
31
 
32
+ Nothing is captured silently. pi-weave creates or extends a personal note only when you ask it to.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pi install npm:pi-weave
30
38
  ```
31
- 🕸️ vault:12 · my-project:ok ← pi's status line when weave is active
39
+
40
+ Other install sources:
41
+
42
+ ```bash
43
+ pi install git:github.com/EranYonai/pi-weave
44
+ pi install /path/to/pi-weave
32
45
  ```
33
46
 
34
- See [docs/design.md](docs/design.md) for the reasoning behind all of it.
47
+ Requires Node **20.13 or newer**.
35
48
 
36
- ## Install
49
+ ## Start taking notes
37
50
 
38
- ```bash
39
- pi install npm:pi-weave # from npm (recommended)
40
- pi install git:github.com/EranYonai/pi-weave # from git
41
- pi install /path/to/pi-weave # local path
51
+ Talk to Pi normally:
52
+
53
+ ```text
54
+ You: Start a note called Authentication migration.
55
+
56
+ You: We probably want OIDC next quarter, but existing JWT clients need
57
+ a compatibility window.
58
+
59
+ You: Add that the gateway team owns the migration plan.
60
+
61
+ You: What open questions are in this note?
42
62
  ```
43
63
 
44
- Requires Node **>= 20.13.0**. For development against a checkout: `pi -e ./src/pi/index.ts`.
64
+ For live narration or interview notes, tell Pi that you are dictating:
65
+
66
+ ```text
67
+ You: Start a note for this interview. I’m going to narrate; keep my words
68
+ verbatim and organize the note as we go.
69
+ ```
45
70
 
46
- On session start pi-weave detects the repository you are in, checks whether `.okf` exists and is fresh, and reports it in the status footer
47
- — a filled `●` marks weave as active. An unindexed repository gets a one-line nudge; a stale one gets a warning.
71
+ For each chunk, Pi:
48
72
 
49
- ## Tools and commands
73
+ 1. appends your words unchanged to the note’s `## Raw` tail;
74
+ 2. refreshes the structured summary above it;
75
+ 3. leaves the raw record untouched.
50
76
 
51
- | Surface | Name | Purpose |
52
- |---|---|---|
53
- | Tool | `weave_note` | `list` / `get` / `add` / `append` / `finalize` / `search` over vault notes |
54
- | Tool | `weave_repo` | `status` / `scan` / `overview` of the `.okf` repository index |
55
- | Command | `/weave` | workspace dashboard (vault + repository) |
56
- | Command | `/weave-view` | open the knowledge workspace in your browser |
57
- | Command | `/weave-scan` | build or refresh the repository index (light) |
58
- | Command | `/weave-scan deep` | light index plus model-written per-file summaries (opt-in, incremental, background) |
59
- | Command | `/weave-scan sessions` | summarize pi session history into the vault as memory notes (incremental, background) |
60
- | Command | `/weave-scan-cancel` | stop an in-flight `/weave-scan deep` or `sessions` run |
61
- | Skill | `weave-notepad` | how the agent should take good notes |
62
- | Skill | `weave-explore` | how the agent should explore repositories |
63
-
64
- `/weave-scan deep` refreshes the light index and then writes a short model summary per file to `.okf/repository/summaries/`, skipping files
65
- whose content hash has not changed since their last summary. It costs tokens, so it never runs implicitly — and it runs in the background,
66
- so `/weave-scan-cancel` can stop it mid-flight.
67
-
68
- `/weave-scan sessions` is the repo-agnostic sibling (docs/session-scan.md): it reads every pi session transcript under
69
- `~/.pi/agent/sessions/`, hashes each file while reading it, and writes one generated vault note per changed session under
70
- `~/.okf/notes/sessions/` — pi's memory as first-class, wikilinked notes in an inner folder of the vault graph. Unchanged transcripts cost no
71
- LLM calls at all, and older layouts migrate automatically on the next scan.
72
-
73
- ## The workspace
74
-
75
- `/weave-view` opens a browser knowledge workspace over the same graph the tools see — the vault and the repository index as one model.
77
+ This makes the note readable during the conversation without replacing your words with an AI reconstruction. Notes based on your dictation
78
+ remain marked `source: human`; notes drafted by Pi are marked `source: agent`.
79
+
80
+ Useful requests include:
81
+
82
+ | Say this | What happens |
83
+ |---|---|
84
+ | “Start a note about…” | Creates a Markdown note in the vault |
85
+ | “Add this to the note” | Finds the existing note and appends to it |
86
+ | “Clean up” or “finalize this note” | Reorganizes the readable body and preserves the raw tail |
87
+ | “What did we decide about…?” | Searches the vault, then reads the relevant notes |
88
+ | “Remember that…” | Stores durable knowledge for a future session |
89
+
90
+ ## `/weave-view`
76
91
 
77
92
  ```bash
78
- /weave-view # browser workspace (default), opens a tab
79
- /weave-view --no-open # same server, just prints the URL
80
- /weave-view tui # the in-terminal explorer instead
93
+ /weave-view # open the browser workspace
94
+ /weave-view --no-open # start it and print the URL
95
+ /weave-view tui # terminal UI for SSH or browser-free use
81
96
  ```
82
97
 
83
- Three resizable columns and a context rail:
98
+ The browser workspace has four connected views:
99
+
100
+ - **Tree** — notes, folders, and repository structure, with text and provenance filters.
101
+ - **Note** — rendered Markdown with clickable `[[wikilinks]]`, link previews, tags, authorship, and an action to open the source in `$EDITOR`.
102
+ - **Graph** — a navigable map of notes, links, mentions, modules, and repository relationships. Selecting something updates every view.
103
+ - **Context** — links, backlinks, tags, and code mentions for the current selection.
104
+
105
+ Search with `⌘K` / `Ctrl K`. Press `?` for all shortcuts. The workspace updates as notes change on disk, so a note written by Pi appears
106
+ without a reload. It follows the system theme by default and can be switched between light and dark.
84
107
 
85
- - **Tree** an expandable containment tree over notes and repository structure, with a filter box and provenance cycling.
86
- - **Note** — the selected note rendered with [marked](https://marked.js.org) and sanitised with DOMPurify. `[[wikilinks]]` navigate inside
87
- the workspace; links with no target render as ghosts rather than dead text.
88
- - **Graph** — [sigma.js](https://www.sigmajs.org) v3 on WebGL with a [d3-force](https://d3js.org/d3-force) layout: neighbourhood highlight
89
- on selection, semantic zoom that reveals labels as you go in, and cluster collapse as real graph reduction rather than hiding.
90
- - **Context rail** — links, backlinks, tags and mentions for whatever is selected, every entry clickable.
108
+ The browser is read-only. Edit with `$EDITOR`, Obsidian, or the `weave_note` tool; unknown front-matter fields remain preserved.
91
109
 
92
- Selecting anywhere highlights everywhere: the tree, the note body, the graph and the rail are lenses onto one selection. Updates arrive live
93
- over SSE as files change on disk, so an agent writing a note shows up without a refresh.
110
+ `/weave-view tui` is the smaller, read-only terminal explorer: tree, focused neighborhood, details, and link health over the same graph.
94
111
 
95
- `⌘K` opens a search palette spanning both faces (notes ranked with snippets, repository nodes by label). The whole workspace is keyboard
96
- drivable — `⌘1/2/3` focus a column, `/` filters the tree, `g` fits the graph, `Esc` clears, and `?` lists the rest. Column widths persist.
112
+ ## Repository knowledge
97
113
 
98
- The workspace is **read-first**, but no longer read-only. `⌘E` toggles the note column between read and edit, `⌘S` saves, and every save
99
- carries the revision read at load — a stale one gets a `409` and a choice of reload, overwrite or keep editing. The draft is never silently
100
- discarded or clobbered: a remote change arriving over SSE for the note you are editing is recorded rather than applied, and comes back as
101
- that same `409` when you save.
114
+ Inside a Git repository, pi-weave detects whether `<repo>/.okf/` is missing, fresh, or stale.
102
115
 
103
- Front matter the engine does not own survives a browser save **byte-identically** — `aliases`, `cssclass` and a `tags:` block list all come
104
- back unchanged and in place, with `updated:` the only line a save moves. That is the P5 exit criterion, and
105
- `tests/web/editor.roundtrip.test.ts` drives it through the real client, over a real socket, into a real vault — it is what makes editing
106
- here safe alongside Obsidian.
116
+ ```bash
117
+ /weave-scan # fast structural index
118
+ /weave-scan deep # also summarize changed files with the active model
119
+ ```
107
120
 
108
- Rename and delete have routes, client functions and tests but **no UI**, deliberately: the vault has no trash, so the confirmation flow
109
- around a destructive button is a design decision rather than a wiring task. Notes are still authorable through the `weave_note` tool or by
110
- hand, and the note toolbar's "Open in $EDITOR" hands the file to yours. `/weave-view tui` is the read-only in-terminal explorer — the same
111
- model, a containment tree, a 1-hop focus view, node detail and a link-health surface, for when you are on the far end of an SSH session.
121
+ The light index covers languages, packages, modules, entry points, and Git state. A deep scan adds short per-file summaries and only
122
+ revisits files whose content changed.
112
123
 
113
- ### The local server
124
+ The repository index is a cache, not a source of truth. Delete `.okf`, scan again, and nothing important is lost. pi-weave excludes it
125
+ locally from Git by default.
114
126
 
115
- The workspace server is deliberately small and deliberately paranoid, because loopback is not an authorisation boundary — any local process
116
- can reach the port, and any website you visit can try to via DNS rebinding. Four layers:
127
+ ## Commands and tools
117
128
 
118
- 1. Binds `127.0.0.1` on an ephemeral port. Never `0.0.0.0`.
119
- 2. A `Host` header allowlist (`127.0.0.1:PORT`, `localhost:PORT`, `[::1]:PORT`), which is what actually stops rebinding.
120
- 3. A 256-bit per-session token, handed off once in the URL and exchanged for an `HttpOnly; SameSite=Strict` cookie via a redirect that drops
121
- it from the address bar. Compared in constant time.
122
- 4. `Origin` validated when present, and required on anything that is not a `GET` or `HEAD`.
129
+ Most people only need natural language and `/weave-view`.
123
130
 
124
- The page is served under a nonce-only CSP — `default-src 'none'`, no `unsafe-inline`, no `unsafe-eval`, no CORS headers at all. The server
125
- shuts itself down after 30 minutes with no client attached, and always at the end of the pi session.
131
+ | Surface | Name | Purpose |
132
+ |---|---|---|
133
+ | Command | `/weave-view` | Open the browser or terminal workspace |
134
+ | Command | `/weave` | Show vault and repository status |
135
+ | Command | `/weave-scan` | Build or refresh the repository index |
136
+ | Command | `/weave-scan deep` | Add incremental model-written file summaries |
137
+ | Command | `/weave-scan-cancel` | Stop a deep scan |
138
+ | Tool | `weave_note` | List, read, add, append, finalize, and search notes |
139
+ | Tool | `weave_repo` | Check, scan, and summarize the repository index |
126
140
 
127
- ## The formats
141
+ The included `weave-notepad` and `weave-explore` skills teach Pi when and how to use these tools.
128
142
 
129
- Vault note (`~/.okf/notes/auth-boundary.md`):
143
+ ## Files, privacy, and portability
130
144
 
131
- ```markdown
145
+ Personal notes are ordinary Markdown files:
146
+
147
+ ```text
148
+ ~/.okf/
149
+ └── notes/
150
+ ├── authentication-migration.md
151
+ └── release-plan.md
152
+ ```
153
+
154
+ A note has small YAML front matter followed by Markdown:
155
+
156
+ ````markdown
132
157
  ---
133
- title: Auth boundary decision
158
+ title: Authentication migration
134
159
  created: 2026-08-22T09:00:00.000Z
135
160
  updated: 2026-08-22T09:30:00.000Z
136
161
  tags: [auth, security]
137
162
  source: human
138
163
  ---
139
164
 
140
- JWT validation happens at the gateway because…
141
- ```
142
-
143
- Repository index (`<repo>/.okf/`):
165
+ ## Summary
144
166
 
145
- ```text
146
- .okf/
147
- ├── okf.json # format version + generator + source: generated
148
- └── repository/
149
- ├── identity.json # name, remotes, default branch
150
- ├── git.json # HEAD sha + branch + changed-file content hashes (staleness anchor)
151
- └── structure.json # languages, packages, modules, entry points
152
- ```
167
+ Move toward OIDC while keeping a JWT compatibility window.
153
168
 
154
- The `.okf` index is **derived**: delete it, rescan, lose nothing. By default it is excluded from git locally (`.git/info/exclude`);
155
- committing it to share with a team is a deliberate opt-in. The vault location can be overridden with `PI_WEAVE_VAULT`.
156
-
157
- ## Zero runtime dependencies
158
-
159
- `package.json` declares no `dependencies`. The four peers (`@earendil-works/pi-ai`, `@earendil-works/pi-coding-agent`,
160
- `@earendil-works/pi-tui`, `typebox`) are supplied by the pi harness, which loads `src/pi/index.ts` as TypeScript directly — installing
161
- pi-weave runs no build step.
169
+ ---
162
170
 
163
- The browser client cannot work that way, so preact, sigma, graphology, d3-force, marked and DOMPurify are **devDependencies** bundled into a
164
- committed artifact at `src/web/client/dist/app.js`. They are inputs to a build, not runtime requirements of the package.
171
+ ## Raw
172
+ <!-- NEVER edit below this line. Verbatim user input preserved here. -->
165
173
 
166
- ## For other agent harnesses
174
+ ```
175
+ We probably want OIDC next quarter…
176
+ ```
177
+ ````
167
178
 
168
- The skills follow the [Agent Skills standard](https://agentskills.io/specification), and the on-disk artefacts and `src/core` are
169
- harness-agnostic by design: `src/core` may not import anything pi-specific. Claude Code and opencode adapters are on the roadmap
170
- ([docs/design.md](docs/design.md) §21).
179
+ Set `PI_WEAVE_VAULT` to use a different vault location.
171
180
 
172
- ## Documentation
181
+ Reading, writing, searching, and viewing notes are local operations. Deep repository scans send bounded input to whichever model you
182
+ configured in pi. The browser workspace binds only to loopback, uses a per-session token, and shuts down with the pi session.
173
183
 
174
- | Where | What |
175
- |---|---|
176
- | [docs/design.md](docs/design.md) | the design document — *why* pi-weave is shaped this way |
177
- | [docs/weave-workspace.md](docs/weave-workspace.md) | the browser workspace: library choices with measurements, security model, phases |
178
- | [docs/weave-view-tui-design.md](docs/weave-view-tui-design.md) | the in-terminal explorer |
179
- | [AGENTS.md](AGENTS.md) | contributor and agent rules — read before changing anything |
184
+ The vault format, repository index, and skills are intentionally harness-agnostic. `src/core` contains no pi-specific imports.
180
185
 
181
186
  ## Development
182
187
 
183
188
  ```bash
184
189
  npm install
185
- npm run typecheck # tsc --noEmit, strict, both projects
186
- npm test # vitest run
187
- npm run coverage # the 95% gate (lines, branches, functions, statements)
188
- npm run build:web # rebuild the committed browser bundle
189
- npm run check # typecheck + bundle drift check + coverage — run this before committing
190
+ npm run check
190
191
  ```
191
192
 
192
- Two rules worth knowing before you send a patch. Coverage must stay at or above **95%** on every metric; the gate is enforced by vitest
193
- thresholds and `npm run check` fails below it. And the committed web bundle must match its source — `npm run check` rebuilds it in memory
194
- and byte-compares, so run `npm run build:web` and commit the result whenever you touch `src/web/`.
193
+ Useful individual commands:
194
+
195
+ ```bash
196
+ npm run typecheck
197
+ npm test
198
+ npm run coverage
199
+ npm run build:web
200
+ ```
201
+
202
+ Coverage must remain at or above **95%** for lines, branches, functions, and statements. If browser source changes, rebuild and commit
203
+ `src/web/client/dist/app.js`.
204
+
205
+ Read [AGENTS.md](AGENTS.md) before contributing. Work on a feature branch; do not commit directly to `main`.
206
+
207
+ ## More detail
195
208
 
196
- Never commit to `main`; branch, then open a PR. See [AGENTS.md](AGENTS.md) for the rest.
209
+ - [Design](docs/design.md) product and architecture
210
+ - [Notepad skill](skills/weave-notepad/SKILL.md) — capture, narration, and provenance behavior
211
+ - [Historical browser workspace notes](docs/weave-workspace.md) — superseded implementation record
212
+ - [Repository exploration skill](skills/weave-explore/SKILL.md) — how Pi uses the index
197
213
 
198
- ## Licence
214
+ ## License
199
215
 
200
- [MIT](LICENSE).
216
+ [MIT](LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-weave",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
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,
@@ -64,7 +64,6 @@
64
64
  "@earendil-works/pi-ai": "^0.84.2",
65
65
  "@earendil-works/pi-coding-agent": "^0.84.2",
66
66
  "@earendil-works/pi-tui": "^0.84.2",
67
- "@preact/signals": "^2.11.1",
68
67
  "@types/d3-force": "^3.0.10",
69
68
  "@types/node": "^24.0.0",
70
69
  "@vitest/coverage-v8": "^3.2.4",
@@ -33,7 +33,7 @@ import { buildGraph, DEFAULT_MAX_NOTES, type BuildGraphInput } from "../graph/bu
33
33
  import type { GraphModel } from "../graph/model";
34
34
  import { readRepositorySide } from "../graph/current";
35
35
  import { withMutationQueue } from "../mutex";
36
- import { getNote, statNotes } from "../vault";
36
+ import { getNote, listNoteFolders, statNotes } from "../vault";
37
37
  import type { Note } from "../types";
38
38
 
39
39
  /**
@@ -136,11 +136,16 @@ function classify(
136
136
  ): { scope: InvalidationScope; slug: string | null } {
137
137
  const path = resolve(absPath);
138
138
  const rel = relative(resolve(opts.vaultRoot, NOTES_DIR), path);
139
- // Directly inside the notes dir (flat vault: no separator in the relative
140
- // path) and Markdown — anything else in there is not a note.
141
- if (rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel) && !rel.includes(sep)) {
142
- if (rel.endsWith(".md")) return { scope: "vault", slug: rel.slice(0, -".md".length) };
143
- return { scope: "none", slug: null };
139
+ if (rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel)) {
140
+ if (rel.endsWith(".md")) {
141
+ const slug = rel.slice(0, -".md".length).split(sep).join("/");
142
+ return { scope: "vault", slug };
143
+ }
144
+ const base = rel.split(sep).pop() ?? rel;
145
+ if (base.startsWith(".") || (base.includes(".") && !base.endsWith(".md"))) {
146
+ return { scope: "none", slug: null };
147
+ }
148
+ return { scope: "vault", slug: null };
144
149
  }
145
150
  return { scope: within(path, resolve(opts.cwd)) ? "repo" : "none", slug: null };
146
151
  }
@@ -179,6 +184,7 @@ export class WorkspaceCache {
179
184
  * matches the uncached build exactly.
180
185
  */
181
186
  private fileCount = 0;
187
+ private folders: string[] = [];
182
188
  private repo: CachedRepo | null = null;
183
189
  /**
184
190
  * The last snapshot handed out, reused verbatim when a build proves nothing
@@ -368,7 +374,12 @@ export class WorkspaceCache {
368
374
  // graph has no node for (§4.3).
369
375
  const kept = notes.slice(0, DEFAULT_MAX_NOTES);
370
376
  const input: BuildGraphInput = {
371
- vault: { root: this.vaultRoot, exists: true, noteCount: this.fileCount },
377
+ vault: {
378
+ root: this.vaultRoot,
379
+ exists: true,
380
+ noteCount: this.fileCount,
381
+ ...(this.folders.length > 0 ? { folders: this.folders } : {}),
382
+ },
372
383
  notes: kept,
373
384
  repository: repo?.repository ?? null,
374
385
  };
@@ -418,7 +429,9 @@ export class WorkspaceCache {
418
429
  * that disappeared are evicted, so the map never outgrows the vault.
419
430
  */
420
431
  private async refreshNotes(): Promise<Note[]> {
421
- const stats = await statNotes(this.vaultRoot);
432
+ const previousFolders = this.folders;
433
+ const [stats, folders] = await Promise.all([statNotes(this.vaultRoot), listNoteFolders(this.vaultRoot)]);
434
+ this.folders = folders;
422
435
  const previousCount = this.notes.size;
423
436
  const previousFileCount = this.fileCount;
424
437
  this.fileCount = stats.length;
@@ -446,7 +459,11 @@ export class WorkspaceCache {
446
459
  // A note vanished if the map shrank without a compensating read; the
447
460
  // file count moving covers a malformed file appearing or disappearing,
448
461
  // which changes the vault node's `notes` detail without ever parsing.
449
- this.notesChanged = read > 0 || next.size !== previousCount || this.fileCount !== previousFileCount;
462
+ const foldersChanged =
463
+ folders.length !== previousFolders.length ||
464
+ folders.some((f, i) => f !== previousFolders[i]);
465
+ this.notesChanged =
466
+ read > 0 || next.size !== previousCount || this.fileCount !== previousFileCount || foldersChanged;
450
467
  this.notes = next;
451
468
  // `statNotes` yields readdir (slug-ascending) order and sort is stable,
452
469
  // so ties break by slug — identical to `readVault`.
@@ -1,10 +1,7 @@
1
1
  /**
2
- * Bounded-concurrency task runner shared by the deep scan (summaries.ts) and
3
- * the session scan (sessions.ts).
2
+ * Bounded-concurrency task runner shared by repository scans.
4
3
  *
5
- * Extracted from summaries.ts so the two scanners cannot drift: a second copy
6
- * of the scheduler is a second place for an off-by-one or a lost
7
- * cancellation check.
4
+ * Keeping the scheduler here gives every scan the same cancellation behavior.
8
5
  */
9
6
 
10
7
  /**
@@ -33,4 +30,4 @@ export async function mapWithConcurrency<T, R>(
33
30
  }
34
31
  await Promise.all(Array.from({ length: Math.max(1, concurrency) }, worker));
35
32
  return results;
36
- }
33
+ }
@@ -289,59 +289,6 @@ export function parseFrontMatter(text: string): ParsedFrontMatter | null {
289
289
  return { fields, body, lines };
290
290
  }
291
291
 
292
- /**
293
- * Upsert owned scalar fields into a front-matter block, preserving order.
294
- *
295
- * For each wanted key: the **first** line declaring it is replaced with a
296
- * fresh `key: value` line in place, and any later declarations are dropped —
297
- * mirroring how duplicate managed keys collapse in `replayBlock` (the
298
- * subset parser keeps the last occurrence, so collapsing to one line with
299
- * the fresh value is the consistent outcome). A key introducing a block
300
- * construct (`scalar: false`) is replaced too: the value this function writes
301
- * is a scalar, and leaving the old block head in place would orphan its
302
- * indented children under a duplicated key.
303
- *
304
- * Wanted keys the block never declared are appended at the end, in the order
305
- * given. Everything else — unknown keys, blank lines, junk — is carried
306
- * through byte-identically, the same round-trip contract `serializeNote`
307
- * honors for the note engine's writes.
308
- *
309
- * Keys arrive from pi-weave's own generated-note writers (`session_id`,
310
- * `session_hash`, …); there is no escaping for the *key* because a key is
311
- * caller-controlled code, not user input — `quoteField` guards the value.
312
- */
313
- export function upsertFrontMatterFields(
314
- lines: NoteFrontMatter,
315
- fields: Record<string, string>,
316
- ): NoteFrontMatter {
317
- const wanted = new Set(Object.keys(fields));
318
- const out: string[] = [];
319
- const written = new Set<string>();
320
- let inDroppedBlock = false;
321
- for (const line of scanFrontMatter(lines)) {
322
- // Continuation lines of a block construct whose head we replaced: their
323
- // parent key is gone, so carrying them would leave orphaned YAML children
324
- // under a scalar. The block ends at the first non-indented line.
325
- if (inDroppedBlock) {
326
- if (/^\s/.test(line.text)) continue;
327
- inDroppedBlock = false;
328
- }
329
- if (line.key !== null && wanted.has(line.key)) {
330
- if (!written.has(line.key)) {
331
- out.push(`${line.key}: ${quoteField(fields[line.key] ?? "")}`);
332
- written.add(line.key);
333
- if (!line.scalar) inDroppedBlock = true; // swallow the block body too
334
- }
335
- continue; // later duplicates collapse into the first occurrence
336
- }
337
- out.push(line.text);
338
- }
339
- for (const [key, value] of Object.entries(fields)) {
340
- if (!written.has(key)) out.push(`${key}: ${quoteField(value)}`);
341
- }
342
- return out;
343
- }
344
-
345
292
  /**
346
293
  * Parse a note file. Throws on missing/invalid front matter so callers can
347
294
  * treat the file as malformed rather than guessing.
@@ -2,7 +2,7 @@
2
2
  * Pure graph builder: knowledge workspace inputs → GraphModel.
3
3
  *
4
4
  * No I/O, no clock access, no harness imports (design §21,
5
- * docs/weave-view.md §3). Stability contract: identical inputs produce
5
+ * docs/design.md §11). Stability contract: identical inputs produce
6
6
  * byte-identical JSON (ids derive from slugs/paths only; `generatedAt` is
7
7
  * derived from input timestamps, never from the wall clock) — that is what
8
8
  * makes the page's refresh-polling cheap.
@@ -15,7 +15,7 @@ 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 (docs/weave-view.md M3 guard). */
18
+ /** Hard cap on note nodes so a large vault cannot overwhelm the viewer. */
19
19
  export const DEFAULT_MAX_NOTES = 500;
20
20
 
21
21
  export interface BuildGraphInput {
@@ -133,14 +133,18 @@ function buildVaultSide(
133
133
 
134
134
  const keptSlugs = new Set(kept.map((n) => n.slug));
135
135
 
136
- // Nested notes (`sessions/foo` session memory, docs/session-scan.md) nest
137
- // under synthesized folder nodes so the vault tree groups them the way the
138
- // repository tree groups directories. Ids are prefixed `vfolder:` because a
139
- // repository module could legitimately share the path (`module:sessions`);
136
+ // Nested notes nest under synthesized folder nodes so the vault tree groups
137
+ // them the way the repository tree groups directories. Ids are prefixed
138
+ // `vfolder:` because a repository module could legitimately share the path;
140
139
  // the tree renders any `contains` chain, so the kind reuse needs no client
141
140
  // change. Deterministic: dirs sorted, parents before children.
142
141
  const folderIds = new Map<string, string>();
143
- const noteDirs = [...new Set(kept.map((n) => n.slug.split("/").slice(0, -1).join("/")))]
142
+ const noteDirs = [
143
+ ...new Set([
144
+ ...(input.vault.folders ?? []),
145
+ ...kept.map((n) => n.slug.split("/").slice(0, -1).join("/")),
146
+ ]),
147
+ ]
144
148
  .filter((d) => d.length > 0)
145
149
  .sort();
146
150
  const notesIn = (dir: string): number => kept.filter((n) => n.slug.startsWith(`${dir}/`)).length;
@@ -315,7 +319,7 @@ function buildRepositorySide(
315
319
 
316
320
  /**
317
321
  * Build the graph model for the viewer. Notes are capped at `maxNotes`
318
- * (docs/weave-view.md M3); the vault node carries a warning when truncated.
322
+ * (docs/design.md §11); the vault node carries a warning when truncated.
319
323
  */
320
324
  export function buildGraph(input: BuildGraphInput, options: { maxNotes?: number } = {}): GraphModel {
321
325
  const maxNotes = options.maxNotes ?? DEFAULT_MAX_NOTES;
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Workspace assembly readers for the viewers (docs/weave-view.md §2,
3
- * weave-view-tui-design §3.1).
2
+ * Workspace assembly readers for the viewers (docs/design.md §11).
4
3
  *
5
4
  * These functions are *workspace assembly*, symmetric to `getWorkspaceStatus`
6
5
  * (already in core): pure fan-out over `core/vault`, `core/repoIndex`,
@@ -92,7 +91,7 @@ export async function readRepositorySide(
92
91
 
93
92
  /**
94
93
  * Assemble the fresh graph from disk. Called on every viewer fetch
95
- * (no caching — docs/weave-view.md §2). Reads the vault (capped at
94
+ * (no caching). Reads the vault (capped at
96
95
  * DEFAULT_MAX_NOTES) and, when cwd is an indexed git repository, the repo
97
96
  * index + deep-scan summary sidecars. Degrades to a vault-only graph when
98
97
  * the repo has no index or the index is corrupt.
@@ -102,10 +101,10 @@ export async function readRepositorySide(
102
101
  * a third readdir) is now N reads and one readdir (weave-workspace §4.1).
103
102
  */
104
103
  export async function buildCurrentGraph(cwd: string, vaultRoot: string = resolveVaultRoot()): Promise<GraphModel> {
105
- const { notes, fileCount } = await readVault(vaultRoot);
104
+ const { notes, fileCount, folders } = await readVault(vaultRoot);
106
105
 
107
106
  const input: BuildGraphInput = {
108
- vault: { root: vaultRoot, exists: true, noteCount: fileCount },
107
+ vault: { root: vaultRoot, exists: true, noteCount: fileCount, ...(folders ? { folders } : {}) },
109
108
  notes: notes.slice(0, DEFAULT_MAX_NOTES),
110
109
  repository: null,
111
110
  };
@@ -117,4 +116,3 @@ export async function buildCurrentGraph(cwd: string, vaultRoot: string = resolve
117
116
  }
118
117
  return buildGraph(input);
119
118
  }
120
-
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Graph model shared between the builder (core) and the viewer (adapter).
3
- * Pure data — no harness imports (design §21). See docs/weave-view.md §3.
3
+ * Pure data — no harness imports (design §21).
4
4
  */
5
5
 
6
6
  import type { NoteSource, StalenessReport } from "../types";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Obsidian-compatible [[wiki-link]] extraction from note bodies.
3
- * Pure module see docs/weave-view.md §3.
3
+ * Pure module used by graph construction and viewer view-models.
4
4
  */
5
5
 
6
6
  import { slugify } from "../slug";
@@ -14,8 +14,8 @@ const WIKILINK_RE = /\[\[([^\][|]+)(?:\|[^\]]*)?\]\]/g;
14
14
  * `release-plan`. Duplicates are removed, order of first appearance kept.
15
15
  *
16
16
  * Path separators survive: a nested note's slug is its path relative to
17
- * `notes/` (`sessions/foo`), so `[[sessions/foo]]` targets the session note,
18
- * not a flattened name. Each path segment is slugified independently.
17
+ * `notes/`, so links target the nested note rather than a flattened name.
18
+ * Each path segment is slugified independently.
19
19
  */
20
20
  export function extractWikilinks(body: string): string[] {
21
21
  const out: string[] = [];