pi-weave 0.1.7 → 0.1.9
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 +123 -37
- package/package.json +17 -5
- package/skills/weave-notepad/SKILL.md +13 -4
- package/src/core/cache/workspace.ts +466 -0
- package/src/core/concurrency.ts +36 -0
- package/src/core/frontmatter.ts +270 -23
- package/src/core/git.ts +19 -0
- package/src/core/graph/build.ts +97 -5
- package/src/core/graph/current.ts +41 -28
- package/src/core/graph/mentions.ts +170 -0
- package/src/core/graph/model.ts +39 -0
- package/src/core/graph/wikilinks.ts +5 -1
- package/src/core/index.ts +14 -0
- package/src/core/openInEditor.ts +69 -0
- package/src/core/paths.ts +7 -0
- package/src/core/sessions.ts +929 -0
- package/src/core/summaries.ts +1 -19
- package/src/core/types.ts +40 -0
- package/src/core/vault.ts +739 -57
- package/src/core/view/cluster.ts +262 -0
- package/src/core/view/detail.ts +118 -0
- package/src/core/view/focus.ts +109 -0
- package/src/core/view/health.ts +156 -0
- package/src/core/view/index.ts +15 -0
- package/src/core/view/links.ts +105 -0
- package/src/core/view/time.ts +47 -0
- package/src/core/view/tree.ts +269 -0
- package/src/core/view/types.ts +39 -0
- package/src/core/workspace.ts +3 -3
- package/src/pi/index.ts +248 -48
- package/src/pi/sessionScan.ts +104 -0
- package/src/pi/summarize.ts +24 -4
- package/src/pi/tools/noteTool.ts +13 -4
- package/src/pi/viewer/tui/branding.ts +8 -7
- package/src/pi/viewer/tui/explorer.ts +5 -3
- package/src/pi/viewer/tui/model.ts +46 -667
- package/src/pi/viewer/tui/openNote.ts +7 -56
- package/src/pi/viewer/tui/surface/explore.ts +4 -2
- package/src/pi/viewer/web/run.ts +331 -0
- package/src/web/client/api.dom.ts +40 -0
- package/src/web/client/api.ts +472 -0
- package/src/web/client/bootstrap.ts +58 -0
- package/src/web/client/context/context.model.ts +313 -0
- package/src/web/client/dist/app.js +764 -0
- package/src/web/client/graph/Graph.tsx +209 -0
- package/src/web/client/graph/column.model.ts +372 -0
- package/src/web/client/graph/dynamics.ts +176 -0
- package/src/web/client/graph/graph.model.ts +546 -0
- package/src/web/client/graph/positions.ts +380 -0
- package/src/web/client/graph/project.ts +153 -0
- package/src/web/client/graph/renderer.dom.ts +52 -0
- package/src/web/client/graph/renderer.ts +339 -0
- package/src/web/client/graph/scheme.ts +44 -0
- package/src/web/client/live.model.ts +275 -0
- package/src/web/client/live.ts +151 -0
- package/src/web/client/main.tsx +27 -0
- package/src/web/client/note/Editor.tsx +102 -0
- package/src/web/client/note/Note.tsx +113 -0
- package/src/web/client/note/editor.controller.ts +151 -0
- package/src/web/client/note/editor.model.ts +636 -0
- package/src/web/client/note/note.model.ts +738 -0
- package/src/web/client/search/SearchPalette.tsx +105 -0
- package/src/web/client/search/search.model.ts +588 -0
- package/src/web/client/search/search.ts +107 -0
- package/src/web/client/selection.storage.ts +69 -0
- package/src/web/client/shell/Columns.tsx +161 -0
- package/src/web/client/shell/ContextRail.tsx +87 -0
- package/src/web/client/shell/Divider.tsx +44 -0
- package/src/web/client/shell/FocusTrap.tsx +56 -0
- package/src/web/client/shell/Header.tsx +64 -0
- package/src/web/client/shell/HelpOverlay.tsx +70 -0
- package/src/web/client/shell/Shell.tsx +210 -0
- package/src/web/client/shell/StatusBar.tsx +28 -0
- package/src/web/client/shell/cssvars.ts +70 -0
- package/src/web/client/shell/drag.model.ts +170 -0
- package/src/web/client/shell/focus.model.ts +100 -0
- package/src/web/client/shell/keys.model.ts +453 -0
- package/src/web/client/shell/keys.ts +59 -0
- package/src/web/client/shell/layout.model.ts +526 -0
- package/src/web/client/shell/shell.model.ts +333 -0
- package/src/web/client/shell/theme.ts +490 -0
- package/src/web/client/shell/viewport.ts +29 -0
- package/src/web/client/state.ts +89 -0
- package/src/web/client/tree/Tree.tsx +141 -0
- package/src/web/client/tree/tree.model.ts +674 -0
- package/src/web/client/workspace.ts +278 -0
- package/src/web/server/page.ts +258 -0
- package/src/web/server/routes.ts +987 -0
- package/src/web/server/security.ts +361 -0
- package/src/web/server/server.ts +275 -0
- package/src/web/server/sse.ts +321 -0
- package/src/web/server/watcher.ts +507 -0
- package/src/web/shared/graph.ts +213 -0
- package/src/web/shared/layout.ts +314 -0
- package/src/web/shared/logo.ts +11 -0
- package/src/web/shared/metrics.ts +136 -0
- package/src/web/shared/view.ts +200 -0
- package/src/web/shared/wire.ts +358 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser's legal door onto the core view-models (weave-workspace §3, §2.1).
|
|
3
|
+
*
|
|
4
|
+
* ## What this is
|
|
5
|
+
*
|
|
6
|
+
* `src/core/view/` holds the one implementation of the tree, detail and focus
|
|
7
|
+
* projections — the whole point of §3 being that the TUI and the browser
|
|
8
|
+
* workspace *cannot drift*, because there is nothing to drift from. The tier
|
|
9
|
+
* table (§2) forbids `src/web/client/**` from reaching `src/core`, transitively
|
|
10
|
+
* included, so without a door the client's only options were to re-implement
|
|
11
|
+
* those projections or to have the server pre-render them. Both defeat §3.
|
|
12
|
+
*
|
|
13
|
+
* This module is the door. It contains **no logic**: every line below is an
|
|
14
|
+
* `export … from`, so the client and the TUI call the same function object
|
|
15
|
+
* defined in the same file. A wrapper would be a second implementation with a
|
|
16
|
+
* second set of edge cases, which is the failure mode §3 exists to prevent.
|
|
17
|
+
*
|
|
18
|
+
* ## Why this is allowed to import `src/core` when `wire.ts` is not
|
|
19
|
+
*
|
|
20
|
+
* §2.1 records a hard-won rule: *nothing under `src/web/shared/` imports
|
|
21
|
+
* `src/core`, not even as a type*. That rule was written after
|
|
22
|
+
* `import type { GraphModel }` in `wire.ts` broke `tsc -p tsconfig.web.json`
|
|
23
|
+
* with 24 `Cannot find module 'node:fs'` errors — because type erasure happens
|
|
24
|
+
* in the bundler and not in the typechecker, so the compiler still walked the
|
|
25
|
+
* whole Node-flavoured core type graph.
|
|
26
|
+
*
|
|
27
|
+
* The reasoning was correct about *that* closure. `src/core/graph/model.ts` was
|
|
28
|
+
* reached from a barrel whose neighbours read the filesystem. It was not a
|
|
29
|
+
* claim about core in general, and it is not true of this closure:
|
|
30
|
+
*
|
|
31
|
+
* ```text
|
|
32
|
+
* shared/view.ts
|
|
33
|
+
* → core/view/{tree,detail,focus,links,time,types}.ts
|
|
34
|
+
* → core/graph/model.ts → core/types.ts
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* Eleven modules, zero `node:*` imports, zero Node globals, zero npm
|
|
38
|
+
* dependencies, all pure functions over plain data. They are genuinely
|
|
39
|
+
* browser-portable; only the blanket tier rule stood in the way.
|
|
40
|
+
*
|
|
41
|
+
* Two guards keep that a fact rather than an assumption, and they are what
|
|
42
|
+
* makes the exception sound rather than merely convenient:
|
|
43
|
+
*
|
|
44
|
+
* 1. `tests/web/view.purity.test.ts` walks the transitive closure reachable
|
|
45
|
+
* from this file and fails if any module in it acquires a `node:*` import,
|
|
46
|
+
* a Node global, or an npm dependency. Add `node:fs` to
|
|
47
|
+
* `src/core/view/tree.ts` and the suite goes red immediately — not silently
|
|
48
|
+
* at runtime in someone's browser.
|
|
49
|
+
* 2. `tsconfig.web.json` includes `src/web/shared/**`, so the same closure is
|
|
50
|
+
* typechecked with `"types": []` and a `lib` with no Node types. That is
|
|
51
|
+
* the check that caught the original 24 errors, now aimed here on purpose.
|
|
52
|
+
*
|
|
53
|
+
* The narrow, precise version of the §2.1 rule is therefore: *`src/web/shared/`
|
|
54
|
+
* may import the modules of `src/core` that are proven node-free, and nothing
|
|
55
|
+
* else.* `tests/web/tiers.test.ts` encodes that as a **module allowlist**, not
|
|
56
|
+
* a directory one — `core/vault.ts`, `core/git.ts`, `core/repoIndex.ts`,
|
|
57
|
+
* `core/summaries.ts`, `core/paths.ts`, `core/cache/` and
|
|
58
|
+
* `core/graph/current.ts` all remain forbidden here, because reopening the
|
|
59
|
+
* door as "shared may import core" would reopen the exact hole §2.1 closed.
|
|
60
|
+
*
|
|
61
|
+
* ## Why this is a small list
|
|
62
|
+
*
|
|
63
|
+
* Deliberately not `export * from "../../core/view"`. The barrel pulls in
|
|
64
|
+
* `health.ts` (a TUI surface) and `links.ts`'s tag index (derived server-side
|
|
65
|
+
* into `GraphPayload.tags`), neither of which any browser column consumes
|
|
66
|
+
* today — and each one it pulled in would be a module the purity guard then
|
|
67
|
+
* has to defend forever.
|
|
68
|
+
*
|
|
69
|
+
* So this is an allowlist, on the same principle as the `npm` column in
|
|
70
|
+
* `tests/web/tiers.test.ts`: the surface the columns actually use, and nothing
|
|
71
|
+
* more. **P3 added `focusNeighborhood`, `degreeOf` and `clusterAggregate`**,
|
|
72
|
+
* in the commit that first calls them, exactly as this paragraph promised —
|
|
73
|
+
* along with `src/core/view/cluster.ts` in `NODE_FREE_CORE_MODULES`, which is
|
|
74
|
+
* the reviewed edit that grants the permission. `cluster.ts`'s closure adds
|
|
75
|
+
* nothing new (it reaches only `graph/model.ts`, `types.ts` and `view/tree.ts`,
|
|
76
|
+
* all already permitted), so the node-free proof `view.purity.test.ts` runs is
|
|
77
|
+
* unchanged in kind.
|
|
78
|
+
*
|
|
79
|
+
* ## Wire types vs. view types
|
|
80
|
+
*
|
|
81
|
+
* The view-models were written against core's `GraphModel`, and the client
|
|
82
|
+
* holds `GraphPayload` from `./wire`. The two disagree by exactly one field:
|
|
83
|
+
* `WireGraphModel` is `Omit<GraphModel, "danglingLinks">`, because the payload
|
|
84
|
+
* hoists that map to its own top-level `dangling` rather than shipping it
|
|
85
|
+
* twice (§4.2). A caller therefore reassembles
|
|
86
|
+
* `{ ...payload.model, danglingLinks: payload.dangling }` before calling
|
|
87
|
+
* {@link treeRows} or {@link detailModel}. That reassembly is not done here on
|
|
88
|
+
* purpose: it is a wire concern with exactly one correct spelling, and putting
|
|
89
|
+
* it in this file would make the door carry a transformation instead of just
|
|
90
|
+
* being a door.
|
|
91
|
+
*
|
|
92
|
+
* The node and edge types need no such care. `WireGraphNode` / `WireGraphEdge`
|
|
93
|
+
* are asserted mutually assignable with their core counterparts by
|
|
94
|
+
* `tests/web/wire.contract.test.ts`, so a `WireGraphNode` may be passed to
|
|
95
|
+
* {@link listLabel} and a `WireGraphEdge[]` to {@link deriveBacklinks} with no
|
|
96
|
+
* cast. Likewise `WireNodeKind` / `WireNoteSource` are the same unions as
|
|
97
|
+
* core's `NodeKind` / `NoteSource`, which is why this module does not
|
|
98
|
+
* re-export a second name for either.
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
// --- tree column (§1.2, P2) ---------------------------------------------------
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The tree column *is* `treeRows` with a different renderer (§3).
|
|
105
|
+
*
|
|
106
|
+
* `listLabel` is exported alongside it because the browser has places that
|
|
107
|
+
* hold a node but not a row — a graph tooltip, a dangling-link ghost — and a
|
|
108
|
+
* second label rule for those is precisely the drift §3 forbids.
|
|
109
|
+
* `treeEmptyHint` is the tree's honest empty state, for the same reason.
|
|
110
|
+
*/
|
|
111
|
+
export { listLabel, treeEmptyHint, treeRows } from "../../core/view/tree";
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* `TreeState` here is **core's**, not the placeholder in
|
|
115
|
+
* `src/web/client/state.ts`: it carries the filter, the provenance cycle and
|
|
116
|
+
* the internals toggle that `treeRows` actually reads, where the client's
|
|
117
|
+
* P1 stand-in carries only an expanded-id list. P2 replaces the latter with
|
|
118
|
+
* this.
|
|
119
|
+
*/
|
|
120
|
+
export type { TreeRow, TreeState } from "../../core/view/tree";
|
|
121
|
+
|
|
122
|
+
// --- note column (§1.2, P2) ---------------------------------------------------
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* `relTime` renders the note header's "updated 12m ago"; `formatTreeMeta`
|
|
126
|
+
* renders a `TreeMeta` as plain text.
|
|
127
|
+
*
|
|
128
|
+
* A richer renderer is expected to switch on the {@link TreeMeta} union itself
|
|
129
|
+
* (a `<time>` element wants the ISO string, not a pre-formatted one), which is
|
|
130
|
+
* why the structured type comes across too and not only the formatter.
|
|
131
|
+
*/
|
|
132
|
+
export { formatTreeMeta, relTime } from "../../core/view/time";
|
|
133
|
+
|
|
134
|
+
// --- context rail (§1.2, P2) --------------------------------------------------
|
|
135
|
+
|
|
136
|
+
/** Links, backlinks and the ordered meta block for the selected node. */
|
|
137
|
+
export { detailModel } from "../../core/view/detail";
|
|
138
|
+
export type { DetailLinkRow, DetailMetaRow, DetailModel } from "../../core/view/detail";
|
|
139
|
+
|
|
140
|
+
/** "Related": the 1-hop neighborhood, grouped by relationship. */
|
|
141
|
+
export { focusModel } from "../../core/view/focus";
|
|
142
|
+
export type { FocusGroup, FocusModel, FocusRow } from "../../core/view/focus";
|
|
143
|
+
|
|
144
|
+
// --- graph column (§1.2, §7, P3) ------------------------------------------------
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The graph's neighborhood highlight (§7.4) and its node sizing.
|
|
148
|
+
*
|
|
149
|
+
* `focusNeighborhood` is the set sigma's `nodeReducer` dims *outside* of, and
|
|
150
|
+
* it must be the same set the context rail's "Related" is built from — a graph
|
|
151
|
+
* that highlights one neighbourhood while the rail lists another is two
|
|
152
|
+
* answers to one question, which is the drift §3 exists to prevent.
|
|
153
|
+
*
|
|
154
|
+
* `degreeOf` comes across alongside it because the graph column needs a
|
|
155
|
+
* *single* node's degree in one place (a tooltip, a collapsed cluster's
|
|
156
|
+
* badge). The bulk case — a degree for every node at once — is `degrees` in
|
|
157
|
+
* `client/graph/graph.model.ts`, which is O(edges) rather than
|
|
158
|
+
* O(nodes × edges); that is a different algorithm for a different question,
|
|
159
|
+
* not a second implementation of this one.
|
|
160
|
+
*/
|
|
161
|
+
export { degreeOf, focusNeighborhood } from "../../core/view/focus";
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Cluster collapse/expand, as **graph reduction** rather than masking (§7.4).
|
|
165
|
+
*
|
|
166
|
+
* Built in P0 and deliberately not a port of the retired viewer's function of
|
|
167
|
+
* the same name: that one computed a visibility mask, left every node in the
|
|
168
|
+
* graph, and never rewrote an edge — so a link from a hidden file to a visible
|
|
169
|
+
* note simply vanished. This returns reduced node and edge arrays with
|
|
170
|
+
* boundary-crossing edges retargeted onto the standing-in cluster, which is
|
|
171
|
+
* what sigma wants (§7.1: a graph it can consume verbatim) and what keeps the
|
|
172
|
+
* TUI tree and the web graph answering "what is visible?" identically.
|
|
173
|
+
*/
|
|
174
|
+
export { clusterAggregate } from "../../core/view/cluster";
|
|
175
|
+
export type { ClusterAggregate, ClusterInfo, ProvenanceSplit } from "../../core/view/cluster";
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Backlinks for the whole graph in one pass — the rail renders one node's, but
|
|
179
|
+
* the tree wants "does this note have any?" without an O(nodes × edges) scan.
|
|
180
|
+
*/
|
|
181
|
+
export { deriveBacklinks } from "../../core/view/links";
|
|
182
|
+
|
|
183
|
+
// --- shared row/meta types ----------------------------------------------------
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* `SelectableRow` is the base of `TreeRow`, `DetailLinkRow` and `FocusRow`, so
|
|
187
|
+
* a single "render a selectable row" component can be typed against it rather
|
|
188
|
+
* than against a union of three.
|
|
189
|
+
*/
|
|
190
|
+
export type { SelectableRow, TreeMeta } from "../../core/view/types";
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The input type every function above takes.
|
|
194
|
+
*
|
|
195
|
+
* Renamed rather than re-exported as `GraphModel`, because `./wire` already
|
|
196
|
+
* exports that name for the *wire* model and the two are one field apart (see
|
|
197
|
+
* the module header). Two different `GraphModel`s in one tier is a trap; a
|
|
198
|
+
* name that says which side of the boundary it belongs to is not.
|
|
199
|
+
*/
|
|
200
|
+
export type { GraphModel as ViewGraphModel } from "../../core/graph/model";
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire contract between the loopback server and the browser client
|
|
3
|
+
* (weave-workspace §5.3).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this file exists at all
|
|
6
|
+
*
|
|
7
|
+
* The client cannot import `src/core` (§2 tier table): core is
|
|
8
|
+
* Node-flavoured TypeScript and a value import would drag `node:fs` into the
|
|
9
|
+
* bundle. So everything both sides need is either a **type** here or a pure
|
|
10
|
+
* function in `src/web/shared/`. This module is the type half — it is the
|
|
11
|
+
* single place where the shape of an HTTP response or an SSE frame is
|
|
12
|
+
* written down, and both the server that produces it and the client that
|
|
13
|
+
* consumes it are typed from it.
|
|
14
|
+
*
|
|
15
|
+
* ## Tier rules
|
|
16
|
+
*
|
|
17
|
+
* `src/web/shared/**`: itself only. No `node:*`, no DOM globals, no
|
|
18
|
+
* `src/pi`, and — as of the tier fix — **no `src/core`, not even as a type**.
|
|
19
|
+
*
|
|
20
|
+
* That last clause is stricter than the §2 table as originally written, and
|
|
21
|
+
* it exists because "core types only" turned out not to be a real boundary.
|
|
22
|
+
* `import type` erases from the *bundle*, but the compiler still has to
|
|
23
|
+
* resolve it: pulling `GraphModel` in here made `tsc -p tsconfig.web.json`
|
|
24
|
+
* load the entire core type graph, `node:fs` and all, and fail with 24
|
|
25
|
+
* errors under a project that deliberately has no node lib. The type-only
|
|
26
|
+
* distinction is one the bundler makes and the typechecker does not.
|
|
27
|
+
*
|
|
28
|
+
* The DTOs now live in `./graph`, declared structurally, with
|
|
29
|
+
* `tests/web/wire.contract.test.ts` asserting they stay assignable to and
|
|
30
|
+
* from their core counterparts. Drift is a compile error; the client is free
|
|
31
|
+
* of core in every sense.
|
|
32
|
+
*
|
|
33
|
+
* ## The graph payload is not the core `GraphModel`
|
|
34
|
+
*
|
|
35
|
+
* The core model is lossy on purpose: tags are flattened into a comma-joined
|
|
36
|
+
* display string and dangling link targets are counted then discarded
|
|
37
|
+
* (weave-view handoff findings). {@link GraphPayload} therefore carries the
|
|
38
|
+
* model *plus* the two structured indexes the model dropped, rather than
|
|
39
|
+
* growing structure inside `WireGraphNode.detail`, which is display-only by
|
|
40
|
+
* contract.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import type { WireGraphModel, WireNoteSearchHit, WireNoteSource, WireViewNote } from "./graph";
|
|
44
|
+
import type { Point } from "./metrics";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The wire DTOs, re-exported under the names the client and server already
|
|
48
|
+
* use. Aliases rather than a second declaration, so there is exactly one
|
|
49
|
+
* definition of each shape and `./graph` remains the place drift is caught.
|
|
50
|
+
*/
|
|
51
|
+
export type GraphModel = WireGraphModel;
|
|
52
|
+
export type ViewNote = WireViewNote;
|
|
53
|
+
export type NoteSearchHit = WireNoteSearchHit;
|
|
54
|
+
export type { Point };
|
|
55
|
+
|
|
56
|
+
export type {
|
|
57
|
+
WireEdgeKind,
|
|
58
|
+
WireGraphEdge,
|
|
59
|
+
WireGraphModel,
|
|
60
|
+
WireGraphNode,
|
|
61
|
+
WireNodeKind,
|
|
62
|
+
WireNoteMeta,
|
|
63
|
+
WireNoteSearchHit,
|
|
64
|
+
WireNoteSource,
|
|
65
|
+
WireNoteSummary,
|
|
66
|
+
WireStalenessReport,
|
|
67
|
+
WireStalenessState,
|
|
68
|
+
WireViewNote,
|
|
69
|
+
} from "./graph";
|
|
70
|
+
export { WIRE_EDGE_KINDS, WIRE_MODEL_OMITTED_KEYS, WIRE_NODE_KINDS } from "./graph";
|
|
71
|
+
|
|
72
|
+
// --- liveness ----------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Which half of the workspace an SSE frame is about.
|
|
76
|
+
*
|
|
77
|
+
* Deliberately coarser than the paths that produced it: macOS `fs.watch`
|
|
78
|
+
* coalesces and can miss rapid bursts, so a frame means "something in this
|
|
79
|
+
* scope changed, re-read it", never "here is the delta" (§6). A client that
|
|
80
|
+
* treated frames as deltas would silently diverge the first time the OS
|
|
81
|
+
* dropped one.
|
|
82
|
+
*/
|
|
83
|
+
export type ChangeScope = "vault" | "repo" | "git";
|
|
84
|
+
|
|
85
|
+
/** Every {@link ChangeScope}, for exhaustiveness checks and table tests. */
|
|
86
|
+
export const CHANGE_SCOPES: readonly ChangeScope[] = ["vault", "repo", "git"];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* One SSE frame.
|
|
90
|
+
*
|
|
91
|
+
* `stamp` is the graph's content digest at the moment the change was observed
|
|
92
|
+
* — the same value `GET /api/graph` serves as its ETag (§5.3). It is the
|
|
93
|
+
* dedupe key: a client that already holds this stamp has nothing to refetch,
|
|
94
|
+
* which matters because the watcher's debounce window can still emit two
|
|
95
|
+
* frames for one logical edit.
|
|
96
|
+
*
|
|
97
|
+
* Sharing one key with the ETag is load-bearing rather than tidy. While this
|
|
98
|
+
* was `generatedAt`, an edit that did not advance the timestamp maximum
|
|
99
|
+
* produced a frame the client deduped away against the stamp of the graph it
|
|
100
|
+
* already held, so the refetch never happened (§15.6).
|
|
101
|
+
*/
|
|
102
|
+
export interface ChangeEvent {
|
|
103
|
+
scope: ChangeScope;
|
|
104
|
+
stamp: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The `event:` name carried by every {@link ChangeEvent} frame. */
|
|
108
|
+
export const CHANGE_EVENT_NAME = "change";
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Structural guard for a decoded SSE `data:` payload.
|
|
112
|
+
*
|
|
113
|
+
* The client parses frames off a socket that survives server restarts and
|
|
114
|
+
* proxy interference, so "it is JSON" is not the same as "it is ours".
|
|
115
|
+
* Narrow rather than validate-and-throw: a malformed frame should cost a
|
|
116
|
+
* skipped refetch, not an unhandled rejection inside `EventSource`'s
|
|
117
|
+
* callback.
|
|
118
|
+
*/
|
|
119
|
+
export function isChangeEvent(value: unknown): value is ChangeEvent {
|
|
120
|
+
if (typeof value !== "object" || value === null) return false;
|
|
121
|
+
const candidate = value as { scope?: unknown; stamp?: unknown };
|
|
122
|
+
if (typeof candidate.stamp !== "string") return false;
|
|
123
|
+
return CHANGE_SCOPES.includes(candidate.scope as ChangeScope);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --- responses ---------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* `GET /api/graph`.
|
|
130
|
+
*
|
|
131
|
+
* @see GraphModel for the authoritative node/edge data.
|
|
132
|
+
*/
|
|
133
|
+
export interface GraphPayload {
|
|
134
|
+
/** Nodes, edges, staleness, `generatedAt`. The authoritative model. */
|
|
135
|
+
model: GraphModel;
|
|
136
|
+
/**
|
|
137
|
+
* tag → slugs (§4.3).
|
|
138
|
+
*
|
|
139
|
+
* Built by core's `deriveTagIndex` from the notes the graph was built from
|
|
140
|
+
* — *not* by re-parsing `WireGraphNode.detail.tags`, which is a
|
|
141
|
+
* comma-joined display string and must never be turned back into
|
|
142
|
+
* structure.
|
|
143
|
+
*
|
|
144
|
+
* Key order is meaningful and load-bearing: `deriveTagIndex` returns count
|
|
145
|
+
* descending, then tag ascending, and `Object.fromEntries` preserves that
|
|
146
|
+
* as JSON key insertion order. A client that wants the ranking can either
|
|
147
|
+
* trust the order or re-derive it from the array lengths. A tag with no
|
|
148
|
+
* notes cannot occur.
|
|
149
|
+
*
|
|
150
|
+
* Slugs, not node ids — `note:` is a graph-internal prefix.
|
|
151
|
+
*/
|
|
152
|
+
tags: Record<string, string[]>;
|
|
153
|
+
/**
|
|
154
|
+
* slug → unresolved wikilink targets (§4.2).
|
|
155
|
+
*
|
|
156
|
+
* The ghost-node affordance: a `[[target]]` that matches no note is an
|
|
157
|
+
* offer to create one. Populated from core's `GraphModel.danglingLinks`,
|
|
158
|
+
* which the builder now retains instead of counting and discarding.
|
|
159
|
+
*
|
|
160
|
+
* Lives at the payload's top level rather than inside {@link GraphModel},
|
|
161
|
+
* which is why `WireGraphModel` is declared one field narrower than its
|
|
162
|
+
* core counterpart — the same map crossing the wire twice would give the
|
|
163
|
+
* client two places to read one fact from. A note with nothing unresolved
|
|
164
|
+
* is absent from the record, not present with an empty array.
|
|
165
|
+
*/
|
|
166
|
+
dangling: Record<string, string[]>;
|
|
167
|
+
/**
|
|
168
|
+
* Server-precomputed layout, so the graph appears already laid out with no
|
|
169
|
+
* visible settling (§7.3). `null` when the server did not compute one —
|
|
170
|
+
* which is the default, because the layout module imports `d3-force` and
|
|
171
|
+
* the published package has zero runtime dependencies. The client then
|
|
172
|
+
* runs the identical `src/web/shared/layout` code itself.
|
|
173
|
+
*/
|
|
174
|
+
positions: Record<string, Point> | null;
|
|
175
|
+
/**
|
|
176
|
+
* A **content digest** of this payload. The ETag body and the SSE dedupe
|
|
177
|
+
* key (§5.3, §6).
|
|
178
|
+
*
|
|
179
|
+
* Opaque: a truncated SHA-256 of the serialized payload, and nothing may
|
|
180
|
+
* parse it or derive meaning from its value. The only defined operation is
|
|
181
|
+
* equality, and it has exactly one guarantee — two payloads share a stamp
|
|
182
|
+
* if and only if they serialize to the same bytes.
|
|
183
|
+
*
|
|
184
|
+
* This was `model.generatedAt` until §15.6. A max of input timestamps is
|
|
185
|
+
* blind to any change that does not advance the maximum (a body edit, a
|
|
186
|
+
* front-matter edit, deleting a note that is not the newest), so a
|
|
187
|
+
* conditional GET answered `304` with stale content and the SSE dedupe
|
|
188
|
+
* dropped the frame that would have corrected it.
|
|
189
|
+
*
|
|
190
|
+
* For a human-readable "data as of" marker, read {@link GraphModel.generatedAt},
|
|
191
|
+
* which still carries it — the status bar does exactly that.
|
|
192
|
+
*/
|
|
193
|
+
stamp: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* `GET /api/note/:slug`, and the note half of every write response.
|
|
198
|
+
*
|
|
199
|
+
* The route used to serve a bare {@link ViewNote}. It carries a `revision`
|
|
200
|
+
* as of P5 because the editor cannot save safely without one: a save that
|
|
201
|
+
* does not say *which* state it was editing is a last-write-wins save, and
|
|
202
|
+
* the two writers a vault actually has — `$EDITOR` and an agent's
|
|
203
|
+
* `weave_note` call — are both fast enough to lose a paragraph to it.
|
|
204
|
+
*
|
|
205
|
+
* Carried in the **body** rather than as an `ETag`, deliberately. `api.ts`'s
|
|
206
|
+
* {@link HttpResponse} port exposes `ok`, `status` and `json()` and nothing
|
|
207
|
+
* else, because it exists so a two-line fake can stand in for `fetch` in a
|
|
208
|
+
* repository with no DOM (§10). Reading a header would mean widening that
|
|
209
|
+
* port for one field, and the field is a *property of the note*, not of the
|
|
210
|
+
* HTTP representation — unlike `/api/graph`'s stamp, which really is a cache
|
|
211
|
+
* validator and really does belong in an `ETag`.
|
|
212
|
+
*/
|
|
213
|
+
export interface NotePayload {
|
|
214
|
+
note: ViewNote;
|
|
215
|
+
/**
|
|
216
|
+
* Opaque version stamp for the file the note was read from.
|
|
217
|
+
*
|
|
218
|
+
* **Compare it, do not interpret it.** Core currently derives it from
|
|
219
|
+
* `mtimeMs:size` and says in the same breath that the shape is not part of
|
|
220
|
+
* the contract; a client that parsed a timestamp out of it would break the
|
|
221
|
+
* day core upgrades it to a digest. The only defined operation is equality
|
|
222
|
+
* against a later read of the same note.
|
|
223
|
+
*/
|
|
224
|
+
revision: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* `POST /api/note/:slug` request body.
|
|
229
|
+
*
|
|
230
|
+
* Every field optional, and that is not laziness — it mirrors core's
|
|
231
|
+
* `UpdateNoteInput`, where a metadata-only edit (retagging) and a body-only
|
|
232
|
+
* edit (the textarea) are both first-class. A body of `{}` is a legal
|
|
233
|
+
* request that bumps `updated` and nothing else.
|
|
234
|
+
*/
|
|
235
|
+
export interface SaveNoteRequest {
|
|
236
|
+
/** Replacement Markdown body. Omit to change only metadata. */
|
|
237
|
+
body?: string;
|
|
238
|
+
/**
|
|
239
|
+
* Metadata to merge over the note's current values.
|
|
240
|
+
*
|
|
241
|
+
* `created` is absent by construction: it records when the note came into
|
|
242
|
+
* existence and an edit is not a re-creation. `updated` is absent because
|
|
243
|
+
* the server owns it — a client that could set it could make an edit look
|
|
244
|
+
* older than the state it overwrote.
|
|
245
|
+
*/
|
|
246
|
+
meta?: {
|
|
247
|
+
title?: string;
|
|
248
|
+
tags?: string[];
|
|
249
|
+
source?: WireNoteSource;
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* The {@link NotePayload.revision} the client last read.
|
|
253
|
+
*
|
|
254
|
+
* Supply it and a stale save is refused with `409` and a
|
|
255
|
+
* {@link ConflictPayload} carrying the note as it now is. Omit it and the
|
|
256
|
+
* save is last-write-wins — which is a legitimate thing for the *user* to
|
|
257
|
+
* choose after seeing a conflict, and is exactly how "overwrite" is
|
|
258
|
+
* expressed: the same request, resent without this field.
|
|
259
|
+
*/
|
|
260
|
+
expectedRevision?: string;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** `POST /api/note/:slug/rename` request body. */
|
|
264
|
+
export interface RenameNoteRequest {
|
|
265
|
+
/**
|
|
266
|
+
* The destination. Passed through core's `slugify`, so a human title is
|
|
267
|
+
* as acceptable as a slug and both land on the same file name.
|
|
268
|
+
*/
|
|
269
|
+
slug: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** `DELETE /api/note/:slug` response. Hard delete — there is no trash. */
|
|
273
|
+
export interface DeleteNoteResult {
|
|
274
|
+
deleted: true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The `409` body for both write routes.
|
|
279
|
+
*
|
|
280
|
+
* A discriminated union rather than one shape with optional halves, because
|
|
281
|
+
* the two cases carry genuinely different payloads and a client that has to
|
|
282
|
+
* check `current !== undefined` before it can tell them apart is a client
|
|
283
|
+
* that will one day forget to.
|
|
284
|
+
*
|
|
285
|
+
* The `conflict` arm carries the **whole current note**, not just its
|
|
286
|
+
* revision. That is the difference between a UI that can offer
|
|
287
|
+
* reload-or-overwrite immediately and one that has to issue a second request
|
|
288
|
+
* before it can say anything useful — and the second request is one more
|
|
289
|
+
* window in which the file moves again. Core hands the server this note as
|
|
290
|
+
* part of the failure, so shipping it costs nothing.
|
|
291
|
+
*/
|
|
292
|
+
export type ConflictPayload =
|
|
293
|
+
| {
|
|
294
|
+
error: string;
|
|
295
|
+
reason: "conflict";
|
|
296
|
+
/** The note as it is on disk right now, with its current revision. */
|
|
297
|
+
current: NotePayload;
|
|
298
|
+
}
|
|
299
|
+
| {
|
|
300
|
+
error: string;
|
|
301
|
+
reason: "collision";
|
|
302
|
+
/** The destination slug that is already taken. */
|
|
303
|
+
slug: string;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/** `GET /api/okf/:rel`. */
|
|
307
|
+
export interface OkfFilePayload {
|
|
308
|
+
/** The requested path, relative to `<cwd>/.okf`. Echoed, never resolved. */
|
|
309
|
+
path: string;
|
|
310
|
+
body: string;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** `GET /api/search?q=`. */
|
|
314
|
+
export interface SearchPayload {
|
|
315
|
+
query: string;
|
|
316
|
+
hits: NoteSearchHit[];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** `POST /api/open` request body. */
|
|
320
|
+
export interface OpenRequest {
|
|
321
|
+
slug: string;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** `POST /api/open` response. `false` for an unsafe slug or a missing note. */
|
|
325
|
+
export interface OpenResult {
|
|
326
|
+
opened: boolean;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Every non-2xx JSON response. One shape for all of them so the client has
|
|
331
|
+
* exactly one error path.
|
|
332
|
+
*/
|
|
333
|
+
export interface ErrorPayload {
|
|
334
|
+
error: string;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// --- page bootstrap ----------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
/** `id` of the `<script type="application/json">` block in the HTML shell. */
|
|
340
|
+
export const BOOTSTRAP_ELEMENT_ID = "weave-bootstrap";
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* The JSON block embedded in the shell, so the first paint knows where it is
|
|
344
|
+
* without a round trip.
|
|
345
|
+
*/
|
|
346
|
+
export interface Bootstrap {
|
|
347
|
+
/** Absolute path the workspace was started in. */
|
|
348
|
+
cwd: string;
|
|
349
|
+
/** Absolute path of the vault root. */
|
|
350
|
+
vaultRoot: string;
|
|
351
|
+
/**
|
|
352
|
+
* Random per-boot id. `EventSource` reconnects transparently across a
|
|
353
|
+
* server restart, so without this a client cannot distinguish two cases it
|
|
354
|
+
* must handle differently: "I missed some frames" (refetch) and "this is a
|
|
355
|
+
* different server" (full reload).
|
|
356
|
+
*/
|
|
357
|
+
session: string;
|
|
358
|
+
}
|