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.
- package/README.md +148 -132
- package/package.json +1 -2
- package/src/core/cache/workspace.ts +26 -9
- package/src/core/concurrency.ts +3 -6
- package/src/core/frontmatter.ts +0 -53
- package/src/core/graph/build.ts +12 -8
- package/src/core/graph/current.ts +4 -6
- package/src/core/graph/model.ts +1 -1
- package/src/core/graph/wikilinks.ts +3 -3
- package/src/core/index.ts +26 -27
- package/src/core/paths.ts +0 -7
- package/src/core/slug.ts +13 -0
- package/src/core/types.ts +1 -0
- package/src/core/vault.ts +45 -467
- package/src/core/view/detail.ts +1 -1
- package/src/core/view/health.ts +1 -1
- package/src/core/view/tree.ts +15 -3
- package/src/pi/index.ts +6 -85
- package/src/pi/summarize.ts +2 -2
- package/src/pi/viewer/tui/bodyStore.ts +4 -7
- package/src/pi/viewer/tui/branding.ts +7 -148
- package/src/pi/viewer/tui/run.ts +3 -17
- package/src/pi/viewer/tui/surface/base.ts +24 -3
- package/src/pi/viewer/tui/surface/explore.ts +41 -6
- package/src/pi/viewer/tui/workspace.ts +23 -351
- package/src/pi/viewer/tui/workspaceRoot.ts +31 -172
- package/src/pi/viewer/web/run.ts +7 -117
- package/src/web/client/api.dom.ts +2 -2
- package/src/web/client/api.ts +14 -176
- package/src/web/client/bootstrap.ts +5 -14
- package/src/web/client/context/context.model.ts +9 -11
- package/src/web/client/dist/app.js +77 -167
- package/src/web/client/graph/dynamics.ts +5 -65
- package/src/web/client/graph/renderer.dom.ts +7 -8
- package/src/web/client/graph/renderer.ts +9 -35
- package/src/web/client/main.tsx +1 -1
- package/src/web/client/note/Note.tsx +37 -60
- package/src/web/client/note/note.model.ts +23 -0
- package/src/web/client/search/SearchPalette.tsx +45 -36
- package/src/web/client/search/search.model.ts +33 -454
- package/src/web/client/shell/Columns.tsx +13 -81
- package/src/web/client/shell/Header.tsx +2 -10
- package/src/web/client/shell/Shell.tsx +50 -123
- package/src/web/client/shell/StatusBar.tsx +1 -4
- package/src/web/client/shell/icons.model.ts +4 -7
- package/src/web/client/shell/keys.model.ts +5 -42
- package/src/web/client/shell/keys.ts +2 -2
- package/src/web/client/shell/shell.model.ts +10 -133
- package/src/web/client/shell/theme.model.ts +2 -2
- package/src/web/client/shell/theme.ts +33 -121
- package/src/web/client/state.ts +9 -89
- package/src/web/client/tree/Tree.tsx +24 -97
- package/src/web/client/tree/tree.model.ts +8 -56
- package/src/web/client/workspace.ts +72 -242
- package/src/web/server/page.ts +8 -10
- package/src/web/server/routes.ts +30 -415
- package/src/web/server/server.ts +6 -145
- package/src/web/shared/layout.ts +72 -624
- package/src/web/shared/wire.ts +10 -178
- package/src/core/sessions.ts +0 -929
- package/src/pi/sessionScan.ts +0 -104
- package/src/pi/viewer/tui/explorer.ts +0 -586
- package/src/web/client/live.model.ts +0 -275
- package/src/web/client/live.ts +0 -151
- package/src/web/client/note/Editor.tsx +0 -109
- package/src/web/client/note/editor.controller.ts +0 -151
- package/src/web/client/note/editor.model.ts +0 -686
- package/src/web/client/search/search.ts +0 -107
- package/src/web/client/shell/Divider.tsx +0 -44
- package/src/web/client/shell/cssvars.ts +0 -70
- package/src/web/client/shell/drag.model.ts +0 -170
- package/src/web/client/shell/layout.model.ts +0 -500
- package/src/web/client/shell/viewport.ts +0 -29
- package/src/web/server/sse.ts +0 -321
- 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
|
|
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
|
-
**
|
|
14
|
+
**A local knowledge workspace you can talk to.**
|
|
15
15
|
|
|
16
|
-
pi-weave is
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
+
Requires Node **20.13 or newer**.
|
|
35
48
|
|
|
36
|
-
##
|
|
49
|
+
## Start taking notes
|
|
37
50
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
|
57
|
-
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
64
|
-
`/weave-
|
|
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
|
|
79
|
-
/weave-view --no-open #
|
|
80
|
-
/weave-view tui #
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
116
|
+
```bash
|
|
117
|
+
/weave-scan # fast structural index
|
|
118
|
+
/weave-scan deep # also summarize changed files with the active model
|
|
119
|
+
```
|
|
107
120
|
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
141
|
+
The included `weave-notepad` and `weave-explore` skills teach Pi when and how to use these tools.
|
|
128
142
|
|
|
129
|
-
|
|
143
|
+
## Files, privacy, and portability
|
|
130
144
|
|
|
131
|
-
|
|
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:
|
|
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
|
-
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Repository index (`<repo>/.okf/`):
|
|
165
|
+
## Summary
|
|
144
166
|
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
171
|
+
## Raw
|
|
172
|
+
<!-- NEVER edit below this line. Verbatim user input preserved here. -->
|
|
165
173
|
|
|
166
|
-
|
|
174
|
+
```
|
|
175
|
+
We probably want OIDC next quarter…
|
|
176
|
+
```
|
|
177
|
+
````
|
|
167
178
|
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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.
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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: {
|
|
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
|
|
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
|
-
|
|
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`.
|
package/src/core/concurrency.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bounded-concurrency task runner shared by
|
|
3
|
-
* the session scan (sessions.ts).
|
|
2
|
+
* Bounded-concurrency task runner shared by repository scans.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
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
|
+
}
|
package/src/core/frontmatter.ts
CHANGED
|
@@ -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.
|
package/src/core/graph/build.ts
CHANGED
|
@@ -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/
|
|
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
|
|
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
|
|
137
|
-
//
|
|
138
|
-
//
|
|
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 = [
|
|
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/
|
|
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/
|
|
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
|
|
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
|
-
|
package/src/core/graph/model.ts
CHANGED
|
@@ -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).
|
|
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
|
|
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
|
|
18
|
-
*
|
|
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[] = [];
|