@openparachute/vault 0.6.5-rc.13 → 0.6.5-rc.15
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/core/src/seed-packs.test.ts +209 -55
- package/core/src/seed-packs.ts +208 -35
- package/package.json +1 -1
- package/src/live-frame-parity.test.ts +16 -41
- package/src/onboarding-seed.test.ts +79 -18
- package/src/onboarding-seed.ts +2 -2
- package/src/routing.ts +17 -5
- package/src/server.ts +3 -2
- package/src/subscriptions.ts +20 -50
- package/src/vault-create.test.ts +16 -11
- package/src/ws-server.ts +1 -1
- package/src/subscribe.test.ts +0 -609
- package/src/subscribe.ts +0 -249
|
@@ -1,35 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Content + registry pins for the named seed packs (core/src/seed-packs.ts).
|
|
3
3
|
*
|
|
4
|
-
* Pure content tests — no DB. The applier's behavior (idempotence, seeding
|
|
5
|
-
* is exercised store-level in
|
|
6
|
-
* src/add-pack.test.ts.
|
|
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
7
|
*
|
|
8
|
-
* The load-bearing
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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 + #pinned tags.)
|
|
14
|
+
* - `#guide` is the vault's skill-file tag, carrying a `written_for` enum
|
|
15
|
+
* schema (`ai` | `human` | `both`, `ai` first = default). Every seeded guide
|
|
16
|
+
* note is tagged `#guide` + `metadata.written_for`.
|
|
17
|
+
* - the five-guide welcome ring links into a connected web with no dangling
|
|
18
|
+
* wikilinks across the default seed.
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
import { describe, test, expect } from "bun:test";
|
|
17
22
|
import {
|
|
18
23
|
applySeedPack,
|
|
24
|
+
CAPTURE_ANYTHING_PATH,
|
|
19
25
|
CONNECT_AI_PATH,
|
|
20
26
|
GETTING_STARTED_CONTENT,
|
|
21
27
|
GETTING_STARTED_PACK,
|
|
22
28
|
GETTING_STARTED_PATH,
|
|
23
29
|
getSeedPack,
|
|
30
|
+
GUIDE_TAG,
|
|
24
31
|
listSeedPacks,
|
|
25
32
|
NOTES_REQUIRED_TAGS,
|
|
33
|
+
PINNED_TAG,
|
|
26
34
|
SEED_PACK_NAMES,
|
|
27
35
|
SURFACE_STARTER_CONTENT,
|
|
28
36
|
SURFACE_STARTER_PACK,
|
|
29
37
|
SURFACE_STARTER_PATH,
|
|
30
|
-
|
|
38
|
+
TAGS_GRAPH_PATH,
|
|
31
39
|
WELCOME_PATH,
|
|
32
40
|
welcomePack,
|
|
41
|
+
YOURS_TO_KEEP_PATH,
|
|
42
|
+
type SeedPack,
|
|
43
|
+
type SeedPackNote,
|
|
33
44
|
} from "./seed-packs.ts";
|
|
34
45
|
import * as onboardingShim from "./onboarding.ts";
|
|
35
46
|
|
|
@@ -47,17 +58,74 @@ const NOTES_UI_REQUIRED_SCHEMA_TAGS = [
|
|
|
47
58
|
},
|
|
48
59
|
];
|
|
49
60
|
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Extract every REAL `[[wikilink]]` target from note content — mirroring the
|
|
63
|
+
* store's parser (core/src/wikilinks.ts), which ignores wikilinks inside
|
|
64
|
+
* fenced/inline code. So an illustrative `[[wikilinks]]` in a code span (as in
|
|
65
|
+
* the Getting Started guide) is not counted — it never becomes a link.
|
|
66
|
+
*/
|
|
67
|
+
function wikilinkTargets(content: string): string[] {
|
|
68
|
+
const stripped = content
|
|
69
|
+
.replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length)) // fenced code
|
|
70
|
+
.replace(/`[^`]*`/g, (m) => " ".repeat(m.length)); // inline code
|
|
71
|
+
return [...stripped.matchAll(/\[\[([^\]]+)\]\]/g)].map((m) => m[1]!);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A note in a pack, by path (fails loudly if absent). */
|
|
75
|
+
function noteAt(pack: SeedPack, path: string): SeedPackNote {
|
|
76
|
+
const note = pack.notes.find((n) => n.path === path);
|
|
77
|
+
if (!note) throw new Error(`no note at path ${path}`);
|
|
78
|
+
return note;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("guide tag — the vault's skill-file tag", () => {
|
|
82
|
+
test("GUIDE_TAG carries the written_for enum, ai first (= default)", () => {
|
|
83
|
+
expect(GUIDE_TAG.name).toBe("guide");
|
|
84
|
+
// The description's last sentence names the schema field.
|
|
85
|
+
expect(GUIDE_TAG.description).toContain(
|
|
86
|
+
"`written_for` says who a guide is written for.",
|
|
87
|
+
);
|
|
88
|
+
const wf = GUIDE_TAG.fields?.written_for;
|
|
89
|
+
expect(wf).toBeDefined();
|
|
90
|
+
expect(wf!.type).toBe("string");
|
|
91
|
+
expect(wf!.enum).toEqual(["ai", "human", "both"]);
|
|
92
|
+
// First enum value is the schema default — guides lean AI.
|
|
93
|
+
expect(wf!.enum![0]).toBe("ai");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("PINNED_TAG is a plain identity tag (no schema)", () => {
|
|
97
|
+
expect(PINNED_TAG.name).toBe("pinned");
|
|
98
|
+
expect(PINNED_TAG.description).toBe(
|
|
99
|
+
"Notes pinned to the top of the Notes app.",
|
|
100
|
+
);
|
|
101
|
+
expect(PINNED_TAG.fields).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("welcome pack — notes-ui schema parity (containment)", () => {
|
|
106
|
+
test("welcome tags CONTAIN notes-ui's NOTES_REQUIRED_SCHEMA byte-for-byte", () => {
|
|
107
|
+
// The independent copy still matches, and the welcome pack still ships it.
|
|
52
108
|
expect(NOTES_REQUIRED_TAGS).toEqual(NOTES_UI_REQUIRED_SCHEMA_TAGS);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
expect(
|
|
109
|
+
for (const req of NOTES_UI_REQUIRED_SCHEMA_TAGS) {
|
|
110
|
+
const found = welcomePack().tags.find((t) => t.name === req.name);
|
|
111
|
+
expect(found).toBeDefined();
|
|
112
|
+
// Field-level byte pins (toEqual ignores key order; these don't).
|
|
113
|
+
expect(found!.name).toBe(req.name);
|
|
114
|
+
expect(found!.description).toBe(req.description);
|
|
115
|
+
expect(found!.parent_names).toBeUndefined();
|
|
58
116
|
}
|
|
59
117
|
});
|
|
60
118
|
|
|
119
|
+
test("welcome tags = capture + guide + pinned (the guide/pinned tags ride along)", () => {
|
|
120
|
+
expect(welcomePack().tags.map((t) => t.name)).toEqual([
|
|
121
|
+
"capture",
|
|
122
|
+
"guide",
|
|
123
|
+
"pinned",
|
|
124
|
+
]);
|
|
125
|
+
expect(welcomePack().tags).toContain(GUIDE_TAG);
|
|
126
|
+
expect(welcomePack().tags).toContain(PINNED_TAG);
|
|
127
|
+
});
|
|
128
|
+
|
|
61
129
|
test("exactly ONE capture tag — no subtype tags, no parent_names (2026-07-03)", () => {
|
|
62
130
|
// Entry method is metadata.source (text|voice), not taxonomy. The old
|
|
63
131
|
// capture/text + capture/voice subtypes must not creep back into the seed
|
|
@@ -68,65 +136,146 @@ describe("welcome pack — notes-ui schema parity", () => {
|
|
|
68
136
|
});
|
|
69
137
|
});
|
|
70
138
|
|
|
71
|
-
describe("welcome pack —
|
|
72
|
-
test("
|
|
139
|
+
describe("welcome pack — the five-guide ring", () => {
|
|
140
|
+
test("five guides at the ratified paths, in order", () => {
|
|
73
141
|
const pack = welcomePack();
|
|
74
142
|
expect(pack.name).toBe("welcome");
|
|
75
143
|
expect(pack.notes.map((n) => n.path)).toEqual([
|
|
76
144
|
WELCOME_PATH,
|
|
77
|
-
|
|
145
|
+
CAPTURE_ANYTHING_PATH,
|
|
146
|
+
TAGS_GRAPH_PATH,
|
|
78
147
|
CONNECT_AI_PATH,
|
|
148
|
+
YOURS_TO_KEEP_PATH,
|
|
79
149
|
]);
|
|
80
|
-
|
|
81
|
-
const [welcome, tryLinking, connectAi] = pack.notes;
|
|
82
|
-
expect(welcome!.content).toContain(`[[${TRY_LINKING_PATH}]]`);
|
|
83
|
-
expect(tryLinking!.content).toContain(`[[${WELCOME_PATH}]]`);
|
|
84
|
-
expect(connectAi!.content).toContain(`[[${WELCOME_PATH}]]`);
|
|
85
150
|
});
|
|
86
151
|
|
|
87
|
-
test("
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
const
|
|
92
|
-
|
|
152
|
+
test("the link web: welcome → all four; 2→3→4→5→1; connect → Getting Started", () => {
|
|
153
|
+
const pack = welcomePack();
|
|
154
|
+
|
|
155
|
+
// Welcome wikilinks to all four siblings.
|
|
156
|
+
const welcomeLinks = wikilinkTargets(noteAt(pack, WELCOME_PATH).content);
|
|
157
|
+
for (const target of [
|
|
158
|
+
CAPTURE_ANYTHING_PATH,
|
|
159
|
+
TAGS_GRAPH_PATH,
|
|
160
|
+
CONNECT_AI_PATH,
|
|
161
|
+
YOURS_TO_KEEP_PATH,
|
|
162
|
+
]) {
|
|
163
|
+
expect(welcomeLinks).toContain(target);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// The chain forward: 2→3, 3→4, 4→5, 5→1.
|
|
167
|
+
expect(wikilinkTargets(noteAt(pack, CAPTURE_ANYTHING_PATH).content)).toContain(
|
|
168
|
+
TAGS_GRAPH_PATH,
|
|
169
|
+
);
|
|
170
|
+
expect(wikilinkTargets(noteAt(pack, TAGS_GRAPH_PATH).content)).toContain(
|
|
171
|
+
CONNECT_AI_PATH,
|
|
172
|
+
);
|
|
173
|
+
expect(wikilinkTargets(noteAt(pack, CONNECT_AI_PATH).content)).toContain(
|
|
174
|
+
YOURS_TO_KEEP_PATH,
|
|
175
|
+
);
|
|
176
|
+
expect(wikilinkTargets(noteAt(pack, YOURS_TO_KEEP_PATH).content)).toContain(
|
|
177
|
+
WELCOME_PATH,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Connect your AI points the human at the AI-facing Getting Started note.
|
|
181
|
+
expect(wikilinkTargets(noteAt(pack, CONNECT_AI_PATH).content)).toContain(
|
|
182
|
+
GETTING_STARTED_PATH,
|
|
183
|
+
);
|
|
184
|
+
});
|
|
93
185
|
|
|
94
|
-
|
|
186
|
+
test("NO dangling wikilinks across the default seed (welcome + getting-started)", () => {
|
|
187
|
+
// Both packs auto-seed on create, so every [[target]] across their notes
|
|
188
|
+
// must resolve to a note one of them writes — including [[Getting Started]].
|
|
189
|
+
const seededPaths = new Set<string>([
|
|
190
|
+
...welcomePack().notes.map((n) => n.path),
|
|
191
|
+
...GETTING_STARTED_PACK.notes.map((n) => n.path),
|
|
192
|
+
]);
|
|
193
|
+
for (const pack of [welcomePack(), GETTING_STARTED_PACK]) {
|
|
194
|
+
for (const note of pack.notes) {
|
|
195
|
+
for (const target of wikilinkTargets(note.content)) {
|
|
196
|
+
expect(seededPaths.has(target)).toBe(true);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
95
201
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
202
|
+
test("every guide is tagged #guide with metadata.written_for = human", () => {
|
|
203
|
+
for (const note of welcomePack().notes) {
|
|
204
|
+
expect(note.tags).toContain("guide");
|
|
205
|
+
expect(note.metadata).toEqual({ written_for: "human" });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
101
208
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
expect(
|
|
209
|
+
test("Welcome is the ONLY pinned note", () => {
|
|
210
|
+
const pinned = welcomePack().notes.filter((n) => n.tags?.includes("pinned"));
|
|
211
|
+
expect(pinned.map((n) => n.path)).toEqual([WELCOME_PATH]);
|
|
212
|
+
// Welcome carries both guide + pinned.
|
|
213
|
+
expect(noteAt(welcomePack(), WELCOME_PATH).tags).toEqual(["guide", "pinned"]);
|
|
214
|
+
});
|
|
105
215
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
216
|
+
test("content anchors: the ratified copy is present (guards body drift)", () => {
|
|
217
|
+
const pack = welcomePack();
|
|
218
|
+
expect(noteAt(pack, WELCOME_PATH).content).toContain("This vault is yours.");
|
|
219
|
+
expect(noteAt(pack, WELCOME_PATH).content).toContain("tagged #guide");
|
|
220
|
+
expect(noteAt(pack, CAPTURE_ANYTHING_PATH).content).toContain(
|
|
221
|
+
"The one habit that makes a vault work",
|
|
222
|
+
);
|
|
223
|
+
expect(noteAt(pack, TAGS_GRAPH_PATH).content).toContain(
|
|
224
|
+
"Notes and links come first. Tags come later",
|
|
225
|
+
);
|
|
226
|
+
expect(noteAt(pack, YOURS_TO_KEEP_PATH).content).toContain(
|
|
227
|
+
"the moral center of the",
|
|
228
|
+
);
|
|
229
|
+
// Yours to keep carries the self-host export command (backtick-wrapped).
|
|
230
|
+
expect(noteAt(pack, YOURS_TO_KEEP_PATH).content).toContain(
|
|
231
|
+
"`parachute-vault export <dir>`",
|
|
232
|
+
);
|
|
109
233
|
});
|
|
110
234
|
|
|
111
|
-
test("
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
235
|
+
test("consoleOrigin renders both modes on the Connect-your-AI note", () => {
|
|
236
|
+
const origin = "https://cloud.parachute.computer";
|
|
237
|
+
|
|
238
|
+
// With origin: the console sentence names it, no `undefined`, no `at .`.
|
|
239
|
+
const withOrigin = noteAt(welcomePack({ consoleOrigin: origin }), CONNECT_AI_PATH);
|
|
240
|
+
expect(withOrigin.content).toContain(
|
|
241
|
+
`Grab the connection URL from your console at ${origin}.`,
|
|
242
|
+
);
|
|
243
|
+
expect(withOrigin.content).not.toContain("undefined");
|
|
244
|
+
|
|
245
|
+
// Without origin: the line stays generic — no baked loopback, no `at .`.
|
|
246
|
+
const generic = noteAt(welcomePack(), CONNECT_AI_PATH);
|
|
247
|
+
expect(generic.content).toContain(
|
|
248
|
+
"Grab the connection URL from your console.",
|
|
115
249
|
);
|
|
116
|
-
expect(
|
|
117
|
-
expect(
|
|
250
|
+
expect(generic.content).not.toContain("undefined");
|
|
251
|
+
expect(generic.content).not.toContain("at .");
|
|
252
|
+
expect(generic.content).not.toContain(origin);
|
|
118
253
|
});
|
|
119
254
|
});
|
|
120
255
|
|
|
121
256
|
describe("getting-started pack", () => {
|
|
122
|
-
test("carries the
|
|
257
|
+
test("carries the AI-facing guide note, tagged #guide / written_for ai", () => {
|
|
123
258
|
expect(GETTING_STARTED_PACK.name).toBe("getting-started");
|
|
124
|
-
|
|
259
|
+
// Declares the guide tag (self-sufficient — converges with the welcome pack).
|
|
260
|
+
expect(GETTING_STARTED_PACK.tags).toEqual([GUIDE_TAG]);
|
|
125
261
|
expect(GETTING_STARTED_PACK.notes).toEqual([
|
|
126
|
-
{
|
|
262
|
+
{
|
|
263
|
+
path: GETTING_STARTED_PATH,
|
|
264
|
+
tags: ["guide"],
|
|
265
|
+
metadata: { written_for: "ai" },
|
|
266
|
+
content: GETTING_STARTED_CONTENT,
|
|
267
|
+
},
|
|
127
268
|
]);
|
|
128
269
|
});
|
|
129
270
|
|
|
271
|
+
test("the Getting Started body is byte-unchanged", () => {
|
|
272
|
+
// The AI-facing doctrine copy is not touched by the ring rewrite.
|
|
273
|
+
expect(noteAt(GETTING_STARTED_PACK, GETTING_STARTED_PATH).content).toBe(
|
|
274
|
+
GETTING_STARTED_CONTENT,
|
|
275
|
+
);
|
|
276
|
+
expect(GETTING_STARTED_CONTENT.startsWith("# Getting Started\n")).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
130
279
|
test("no dangling [[Surface Starter]] wikilink — points at the add-pack flow instead", () => {
|
|
131
280
|
// Surface Starter is out of the default seed (ratified 2026-07-02), so the
|
|
132
281
|
// default-seeded guide must not carry a wikilink to a note that doesn't
|
|
@@ -141,11 +290,16 @@ describe("getting-started pack", () => {
|
|
|
141
290
|
});
|
|
142
291
|
|
|
143
292
|
describe("surface-starter pack", () => {
|
|
144
|
-
test("carries the surface guide
|
|
293
|
+
test("carries the surface guide, tagged #guide / written_for ai (opt-in, not default)", () => {
|
|
145
294
|
expect(SURFACE_STARTER_PACK.name).toBe("surface-starter");
|
|
146
|
-
expect(SURFACE_STARTER_PACK.tags).toEqual([]);
|
|
295
|
+
expect(SURFACE_STARTER_PACK.tags).toEqual([GUIDE_TAG]);
|
|
147
296
|
expect(SURFACE_STARTER_PACK.notes).toEqual([
|
|
148
|
-
{
|
|
297
|
+
{
|
|
298
|
+
path: SURFACE_STARTER_PATH,
|
|
299
|
+
tags: ["guide"],
|
|
300
|
+
metadata: { written_for: "ai" },
|
|
301
|
+
content: SURFACE_STARTER_CONTENT,
|
|
302
|
+
},
|
|
149
303
|
]);
|
|
150
304
|
expect(SURFACE_STARTER_PACK.description).toContain("not seeded by default");
|
|
151
305
|
});
|