pi-weave 0.1.7 → 0.1.8

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 (84) hide show
  1. package/README.md +114 -34
  2. package/package.json +16 -4
  3. package/src/core/cache/workspace.ts +466 -0
  4. package/src/core/frontmatter.ts +217 -23
  5. package/src/core/git.ts +19 -0
  6. package/src/core/graph/build.ts +37 -4
  7. package/src/core/graph/current.ts +41 -28
  8. package/src/core/graph/mentions.ts +170 -0
  9. package/src/core/graph/model.ts +24 -0
  10. package/src/core/index.ts +12 -0
  11. package/src/core/openInEditor.ts +69 -0
  12. package/src/core/types.ts +40 -0
  13. package/src/core/vault.ts +477 -43
  14. package/src/core/view/cluster.ts +262 -0
  15. package/src/core/view/detail.ts +118 -0
  16. package/src/core/view/focus.ts +109 -0
  17. package/src/core/view/health.ts +156 -0
  18. package/src/core/view/index.ts +15 -0
  19. package/src/core/view/links.ts +105 -0
  20. package/src/core/view/time.ts +47 -0
  21. package/src/core/view/tree.ts +269 -0
  22. package/src/core/view/types.ts +39 -0
  23. package/src/pi/index.ts +104 -11
  24. package/src/pi/viewer/tui/explorer.ts +4 -2
  25. package/src/pi/viewer/tui/model.ts +46 -667
  26. package/src/pi/viewer/tui/openNote.ts +7 -56
  27. package/src/pi/viewer/tui/surface/explore.ts +4 -2
  28. package/src/pi/viewer/web/run.ts +331 -0
  29. package/src/web/client/api.dom.ts +40 -0
  30. package/src/web/client/api.ts +472 -0
  31. package/src/web/client/bootstrap.ts +58 -0
  32. package/src/web/client/context/context.model.ts +313 -0
  33. package/src/web/client/dist/app.js +751 -0
  34. package/src/web/client/graph/Graph.tsx +158 -0
  35. package/src/web/client/graph/column.model.ts +431 -0
  36. package/src/web/client/graph/graph.model.ts +538 -0
  37. package/src/web/client/graph/positions.ts +339 -0
  38. package/src/web/client/graph/project.ts +153 -0
  39. package/src/web/client/graph/renderer.dom.ts +52 -0
  40. package/src/web/client/graph/renderer.ts +279 -0
  41. package/src/web/client/graph/scheme.ts +44 -0
  42. package/src/web/client/live.model.ts +275 -0
  43. package/src/web/client/live.ts +151 -0
  44. package/src/web/client/main.tsx +27 -0
  45. package/src/web/client/note/Editor.tsx +102 -0
  46. package/src/web/client/note/Note.tsx +113 -0
  47. package/src/web/client/note/editor.controller.ts +151 -0
  48. package/src/web/client/note/editor.model.ts +636 -0
  49. package/src/web/client/note/note.model.ts +738 -0
  50. package/src/web/client/search/SearchPalette.tsx +105 -0
  51. package/src/web/client/search/search.model.ts +588 -0
  52. package/src/web/client/search/search.ts +107 -0
  53. package/src/web/client/shell/Columns.tsx +161 -0
  54. package/src/web/client/shell/ContextRail.tsx +87 -0
  55. package/src/web/client/shell/Divider.tsx +44 -0
  56. package/src/web/client/shell/FocusTrap.tsx +56 -0
  57. package/src/web/client/shell/Header.tsx +54 -0
  58. package/src/web/client/shell/HelpOverlay.tsx +70 -0
  59. package/src/web/client/shell/Shell.tsx +193 -0
  60. package/src/web/client/shell/StatusBar.tsx +28 -0
  61. package/src/web/client/shell/cssvars.ts +70 -0
  62. package/src/web/client/shell/drag.model.ts +170 -0
  63. package/src/web/client/shell/focus.model.ts +100 -0
  64. package/src/web/client/shell/keys.model.ts +453 -0
  65. package/src/web/client/shell/keys.ts +59 -0
  66. package/src/web/client/shell/layout.model.ts +526 -0
  67. package/src/web/client/shell/shell.model.ts +333 -0
  68. package/src/web/client/shell/theme.ts +477 -0
  69. package/src/web/client/shell/viewport.ts +29 -0
  70. package/src/web/client/state.ts +78 -0
  71. package/src/web/client/tree/Tree.tsx +138 -0
  72. package/src/web/client/tree/tree.model.ts +674 -0
  73. package/src/web/client/workspace.ts +214 -0
  74. package/src/web/server/page.ts +256 -0
  75. package/src/web/server/routes.ts +975 -0
  76. package/src/web/server/security.ts +361 -0
  77. package/src/web/server/server.ts +275 -0
  78. package/src/web/server/sse.ts +321 -0
  79. package/src/web/server/watcher.ts +507 -0
  80. package/src/web/shared/graph.ts +206 -0
  81. package/src/web/shared/layout.ts +497 -0
  82. package/src/web/shared/metrics.ts +136 -0
  83. package/src/web/shared/view.ts +200 -0
  84. package/src/web/shared/wire.ts +358 -0
@@ -60,4 +60,28 @@ export interface GraphModel {
60
60
  staleness: StalenessReport | null;
61
61
  nodes: GraphNode[];
62
62
  edges: GraphEdge[];
63
+ /**
64
+ * slug → wiki-link targets that resolved to no note in the graph
65
+ * (weave-workspace §4.2).
66
+ *
67
+ * A `[[target]]` that matches nothing is not an error — it is Obsidian's
68
+ * ghost node, the affordance that offers to create the missing note. The
69
+ * builder used to count these and throw the names away, leaving
70
+ * `detail["dangling links"] = "3"` as the only trace; a display string is
71
+ * not something a UI can navigate.
72
+ *
73
+ * Lives here rather than on {@link GraphNode} because `detail` is
74
+ * display-only by contract and must not grow structure (§4.2 is explicit
75
+ * about preferring the model-level map). Notes with nothing unresolved are
76
+ * absent rather than present-and-empty, so the common case costs no bytes.
77
+ *
78
+ * Keyed by **slug**, not by node id: the consumers of this map (the note
79
+ * column, the wire payload's `dangling` field) speak slugs, and `note:` is
80
+ * a graph-internal prefix.
81
+ *
82
+ * Insertion order follows note order and each target list follows
83
+ * first-appearance order in the body, so the JSON stays byte-stable for
84
+ * unchanged inputs.
85
+ */
86
+ danglingLinks: Record<string, string[]>;
63
87
  }
package/src/core/index.ts CHANGED
@@ -14,12 +14,24 @@ export * from "./vault";
14
14
  export * from "./repoIndex";
15
15
  export * from "./summaries";
16
16
  export * from "./workspace";
17
+ export * from "./openInEditor";
17
18
  export * from "./graph/model";
18
19
  export * from "./graph/wikilinks";
20
+ export * from "./view";
19
21
  export { buildGraph, dataTimestamp, DEFAULT_MAX_NOTES, type BuildGraphInput } from "./graph/build";
20
22
  export {
21
23
  buildCurrentGraph,
22
24
  readNoteForView,
23
25
  readOkfFileForView,
26
+ readRepositorySide,
24
27
  type ViewNote,
25
28
  } from "./graph/current";
29
+ export {
30
+ classifyPath,
31
+ DEFAULT_STALENESS_TTL_MS,
32
+ WorkspaceCache,
33
+ type CacheStats,
34
+ type InvalidationScope,
35
+ type WorkspaceCacheOptions,
36
+ type WorkspaceSnapshot,
37
+ } from "./cache/workspace";
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Open a vault note in the user's editor.
3
+ *
4
+ * Originally `src/pi/viewer/tui/openNote.ts`, promoted into core because the
5
+ * browser workspace needs it too and `src/web/server/**` may not import
6
+ * `src/pi` (weave-workspace §2). It belongs here on the merits regardless:
7
+ * it is a pure fan-out over `core/vault` plus one `execFile`, with no
8
+ * harness and no terminal concept in it, and the same reasoning that put
9
+ * `readNoteForView` in core applies unchanged.
10
+ *
11
+ * The old path re-exports these two names, so no TUI file changed.
12
+ *
13
+ * Security: the slug is validated against the vault (traversal-safe) before
14
+ * any shell-out, and the command is invoked with `execFile`, not a shell, so
15
+ * neither the slug nor a `$EDITOR` containing metacharacters can compose a
16
+ * command. `$EDITOR` itself is trusted — it is the user's own configuration,
17
+ * and a hostile one already has whatever the user has.
18
+ */
19
+
20
+ import { execFile } from "node:child_process";
21
+ import { platform } from "node:os";
22
+ import { promisify } from "node:util";
23
+ import { getNote, resolveNotePath } from "./vault";
24
+
25
+ const execFileAsync = promisify(execFile);
26
+
27
+ /**
28
+ * The OS command used to open a note file in the user's editor. Respects
29
+ * $EDITOR / $VISUAL (which may carry args, e.g. "code --wait"); falls back
30
+ * to the platform default opener. `env` and `os` are injectable so the
31
+ * mapping is unit-testable on any host without stubbing globals.
32
+ */
33
+ export function openNoteCommand(
34
+ notePath: string,
35
+ env: NodeJS.ProcessEnv = process.env,
36
+ os: NodeJS.Platform = platform(),
37
+ ): { command: string; args: string[] } {
38
+ const editor = (env.EDITOR || env.VISUAL || "").trim();
39
+ if (editor) {
40
+ // editor is a trimmed non-empty string, so split+filter always yields a
41
+ // first segment — assert it rather than leave a dead `?? ""` branch.
42
+ const parts = editor.split(/\s+/).filter(Boolean);
43
+ const command = parts[0] as string;
44
+ return { command, args: [...parts.slice(1), notePath] };
45
+ }
46
+ if (os === "darwin") return { command: "open", args: [notePath] };
47
+ if (os === "win32") return { command: "cmd", args: ["/c", "start", "", notePath] };
48
+ return { command: "xdg-open", args: [notePath] };
49
+ }
50
+
51
+ /**
52
+ * Open a note in the OS editor. The slug is validated against the vault
53
+ * (traversal-safe) before any shell-out; returns false when the note does
54
+ * not exist or the slug is unsafe.
55
+ */
56
+ export async function openNoteInEditor(
57
+ vaultRoot: string,
58
+ slug: string,
59
+ openCommand: (notePath: string) => { command: string; args: string[] } = openNoteCommand,
60
+ ): Promise<boolean> {
61
+ const path = resolveNotePath(vaultRoot, slug);
62
+ if (path === null) return false; // traversal-safe
63
+ const note = await getNote(vaultRoot, slug);
64
+ if (note === null) return false;
65
+ const { command, args } = openCommand(path);
66
+ if (!command) return false;
67
+ await execFileAsync(command, args);
68
+ return true;
69
+ }
package/src/core/types.ts CHANGED
@@ -22,11 +22,51 @@ export interface NoteMeta {
22
22
  source: NoteSource;
23
23
  }
24
24
 
25
+ /**
26
+ * The verbatim lines of a note's front-matter block, in file order, without
27
+ * the enclosing `---` fences.
28
+ *
29
+ * This is how pi-weave keeps front matter **lossless**. `NoteMeta` names the
30
+ * five fields the engine understands; a vault is a directory of files a human
31
+ * also edits, so real notes carry keys the engine has never heard of —
32
+ * Obsidian's `aliases`, `cssclass`, `publish`, whatever the user invented last
33
+ * Tuesday. Modelling those as parsed values would mean re-emitting them, and
34
+ * re-emitting a value the parser only half-understands is how formatting (and
35
+ * then content) gets destroyed.
36
+ *
37
+ * So they are not parsed at all: the raw lines are carried through, and
38
+ * `serializeNote` replays them, substituting a freshly rendered line only for
39
+ * the keys `NoteMeta` actually owns. Unknown keys — including ones using
40
+ * syntax the subset parser cannot represent, such as block lists or folded
41
+ * scalars — come back out byte-identical, in their original order.
42
+ *
43
+ * Deliberately `readonly string[]` rather than a parsed record: a second
44
+ * representation of a line is a second thing that can disagree with the file.
45
+ * The key of a line is derived with the same rule the parser uses, at the one
46
+ * place that needs it.
47
+ */
48
+ export type NoteFrontMatter = readonly string[];
49
+
25
50
  /** A vault note: metadata plus its Markdown body. */
26
51
  export interface Note extends NoteMeta {
27
52
  /** File-name slug (no extension). Stable identity of the note. */
28
53
  slug: string;
29
54
  body: string;
55
+ /**
56
+ * The note's front-matter block as it appeared on disk, so a read → write
57
+ * round trip preserves keys outside {@link NoteMeta} (see
58
+ * {@link NoteFrontMatter}).
59
+ *
60
+ * Optional because a `Note` need not have come from a file: graph fixtures
61
+ * and in-memory projections construct one directly. Absent means "no layout
62
+ * to preserve", and `serializeNote` then emits the canonical five fields.
63
+ *
64
+ * Not carried on {@link NoteSummary}, and deliberately not on the wire: the
65
+ * browser never needs to see a user's unknown keys in order for them to
66
+ * survive, because every write path re-reads the file it is about to
67
+ * overwrite.
68
+ */
69
+ frontMatter?: NoteFrontMatter;
30
70
  }
31
71
 
32
72
  /** Summary of one note for list/search output. */