@openparachute/vault 0.6.5-rc.2 → 0.6.5

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 (70) hide show
  1. package/.parachute/module.json +1 -0
  2. package/core/src/core.test.ts +7 -7
  3. package/core/src/do-param-cap.test.ts +161 -0
  4. package/core/src/links.ts +8 -13
  5. package/core/src/mcp.ts +1 -1
  6. package/core/src/notes.ts +33 -15
  7. package/core/src/onboarding.ts +14 -296
  8. package/core/src/schema.ts +5 -5
  9. package/core/src/seed-packs.test.ts +357 -0
  10. package/core/src/seed-packs.ts +823 -0
  11. package/core/src/sql-in.test.ts +58 -0
  12. package/core/src/sql-in.ts +76 -0
  13. package/core/src/tag-hierarchy.ts +1 -1
  14. package/core/src/tag-schemas.ts +2 -2
  15. package/core/src/transcription/provider.ts +141 -0
  16. package/core/src/types.ts +1 -1
  17. package/core/src/vault-projection.ts +1 -1
  18. package/core/src/wikilinks.ts +10 -4
  19. package/package.json +1 -1
  20. package/src/add-pack.test.ts +142 -0
  21. package/src/admin-spa.ts +2 -1
  22. package/src/auth.ts +1 -1
  23. package/src/cli.ts +806 -7
  24. package/src/export-watch.ts +1 -1
  25. package/src/live-frame-parity.test.ts +201 -0
  26. package/src/module-manifest.ts +8 -0
  27. package/src/onboarding-seed.test.ts +188 -40
  28. package/src/onboarding-seed.ts +41 -46
  29. package/src/routes.ts +20 -3
  30. package/src/routing.test.ts +2 -2
  31. package/src/routing.ts +18 -6
  32. package/src/self-register.test.ts +19 -0
  33. package/src/self-register.ts +6 -1
  34. package/src/server.ts +133 -31
  35. package/src/services-manifest.ts +8 -0
  36. package/src/subscriptions.ts +100 -42
  37. package/src/tag-scope.ts +3 -3
  38. package/src/test-support/live-frame-corpus.ts +72 -0
  39. package/src/token-store.ts +2 -2
  40. package/src/transcription/build.test.ts +305 -0
  41. package/src/transcription/build.ts +332 -0
  42. package/src/transcription/capability.test.ts +118 -0
  43. package/src/transcription/capability.ts +96 -0
  44. package/src/transcription/download.test.ts +101 -0
  45. package/src/transcription/download.ts +71 -0
  46. package/src/transcription/install-python.test.ts +366 -0
  47. package/src/transcription/install-python.ts +471 -0
  48. package/src/transcription/install.test.ts +167 -0
  49. package/src/transcription/install.ts +296 -0
  50. package/src/transcription/providers/onnx-asr.test.ts +229 -0
  51. package/src/transcription/providers/onnx-asr.ts +239 -0
  52. package/src/transcription/providers/parakeet-mlx.test.ts +290 -0
  53. package/src/transcription/providers/parakeet-mlx.ts +242 -0
  54. package/src/transcription/providers/scribe-http.test.ts +195 -0
  55. package/src/transcription/providers/scribe-http.ts +144 -0
  56. package/src/transcription/providers/transcribe-cpp.test.ts +314 -0
  57. package/src/transcription/providers/transcribe-cpp.ts +293 -0
  58. package/src/transcription/select.test.ts +299 -0
  59. package/src/transcription/select.ts +397 -0
  60. package/src/transcription/tiers.test.ts +197 -0
  61. package/src/transcription/tiers.ts +184 -0
  62. package/src/transcription-worker.test.ts +44 -0
  63. package/src/transcription-worker.ts +57 -122
  64. package/src/vault-create.test.ts +43 -10
  65. package/src/vault.test.ts +49 -1
  66. package/src/ws-server.ts +408 -0
  67. package/src/ws-subscribe.test.ts +474 -0
  68. package/src/ws-subscribe.ts +242 -0
  69. package/src/subscribe.test.ts +0 -609
  70. package/src/subscribe.ts +0 -248
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Content + registry pins for the named seed packs (core/src/seed-packs.ts).
3
+ *
4
+ * Pure content tests — no DB. The applier's behavior (idempotence, seeding,
5
+ * tag/metadata write-through) is exercised store-level in
6
+ * src/onboarding-seed.test.ts and CLI-level in src/add-pack.test.ts.
7
+ *
8
+ * The load-bearing pins:
9
+ * - the `welcome` pack's tags must CONTAIN notes-ui's NOTES_REQUIRED_SCHEMA
10
+ * byte-for-byte — the Notes PWA's connect-time audit compares `description`
11
+ * verbatim, and its schema banner only clears when the capture tag genuinely
12
+ * carries the semantics Notes declares. (Containment, not equality: the
13
+ * welcome pack now also ships the #guide tag.)
14
+ * - `#guide` is the vault's skill-file tag (no schema — guides are AI-first
15
+ * and human-readable markdown, so no per-guide audience field). Every seeded
16
+ * guide note is tagged `#guide`.
17
+ * - the five-guide welcome ring links into a connected web with no dangling
18
+ * wikilinks across the default seed.
19
+ */
20
+
21
+ import { describe, test, expect } from "bun:test";
22
+ import {
23
+ applySeedPack,
24
+ CAPTURE_ANYTHING_PATH,
25
+ CONNECT_AI_PATH,
26
+ DEFAULT_VAULT_DESCRIPTION,
27
+ IMPORTED_VAULT_DESCRIPTION,
28
+ YOURS_TO_KEEP_PATH,
29
+ GETTING_STARTED_CONTENT,
30
+ GETTING_STARTED_PACK,
31
+ GETTING_STARTED_PATH,
32
+ getSeedPack,
33
+ GUIDE_TAG,
34
+ listSeedPacks,
35
+ NOTES_REQUIRED_TAGS,
36
+ SEED_PACK_NAMES,
37
+ SURFACE_STARTER_CONTENT,
38
+ SURFACE_STARTER_PACK,
39
+ SURFACE_STARTER_PATH,
40
+ TAGS_GRAPH_PATH,
41
+ WELCOME_PATH,
42
+ welcomePack,
43
+ YOURS_TO_KEEP_PATH,
44
+ type SeedPack,
45
+ type SeedPackNote,
46
+ } from "./seed-packs.ts";
47
+ import * as onboardingShim from "./onboarding.ts";
48
+
49
+ /**
50
+ * LITERAL copy of `NOTES_REQUIRED_SCHEMA.tags` from
51
+ * parachute-surface/packages/notes-ui/src/lib/vault/schema.ts — deliberately
52
+ * NOT imported (different repo; and the whole point is an independent copy
53
+ * that breaks this test when either side drifts). If this test fails, change
54
+ * notes-ui and the pack in lockstep, never one side alone.
55
+ */
56
+ const NOTES_UI_REQUIRED_SCHEMA_TAGS = [
57
+ {
58
+ name: "capture",
59
+ description: "Notes captured directly by the user (text or voice).",
60
+ },
61
+ ];
62
+
63
+ /**
64
+ * Extract every REAL `[[wikilink]]` target from note content — mirroring the
65
+ * store's parser (core/src/wikilinks.ts), which ignores wikilinks inside
66
+ * fenced/inline code. So an illustrative `[[wikilinks]]` in a code span (as in
67
+ * the Getting Started guide) is not counted — it never becomes a link.
68
+ */
69
+ function wikilinkTargets(content: string): string[] {
70
+ const stripped = content
71
+ .replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length)) // fenced code
72
+ .replace(/`[^`]*`/g, (m) => " ".repeat(m.length)); // inline code
73
+ return [...stripped.matchAll(/\[\[([^\]]+)\]\]/g)].map((m) => m[1]!);
74
+ }
75
+
76
+ /** A note in a pack, by path (fails loudly if absent). */
77
+ function noteAt(pack: SeedPack, path: string): SeedPackNote {
78
+ const note = pack.notes.find((n) => n.path === path);
79
+ if (!note) throw new Error(`no note at path ${path}`);
80
+ return note;
81
+ }
82
+
83
+ describe("guide tag — the vault's skill-file tag", () => {
84
+ test("GUIDE_TAG is the skill-file tag with no per-guide audience schema", () => {
85
+ expect(GUIDE_TAG.name).toBe("guide");
86
+ expect(GUIDE_TAG.description).toContain("skill file");
87
+ // written_for was dropped (2026-07-06): guides are AI-first + human-readable
88
+ // markdown, so a per-guide audience field earns nothing. No schema on the tag.
89
+ expect(GUIDE_TAG.fields).toBeUndefined();
90
+ });
91
+
92
+ });
93
+
94
+ describe("welcome pack — notes-ui schema parity (containment)", () => {
95
+ test("welcome tags CONTAIN notes-ui's NOTES_REQUIRED_SCHEMA byte-for-byte", () => {
96
+ // The independent copy still matches, and the welcome pack still ships it.
97
+ expect(NOTES_REQUIRED_TAGS).toEqual(NOTES_UI_REQUIRED_SCHEMA_TAGS);
98
+ for (const req of NOTES_UI_REQUIRED_SCHEMA_TAGS) {
99
+ const found = welcomePack().tags.find((t) => t.name === req.name);
100
+ expect(found).toBeDefined();
101
+ // Field-level byte pins (toEqual ignores key order; these don't).
102
+ expect(found!.name).toBe(req.name);
103
+ expect(found!.description).toBe(req.description);
104
+ expect(found!.parent_names).toBeUndefined();
105
+ }
106
+ });
107
+
108
+ test("welcome tags = capture + guide (the guide tag rides along)", () => {
109
+ expect(welcomePack().tags.map((t) => t.name)).toEqual([
110
+ "capture",
111
+ "guide",
112
+ ]);
113
+ expect(welcomePack().tags).toContain(GUIDE_TAG);
114
+ });
115
+
116
+ test("exactly ONE capture tag — no subtype tags, no parent_names (2026-07-03)", () => {
117
+ // Entry method is metadata.source (text|voice), not taxonomy. The old
118
+ // capture/text + capture/voice subtypes must not creep back into the seed
119
+ // (existing vaults carrying them stay valid — tags are user data).
120
+ expect(NOTES_REQUIRED_TAGS).toHaveLength(1);
121
+ expect(NOTES_REQUIRED_TAGS[0]!.name).toBe("capture");
122
+ expect(NOTES_REQUIRED_TAGS[0]!.parent_names).toBeUndefined();
123
+ });
124
+ });
125
+
126
+ describe("welcome pack — the five-guide ring", () => {
127
+ test("five guides at the ratified paths, in order", () => {
128
+ const pack = welcomePack();
129
+ expect(pack.name).toBe("welcome");
130
+ expect(pack.notes.map((n) => n.path)).toEqual([
131
+ WELCOME_PATH,
132
+ CAPTURE_ANYTHING_PATH,
133
+ TAGS_GRAPH_PATH,
134
+ CONNECT_AI_PATH,
135
+ YOURS_TO_KEEP_PATH,
136
+ ]);
137
+ });
138
+
139
+ test("the link web: welcome → all four; 2→3→4→5→1; connect → Getting Started", () => {
140
+ const pack = welcomePack();
141
+
142
+ // Welcome wikilinks to all four siblings.
143
+ const welcomeLinks = wikilinkTargets(noteAt(pack, WELCOME_PATH).content);
144
+ for (const target of [
145
+ CAPTURE_ANYTHING_PATH,
146
+ TAGS_GRAPH_PATH,
147
+ CONNECT_AI_PATH,
148
+ YOURS_TO_KEEP_PATH,
149
+ ]) {
150
+ expect(welcomeLinks).toContain(target);
151
+ }
152
+
153
+ // The chain forward: 2→3, 3→4, 4→5, 5→1.
154
+ expect(wikilinkTargets(noteAt(pack, CAPTURE_ANYTHING_PATH).content)).toContain(
155
+ TAGS_GRAPH_PATH,
156
+ );
157
+ expect(wikilinkTargets(noteAt(pack, TAGS_GRAPH_PATH).content)).toContain(
158
+ CONNECT_AI_PATH,
159
+ );
160
+ expect(wikilinkTargets(noteAt(pack, CONNECT_AI_PATH).content)).toContain(
161
+ YOURS_TO_KEEP_PATH,
162
+ );
163
+ expect(wikilinkTargets(noteAt(pack, YOURS_TO_KEEP_PATH).content)).toContain(
164
+ WELCOME_PATH,
165
+ );
166
+
167
+ // Connect your AI points the human at the AI-facing Getting Started note.
168
+ expect(wikilinkTargets(noteAt(pack, CONNECT_AI_PATH).content)).toContain(
169
+ GETTING_STARTED_PATH,
170
+ );
171
+ });
172
+
173
+ test("NO dangling wikilinks across the default seed (welcome + getting-started)", () => {
174
+ // Both packs auto-seed on create, so every [[target]] across their notes
175
+ // must resolve to a note one of them writes — including [[Getting Started]].
176
+ const seededPaths = new Set<string>([
177
+ ...welcomePack().notes.map((n) => n.path),
178
+ ...GETTING_STARTED_PACK.notes.map((n) => n.path),
179
+ ]);
180
+ for (const pack of [welcomePack(), GETTING_STARTED_PACK]) {
181
+ for (const note of pack.notes) {
182
+ for (const target of wikilinkTargets(note.content)) {
183
+ expect(seededPaths.has(target)).toBe(true);
184
+ }
185
+ }
186
+ }
187
+ });
188
+
189
+ test("every welcome guide is tagged #guide", () => {
190
+ for (const note of welcomePack().notes) {
191
+ expect(note.tags).toContain("guide");
192
+ }
193
+ });
194
+
195
+ test("no seeded note is pinned (pinning is a user action, not seeded)", () => {
196
+ // The vestigial #pinned seed was dropped 2026-07-07 — it did nothing visible
197
+ // on a fresh vault. Pinning still works when a user pins a note in the app.
198
+ const pinned = welcomePack().notes.filter((n) => n.tags?.includes("pinned"));
199
+ expect(pinned).toEqual([]);
200
+ expect(noteAt(welcomePack(), WELCOME_PATH).tags).toEqual(["guide"]);
201
+ });
202
+
203
+ test("content anchors: the ratified copy is present (guards body drift)", () => {
204
+ const pack = welcomePack();
205
+ expect(noteAt(pack, WELCOME_PATH).content).toContain("This vault is yours.");
206
+ expect(noteAt(pack, WELCOME_PATH).content).toContain("tagged #guide");
207
+ expect(noteAt(pack, CAPTURE_ANYTHING_PATH).content).toContain(
208
+ "The one habit that makes a vault work",
209
+ );
210
+ expect(noteAt(pack, TAGS_GRAPH_PATH).content).toContain(
211
+ "Notes and links come first. Tags come later",
212
+ );
213
+ expect(noteAt(pack, YOURS_TO_KEEP_PATH).content).toContain(
214
+ "the moral center of the",
215
+ );
216
+ // Yours to keep carries the self-host export command (backtick-wrapped).
217
+ expect(noteAt(pack, YOURS_TO_KEEP_PATH).content).toContain(
218
+ "`parachute-vault export <dir>`",
219
+ );
220
+ });
221
+
222
+ test("consoleOrigin renders both modes on the Connect-your-AI note", () => {
223
+ const origin = "https://cloud.parachute.computer";
224
+
225
+ // With origin: the console sentence names it, no `undefined`, no `at .`.
226
+ const withOrigin = noteAt(welcomePack({ consoleOrigin: origin }), CONNECT_AI_PATH);
227
+ expect(withOrigin.content).toContain(
228
+ `Grab the connection URL from your console at ${origin}.`,
229
+ );
230
+ expect(withOrigin.content).not.toContain("undefined");
231
+
232
+ // Without origin: the line stays generic — no baked loopback, no `at .`.
233
+ const generic = noteAt(welcomePack(), CONNECT_AI_PATH);
234
+ expect(generic.content).toContain(
235
+ "Grab the connection URL from your console.",
236
+ );
237
+ expect(generic.content).not.toContain("undefined");
238
+ expect(generic.content).not.toContain("at .");
239
+ expect(generic.content).not.toContain(origin);
240
+ });
241
+ });
242
+
243
+ describe("getting-started pack", () => {
244
+ test("carries the AI-facing guide note, tagged #guide", () => {
245
+ expect(GETTING_STARTED_PACK.name).toBe("getting-started");
246
+ // Declares the guide tag (self-sufficient — converges with the welcome pack).
247
+ expect(GETTING_STARTED_PACK.tags).toEqual([GUIDE_TAG]);
248
+ expect(GETTING_STARTED_PACK.notes).toEqual([
249
+ {
250
+ path: GETTING_STARTED_PATH,
251
+ tags: ["guide"],
252
+ content: GETTING_STARTED_CONTENT,
253
+ },
254
+ ]);
255
+ });
256
+
257
+ test("the Getting Started body is byte-unchanged", () => {
258
+ // The AI-facing doctrine copy is not touched by the ring rewrite.
259
+ expect(noteAt(GETTING_STARTED_PACK, GETTING_STARTED_PATH).content).toBe(
260
+ GETTING_STARTED_CONTENT,
261
+ );
262
+ expect(GETTING_STARTED_CONTENT.startsWith("# Getting Started\n")).toBe(true);
263
+ });
264
+
265
+ test("no dangling [[Surface Starter]] wikilink — points at the add-pack flow instead", () => {
266
+ // Surface Starter is out of the default seed (ratified 2026-07-02), so the
267
+ // default-seeded guide must not carry a wikilink to a note that doesn't
268
+ // exist. It names the pack + the way to add it.
269
+ expect(GETTING_STARTED_CONTENT).not.toContain("[[Surface Starter]]");
270
+ expect(GETTING_STARTED_CONTENT).toContain("Surface Starter");
271
+ // Points at the add-pack flow (console affordance / CLI) without spelling
272
+ // out a terminal command — de-emphasise the CLI (Aaron 2026-07-06).
273
+ expect(GETTING_STARTED_CONTENT).toContain("add-pack");
274
+ // The surface packages are still named for discoverability.
275
+ expect(GETTING_STARTED_CONTENT).toContain("@openparachute/surface-client");
276
+ expect(GETTING_STARTED_CONTENT).toContain("@openparachute/surface-render");
277
+ });
278
+
279
+ test("Getting Started is onboarding-first + carries no dead CLI guidance (2026-07-06 rewrite)", () => {
280
+ // Two-part structure: onboarding conversation leads, mechanics reference follows.
281
+ expect(GETTING_STARTED_CONTENT).toContain("## Part 1 — Onboarding the person");
282
+ expect(GETTING_STARTED_CONTENT).toContain("## Part 2 — Vault mechanics");
283
+ // Points the person at the five human guides by name (all resolve to seeds).
284
+ for (const g of [WELCOME_PATH, CAPTURE_ANYTHING_PATH, TAGS_GRAPH_PATH, CONNECT_AI_PATH, YOURS_TO_KEEP_PATH]) {
285
+ expect(GETTING_STARTED_CONTENT).toContain("[[" + g + "]]");
286
+ }
287
+ // The old live bug: it told cloud AIs to run a CLI that doesn't exist there.
288
+ expect(GETTING_STARTED_CONTENT).not.toContain("parachute-vault import");
289
+ // Data-in leads with the import UI + MCP bridge, not the terminal.
290
+ expect(GETTING_STARTED_CONTENT).toContain("Import button");
291
+ expect(GETTING_STARTED_CONTENT).toContain("over MCP");
292
+ });
293
+
294
+ test("default vault-description constants orient + tell the AI to self-replace them", () => {
295
+ for (const d of [DEFAULT_VAULT_DESCRIPTION, IMPORTED_VAULT_DESCRIPTION]) {
296
+ expect(d.length).toBeGreaterThan(100);
297
+ expect(d).toContain("Getting Started");
298
+ expect(d).toContain("vault-info { description:"); // the self-replace instruction
299
+ }
300
+ expect(DEFAULT_VAULT_DESCRIPTION).toContain("brand-new");
301
+ expect(IMPORTED_VAULT_DESCRIPTION).toContain("imported");
302
+ });
303
+ });
304
+
305
+ describe("surface-starter pack", () => {
306
+ test("carries the surface guide, tagged #guide (opt-in, not default)", () => {
307
+ expect(SURFACE_STARTER_PACK.name).toBe("surface-starter");
308
+ expect(SURFACE_STARTER_PACK.tags).toEqual([GUIDE_TAG]);
309
+ expect(SURFACE_STARTER_PACK.notes).toEqual([
310
+ {
311
+ path: SURFACE_STARTER_PATH,
312
+ tags: ["guide"],
313
+ content: SURFACE_STARTER_CONTENT,
314
+ },
315
+ ]);
316
+ expect(SURFACE_STARTER_PACK.description).toContain("not seeded by default");
317
+ });
318
+
319
+ test("links back to Getting Started (default-seeded, so the wikilink resolves)", () => {
320
+ expect(SURFACE_STARTER_CONTENT).toContain("[[Getting Started]]");
321
+ });
322
+ });
323
+
324
+ describe("pack registry", () => {
325
+ test("lists exactly the three packs, in order", () => {
326
+ expect([...SEED_PACK_NAMES]).toEqual([
327
+ "welcome",
328
+ "getting-started",
329
+ "surface-starter",
330
+ ]);
331
+ expect(listSeedPacks().map((p) => p.name)).toEqual([...SEED_PACK_NAMES]);
332
+ for (const p of listSeedPacks()) {
333
+ expect(p.description.length).toBeGreaterThan(0);
334
+ }
335
+ });
336
+
337
+ test("getSeedPack resolves each name and returns null for unknown", () => {
338
+ for (const name of SEED_PACK_NAMES) {
339
+ expect(getSeedPack(name)?.name).toBe(name);
340
+ }
341
+ expect(getSeedPack("nope")).toBeNull();
342
+ expect(getSeedPack("")).toBeNull();
343
+ });
344
+
345
+ test("applySeedPack is exported for both runtimes", () => {
346
+ expect(typeof applySeedPack).toBe("function");
347
+ });
348
+ });
349
+
350
+ describe("onboarding.ts back-compat shim", () => {
351
+ test("re-exports the canonical paths + contents from seed-packs", () => {
352
+ expect(onboardingShim.GETTING_STARTED_PATH).toBe(GETTING_STARTED_PATH);
353
+ expect(onboardingShim.GETTING_STARTED_CONTENT).toBe(GETTING_STARTED_CONTENT);
354
+ expect(onboardingShim.SURFACE_STARTER_PATH).toBe(SURFACE_STARTER_PATH);
355
+ expect(onboardingShim.SURFACE_STARTER_CONTENT).toBe(SURFACE_STARTER_CONTENT);
356
+ });
357
+ });