@openparachute/vault 0.6.5-rc.14 → 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.
@@ -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 src/onboarding-seed.test.ts and CLI-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 pin: the `welcome` pack's tags must stay byte-equal to
9
- * notes-ui's NOTES_REQUIRED_SCHEMA the Notes PWA's connect-time audit
10
- * compares `description` verbatim, and its schema banner only clears when the
11
- * tags genuinely carry the semantics Notes declares. ONE tag since 2026-07-03:
12
- * `#capture` only entry method (text|voice) is note `metadata.source`
13
- * provenance, not taxonomy (sibling notes-ui PR carries the client side).
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
- TRY_LINKING_PATH,
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
- describe("welcome pack — notes-ui schema parity", () => {
51
- test("welcome tags are byte-equal to notes-ui's NOTES_REQUIRED_SCHEMA", () => {
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
- expect(welcomePack().tags).toEqual(NOTES_UI_REQUIRED_SCHEMA_TAGS);
54
- // Field-level byte pins (toEqual ignores key order; these don't).
55
- for (const [i, expected] of NOTES_UI_REQUIRED_SCHEMA_TAGS.entries()) {
56
- expect(NOTES_REQUIRED_TAGS[i]!.name).toBe(expected.name);
57
- expect(NOTES_REQUIRED_TAGS[i]!.description).toBe(expected.description);
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 — person-voiced welcome web", () => {
72
- test("three notes forming a linked web (welcome ⇄ try-linking, connect-AI → welcome)", () => {
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
- TRY_LINKING_PATH,
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("content with consoleOrigin is byte-equal to the cloud's welcome copy", () => {
88
- // Pinned against parachute-cloud workers/vault/src/welcome.ts
89
- // `welcomeNotes(consoleOrigin)` — the sibling cloud PR swaps that module
90
- // for this pack, so the ported content must not drift.
91
- const origin = "https://cloud.parachute.computer";
92
- const pack = welcomePack({ consoleOrigin: origin });
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
- expect(pack.notes[0]!.content).toBe(`# ${WELCOME_PATH}
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
- This vault is yours.
97
- Write anything.
98
- Notes can link to each other, like this: [[${TRY_LINKING_PATH}]].
99
- `);
100
- expect(pack.notes[1]!.content).toBe(`# ${TRY_LINKING_PATH}
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
- Wrap a note's name in double square brackets to make a wikilink, like this one back to [[${WELCOME_PATH}]].
103
- `);
104
- expect(pack.notes[2]!.content).toBe(`# ${CONNECT_AI_PATH}
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
- Your vault speaks MCP. Grab the connection URL from your console at ${origin}.
107
- Start from [[${WELCOME_PATH}]].
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("without consoleOrigin the Connect-AI line stays generic (no baked origin)", () => {
112
- const connectAi = welcomePack().notes[2]!;
113
- expect(connectAi.content).toContain(
114
- "Your vault speaks MCP. Grab the connection URL from your console.",
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(connectAi.content).not.toContain("undefined");
117
- expect(connectAi.content).not.toContain("at .");
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 doctrine note at the canonical path", () => {
257
+ test("carries the AI-facing guide note, tagged #guide / written_for ai", () => {
123
258
  expect(GETTING_STARTED_PACK.name).toBe("getting-started");
124
- expect(GETTING_STARTED_PACK.tags).toEqual([]);
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
- { path: GETTING_STARTED_PATH, content: GETTING_STARTED_CONTENT },
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 at the canonical path (opt-in, not default)", () => {
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
- { path: SURFACE_STARTER_PATH, content: SURFACE_STARTER_CONTENT },
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
  });
@@ -5,8 +5,9 @@
5
5
  * A pack is a named bundle of `tags` (upserted) + `notes` (created only when
6
6
  * no note already lives at the path). Three packs ship today:
7
7
  *
8
- * - `welcome` — the person-voiced three-note welcome web + the `capture`
9
- * tag the Notes surface expects. Default-seeded on vault creation.
8
+ * - `welcome` — the five-guide welcome ring (Welcome, Capture anything, Tags
9
+ * and the graph, Connect your AI, Yours to keep) + the `capture` / `guide`
10
+ * / `pinned` tags the Notes surface expects. Default-seeded on creation.
10
11
  * - `getting-started` — the AI-facing start-here guide (SKILL.md-style
11
12
  * doctrine addressed to a connected assistant). Default-seeded.
12
13
  * - `surface-starter` — the living starter guide for building a custom
@@ -14,6 +15,11 @@
14
15
  * Surface Starter is out of the default seed) — added on demand via
15
16
  * `parachute-vault add-pack surface-starter` or a console affordance.
16
17
  *
18
+ * Guides are the vault's skill files — the Parachute equivalent of a
19
+ * `SKILL.md`. They're tagged `#guide` (GUIDE_TAG), and a `written_for` schema
20
+ * field on that tag records who each guide is written for (`ai` | `human` |
21
+ * `both`; `ai` first = the default, since guides lean AI).
22
+ *
17
23
  * This module is the single source of truth for pack content across BOTH
18
24
  * runtimes: the bun vault (`src/onboarding-seed.ts` default seed + the
19
25
  * `add-pack` CLI verb) and the cloud vault DO (parachute-cloud
@@ -26,23 +32,37 @@
26
32
  * Getting Started / Surface Starter path + content constants.
27
33
  */
28
34
 
29
- import type { Store } from "./types.ts";
35
+ import type { Store, TagFieldSchema } from "./types.ts";
30
36
 
31
37
  // ---------------------------------------------------------------------------
32
38
  // Pack shape
33
39
  // ---------------------------------------------------------------------------
34
40
 
35
- /** A tag declaration a pack upserts (identity row, not a schema migration). */
41
+ /**
42
+ * A tag declaration a pack upserts (identity row, not a schema migration).
43
+ *
44
+ * `fields` carries an optional typed-metadata schema — the same
45
+ * `Record<field, { type, enum?, indexed? }>` shape `update-tag` accepts — so a
46
+ * pack can declare a schema-carrying tag (e.g. GUIDE_TAG's `written_for`
47
+ * enum). The applier passes it straight through to `upsertTagRecord`.
48
+ */
36
49
  export interface SeedPackTag {
37
50
  name: string;
38
51
  description: string;
39
52
  parent_names?: string[];
53
+ fields?: Record<string, TagFieldSchema>;
40
54
  }
41
55
 
42
- /** A note a pack seeds — created only when no note exists at `path`. */
56
+ /**
57
+ * A note a pack seeds — created only when no note exists at `path`. `tags` +
58
+ * `metadata` are applied at create time (both runtimes' `createNote` accepts
59
+ * them); a guide note carries `tags: ["guide"]` + `metadata: { written_for }`.
60
+ */
43
61
  export interface SeedPackNote {
44
62
  path: string;
45
63
  content: string;
64
+ tags?: string[];
65
+ metadata?: Record<string, unknown>;
46
66
  }
47
67
 
48
68
  /** A named bundle of starter tags + notes. */
@@ -83,58 +103,188 @@ export const NOTES_REQUIRED_TAGS: ReadonlyArray<SeedPackTag> = [
83
103
  },
84
104
  ];
85
105
 
106
+ /**
107
+ * The `guide` tag — the vault's skill-file tag. Guides are the Parachute
108
+ * equivalent of a `SKILL.md`: notes that teach a connected AI (and the human)
109
+ * how this vault works and how to work it. The `written_for` schema field says
110
+ * who a guide is written for; `ai` is the enum's FIRST value, so it's the
111
+ * schema default — guides lean AI.
112
+ *
113
+ * Declared by every pack that ships guide notes (welcome / getting-started /
114
+ * surface-starter). The upserts converge on one row, so each pack stays
115
+ * self-sufficient — applying any one of them alone still lands the tag.
116
+ */
117
+ export const GUIDE_TAG: SeedPackTag = {
118
+ name: "guide",
119
+ description:
120
+ "Guides — the vault's skill files. Notes that teach a connected AI (and the human) how this vault works and how to work it. `written_for` says who a guide is written for.",
121
+ fields: {
122
+ // First enum value is the schema default — guides lean AI.
123
+ written_for: { type: "string", enum: ["ai", "human", "both"] },
124
+ },
125
+ };
126
+
127
+ /** The `pinned` tag — notes pinned to the top of the Notes app. No schema. */
128
+ export const PINNED_TAG: SeedPackTag = {
129
+ name: "pinned",
130
+ description: "Notes pinned to the top of the Notes app.",
131
+ };
132
+
86
133
  export const WELCOME_PATH = "Welcome to your vault 🪂";
87
- export const TRY_LINKING_PATH = "Try linking notes";
134
+ export const CAPTURE_ANYTHING_PATH = "Capture anything";
135
+ export const TAGS_GRAPH_PATH = "Tags and the graph";
88
136
  export const CONNECT_AI_PATH = "Connect your AI";
137
+ export const YOURS_TO_KEEP_PATH = "Yours to keep";
89
138
 
90
139
  /**
91
- * Build the `welcome` pack: a three-note welcome web (welcome try-linking →
92
- * back, connect-AI welcome) so the graph view shows a connected structure
93
- * from minute one, plus the `capture` tag above. The notes are ordinary
94
- * notes no special flags, deletable like anything else.
140
+ * Build the `welcome` pack: the five-guide welcome ring Welcome, Capture
141
+ * anything, Tags and the graph, Connect your AI, Yours to keep — plus the
142
+ * `capture` / `guide` / `pinned` tags. The guides are ordinary notes tagged
143
+ * `#guide` (the vault's skill-file tag), each `metadata.written_for: "human"`;
144
+ * Welcome is `#pinned` so it sits at the top of the Notes app. They form a
145
+ * small linked web (Welcome → all four; the rest chain 2→3→4→5→1; Connect
146
+ * your AI also links the AI-facing [[Getting Started]]) so the graph view
147
+ * shows a connected structure from minute one. Deletable like anything else.
95
148
  *
96
- * Content is ported EXACTLY from parachute-cloud workers/vault/src/welcome.ts
97
- * (person-voiced, addressed to the everyday user). `consoleOrigin` is the
98
- * origin of the operator's console (the cloud console, or a hub portal) —
99
- * when known, the Connect-your-AI note names it; when omitted (the bun vault
100
- * seeds at create time, often pre-expose, and must never bake in a loopback
101
- * origin) the line stays generic.
149
+ * These five bodies are the ratified guides (already live at
150
+ * parachute.computer/guides). `consoleOrigin` is the origin of the operator's
151
+ * console (the cloud console, or a hub portal) — when known, the Connect-your-AI
152
+ * note names it; when omitted (the bun vault seeds at create time, often
153
+ * pre-expose, and must never bake in a loopback origin) the line stays generic.
102
154
  */
103
155
  export function welcomePack(opts: { consoleOrigin?: string } = {}): SeedPack {
104
156
  const consoleLine = opts.consoleOrigin
105
157
  ? `Grab the connection URL from your console at ${opts.consoleOrigin}.`
106
158
  : `Grab the connection URL from your console.`;
159
+ const guideHuman = { tags: ["guide"], metadata: { written_for: "human" } };
107
160
  return {
108
161
  name: "welcome",
109
162
  description:
110
- "A small linked welcome web (three notes) + the capture tag the Notes surface uses. Seeded by default on new vaults.",
111
- tags: NOTES_REQUIRED_TAGS,
163
+ "The five-guide welcome ring (Welcome, Capture anything, Tags and the graph, Connect your AI, Yours to keep) the vault's #guide skill files — plus the capture/guide/pinned tags. Seeded by default on new vaults.",
164
+ // capture (Notes surface) + guide (the skill-file tag, carries the
165
+ // written_for schema) + pinned (top-of-app). Upserts are idempotent.
166
+ tags: [...NOTES_REQUIRED_TAGS, GUIDE_TAG, PINNED_TAG],
112
167
  // `[[wikilinks]]` resolve by note path — pending links auto-resolve when
113
168
  // the target is created, so order only affects how briefly a link sits
114
- // unresolved during the seed.
169
+ // unresolved during the seed. Every [[target]] here resolves to a note the
170
+ // default seed writes (the four siblings + [[Getting Started]] from the
171
+ // getting-started pack) — no dangling links on a fresh vault.
115
172
  notes: [
116
173
  {
117
174
  path: WELCOME_PATH,
175
+ tags: ["guide", "pinned"],
176
+ metadata: { written_for: "human" },
118
177
  content: `# ${WELCOME_PATH}
119
178
 
120
179
  This vault is yours.
121
- Write anything.
122
- Notes can link to each other, like this: [[${TRY_LINKING_PATH}]].
180
+
181
+ Write anything — ideas, people, plans, wifi passwords. Notes can link to each
182
+ other, like this: [[${CAPTURE_ANYTHING_PATH}]]. Follow that link and keep going — these
183
+ guides form a small web you can wander:
184
+
185
+ - [[${CAPTURE_ANYTHING_PATH}]] — get thoughts in and link them together
186
+ - [[${TAGS_GRAPH_PATH}]] — structure that grows out of what you write
187
+ - [[${CONNECT_AI_PATH}]] — any AI can read and write your vault
188
+ - [[${YOURS_TO_KEEP_PATH}]] — export everything, anytime
189
+
190
+ They're ordinary notes, tagged #guide. Edit them, add to them, or delete the
191
+ lot — the vault is yours, remember.
192
+ `,
193
+ },
194
+ {
195
+ path: CAPTURE_ANYTHING_PATH,
196
+ ...guideHuman,
197
+ content: `# ${CAPTURE_ANYTHING_PATH}
198
+
199
+ The one habit that makes a vault work: when a thought strikes, write it down.
200
+ Don't sort it. Don't file it. Let it land.
201
+
202
+ A note can be two words or two pages. The blog idea on a trail, the follow-up
203
+ from a meeting, the dream at 2am — capture first. Organizing can happen later,
204
+ or never; that's what tags and your AI are for.
205
+
206
+ To connect two notes, wrap a note's name in double square brackets:
207
+ [[${WELCOME_PATH}]] is a link back to where you started. If the note
208
+ doesn't exist yet, the link waits patiently and connects the moment it does.
209
+
210
+ That's the whole trick. Storage is easy — connection is everything. A linked
211
+ note is a thought your future self, and your AI, can find again.
212
+
213
+ Next: [[${TAGS_GRAPH_PATH}]].
123
214
  `,
124
215
  },
125
216
  {
126
- path: TRY_LINKING_PATH,
127
- content: `# ${TRY_LINKING_PATH}
217
+ path: TAGS_GRAPH_PATH,
218
+ ...guideHuman,
219
+ content: `# ${TAGS_GRAPH_PATH}
220
+
221
+ Notes and links come first. Tags come later, when you notice a pattern.
222
+
223
+ A tag answers "what kind of thing is this?" — #person, #recipe, #idea. Tag a
224
+ few notes the same way and a view lights up: all your people, all your recipes,
225
+ everything half-finished. Your vault starts with just two:
128
226
 
129
- Wrap a note's name in double square brackets to make a wikilink, like this one back to [[${WELCOME_PATH}]].
227
+ - #capture the Notes app puts it on whatever you write or speak in directly,
228
+ marking your own raw words.
229
+ - #guide — these notes.
230
+
231
+ Underneath, everything is one graph: every note a node, every link an edge.
232
+ Open the graph view and watch it grow; ask a connected AI "what's near this
233
+ note?" and it walks the edges for you.
234
+
235
+ The structure isn't a filing system you design on day one. It grows out of what
236
+ you actually write — so don't over-organize an empty vault. Write, link, and
237
+ let the shape appear.
238
+
239
+ Next: [[${CONNECT_AI_PATH}]].
130
240
  `,
131
241
  },
132
242
  {
133
243
  path: CONNECT_AI_PATH,
244
+ ...guideHuman,
134
245
  content: `# ${CONNECT_AI_PATH}
135
246
 
136
- Your vault speaks MCP. ${consoleLine}
137
- Start from [[${WELCOME_PATH}]].
247
+ Your vault speaks MCP — an open standard — so any AI can read and write it:
248
+ Claude, ChatGPT, Claude Code, Cursor, or an agent you build. ${consoleLine}
249
+
250
+ Once you're connected, try:
251
+
252
+ - "Read the Getting Started note and help me set this vault up."
253
+ - "What did I want to write about?"
254
+ - "Tag my untagged notes."
255
+
256
+ Your AI sees what you see — the same notes, links, and tags — and what it
257
+ writes lands here, as notes you can read, edit, or delete. There's even a note
258
+ in this vault written for it: [[${GETTING_STARTED_PATH}]], the vault-design brief your
259
+ AI reads so you don't have to.
260
+
261
+ One memory, shared with every AI you choose to connect. That's the point.
262
+
263
+ Next: [[${YOURS_TO_KEEP_PATH}]].
264
+ `,
265
+ },
266
+ {
267
+ path: YOURS_TO_KEEP_PATH,
268
+ ...guideHuman,
269
+ content: `# ${YOURS_TO_KEEP_PATH}
270
+
271
+ Everything here — every note, tag, and link — exports as plain markdown files
272
+ with the structure intact. On Parachute Cloud, it's the Export button in your
273
+ console. Self-hosting, it's one command: \`parachute-vault export <dir>\`.
274
+
275
+ What you get is a folder any app can open, and another Parachute can import
276
+ losslessly. Move from our cloud to your own machine, or the other way — same
277
+ software, same format, nothing held hostage between the doors.
278
+
279
+ We build it this way on purpose. A connection you can't leave isn't a
280
+ relationship. You can always leave — which is exactly why you can trust
281
+ staying.
282
+
283
+ Export isn't a feature we grudgingly support; it's the moral center of the
284
+ design. We test it constantly, because a promise you don't test is just a
285
+ hope.
286
+
287
+ Back to [[${WELCOME_PATH}]].
138
288
  `,
139
289
  },
140
290
  ],
@@ -313,8 +463,17 @@ export const GETTING_STARTED_PACK: SeedPack = {
313
463
  name: "getting-started",
314
464
  description:
315
465
  "The AI-facing start-here guide: vault design vocabulary (tags/paths/schemas), imports, write gotchas. Seeded by default on new vaults.",
316
- tags: [],
317
- notes: [{ path: GETTING_STARTED_PATH, content: GETTING_STARTED_CONTENT }],
466
+ // Declares GUIDE_TAG too — each pack is self-sufficient; the upsert converges
467
+ // with the welcome pack's. This guide is written FOR the AI.
468
+ tags: [GUIDE_TAG],
469
+ notes: [
470
+ {
471
+ path: GETTING_STARTED_PATH,
472
+ tags: ["guide"],
473
+ metadata: { written_for: "ai" },
474
+ content: GETTING_STARTED_CONTENT,
475
+ },
476
+ ],
318
477
  };
319
478
 
320
479
  // ---------------------------------------------------------------------------
@@ -450,8 +609,17 @@ export const SURFACE_STARTER_PACK: SeedPack = {
450
609
  name: "surface-starter",
451
610
  description:
452
611
  "A living starter guide for building a custom surface (UI) over this vault with the published surface packages. Opt-in — not seeded by default.",
453
- tags: [],
454
- notes: [{ path: SURFACE_STARTER_PATH, content: SURFACE_STARTER_CONTENT }],
612
+ // A guide too (written for the AI) — declares GUIDE_TAG so the opt-in
613
+ // add-pack path lands the tag even if this pack is applied on its own.
614
+ tags: [GUIDE_TAG],
615
+ notes: [
616
+ {
617
+ path: SURFACE_STARTER_PATH,
618
+ tags: ["guide"],
619
+ metadata: { written_for: "ai" },
620
+ content: SURFACE_STARTER_CONTENT,
621
+ },
622
+ ],
455
623
  };
456
624
 
457
625
  // ---------------------------------------------------------------------------
@@ -541,18 +709,23 @@ export async function applySeedPack(
541
709
  await store.upsertTagRecord(decl.name, {
542
710
  description: decl.description,
543
711
  ...(decl.parent_names ? { parent_names: decl.parent_names } : {}),
712
+ ...(decl.fields ? { fields: decl.fields } : {}),
544
713
  });
545
714
  result.tags.push(decl.name);
546
715
  }
547
716
 
548
- for (const { path, content } of pack.notes) {
549
- const existing = await store.getNoteByPath(path);
717
+ for (const note of pack.notes) {
718
+ const existing = await store.getNoteByPath(note.path);
550
719
  if (existing) {
551
- result.skippedNotes.push(path);
720
+ result.skippedNotes.push(note.path);
552
721
  continue;
553
722
  }
554
- await store.createNote(content, { path });
555
- result.seededNotes.push(path);
723
+ await store.createNote(note.content, {
724
+ path: note.path,
725
+ tags: note.tags,
726
+ metadata: note.metadata,
727
+ });
728
+ result.seededNotes.push(note.path);
556
729
  }
557
730
 
558
731
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openparachute/vault",
3
- "version": "0.6.5-rc.14",
3
+ "version": "0.6.5-rc.15",
4
4
  "description": "Agent-native knowledge graph. Notes, tags, links over MCP.",
5
5
  "module": "src/cli.ts",
6
6
  "type": "module",
@@ -23,24 +23,29 @@ import { BunStore } from "./vault-store.ts";
23
23
  import { seedOnboardingNotes } from "./onboarding-seed.ts";
24
24
  import {
25
25
  applySeedPack,
26
+ CAPTURE_ANYTHING_PATH,
26
27
  CONNECT_AI_PATH,
27
28
  GETTING_STARTED_PATH,
28
29
  NOTES_REQUIRED_TAGS,
29
30
  SURFACE_STARTER_PACK,
30
31
  SURFACE_STARTER_PATH,
31
- TRY_LINKING_PATH,
32
+ TAGS_GRAPH_PATH,
32
33
  WELCOME_PATH,
34
+ YOURS_TO_KEEP_PATH,
33
35
  } from "../core/src/seed-packs.ts";
34
36
  import {
35
37
  buildVaultProjection,
36
38
  projectionToMarkdown,
37
39
  } from "../core/src/vault-projection.ts";
38
40
 
39
- /** Every note path the default seed writes on a blank vault. */
41
+ /** Every note path the default seed writes on a blank vault (the five-guide
42
+ * welcome ring + the AI-facing Getting Started). */
40
43
  const DEFAULT_SEED_PATHS = [
41
44
  WELCOME_PATH,
42
- TRY_LINKING_PATH,
45
+ CAPTURE_ANYTHING_PATH,
46
+ TAGS_GRAPH_PATH,
43
47
  CONNECT_AI_PATH,
48
+ YOURS_TO_KEEP_PATH,
44
49
  GETTING_STARTED_PATH,
45
50
  ];
46
51
 
@@ -80,15 +85,24 @@ describe("seedOnboardingNotes — default packs (welcome + getting-started)", ()
80
85
  expect(gs!.content).toContain("parachute-vault import");
81
86
  expect(gs!.content).toContain("Adapt this note");
82
87
 
83
- // The welcome web: three person-voiced notes.
84
- for (const path of [WELCOME_PATH, TRY_LINKING_PATH, CONNECT_AI_PATH]) {
88
+ // The welcome ring: five person-voiced guide notes.
89
+ for (const path of [
90
+ WELCOME_PATH,
91
+ CAPTURE_ANYTHING_PATH,
92
+ TAGS_GRAPH_PATH,
93
+ CONNECT_AI_PATH,
94
+ YOURS_TO_KEEP_PATH,
95
+ ]) {
85
96
  expect(await store.getNoteByPath(path)).not.toBeNull();
86
97
  }
87
98
  });
88
99
 
89
- test("seeds the ONE capture tag Notes requires (byte-equal semantics)", async () => {
100
+ test("seeds the capture tag Notes requires (byte-equal semantics) + the guide/pinned tags", async () => {
90
101
  const result = await seedOnboardingNotes(store);
91
- expect(result.tags).toEqual(["capture"]);
102
+ // welcome upserts [capture, guide, pinned]; getting-started re-upserts
103
+ // [guide] (converges). applySeedPack reports every declared tag per pack,
104
+ // so the concatenation carries guide twice — deterministic.
105
+ expect(result.tags).toEqual(["capture", "guide", "pinned", "guide"]);
92
106
 
93
107
  for (const decl of NOTES_REQUIRED_TAGS) {
94
108
  const record = await store.getTagRecord(decl.name);
@@ -97,6 +111,14 @@ describe("seedOnboardingNotes — default packs (welcome + getting-started)", ()
97
111
  expect(record!.parent_names ?? []).toEqual(decl.parent_names ?? []);
98
112
  }
99
113
 
114
+ // The guide tag lands with its written_for enum schema (applier passes
115
+ // `fields` through to upsertTagRecord). "ai" is the default (first value).
116
+ const guide = await store.getTagRecord("guide");
117
+ expect(guide).not.toBeNull();
118
+ expect(guide!.fields?.written_for?.enum).toEqual(["ai", "human", "both"]);
119
+ const pinned = await store.getTagRecord("pinned");
120
+ expect(pinned).not.toBeNull();
121
+
100
122
  // The retired subtype tags are NOT seeded — entry method is note
101
123
  // metadata.source (text|voice), not taxonomy (2026-07-03). Existing
102
124
  // vaults carrying these tags stay valid; new vaults don't get them.
@@ -104,6 +126,32 @@ describe("seedOnboardingNotes — default packs (welcome + getting-started)", ()
104
126
  expect(await store.getTagRecord("capture/voice")).toBeNull();
105
127
  });
106
128
 
129
+ test("seeded guides carry #guide + metadata.written_for (create-time tag/metadata write-through)", async () => {
130
+ await seedOnboardingNotes(store);
131
+
132
+ // Human-facing welcome guide: tagged guide + pinned, written_for human.
133
+ const welcome = await store.getNoteByPath(WELCOME_PATH);
134
+ expect([...(welcome!.tags ?? [])].sort()).toEqual(["guide", "pinned"]);
135
+ expect(welcome!.metadata?.written_for).toBe("human");
136
+
137
+ // The four sibling guides: guide only, written_for human.
138
+ for (const path of [
139
+ CAPTURE_ANYTHING_PATH,
140
+ TAGS_GRAPH_PATH,
141
+ CONNECT_AI_PATH,
142
+ YOURS_TO_KEEP_PATH,
143
+ ]) {
144
+ const note = await store.getNoteByPath(path);
145
+ expect(note!.tags).toEqual(["guide"]);
146
+ expect(note!.metadata?.written_for).toBe("human");
147
+ }
148
+
149
+ // The AI-facing Getting Started guide: guide, written_for ai.
150
+ const gs = await store.getNoteByPath(GETTING_STARTED_PATH);
151
+ expect(gs!.tags).toEqual(["guide"]);
152
+ expect(gs!.metadata?.written_for).toBe("ai");
153
+ });
154
+
107
155
  test("GAP 4: Getting Started shows a concrete update-tag fields example + write gotchas", async () => {
108
156
  await seedOnboardingNotes(store);
109
157
  const gs = await store.getNoteByPath(GETTING_STARTED_PATH);
@@ -121,24 +169,36 @@ describe("seedOnboardingNotes — default packs (welcome + getting-started)", ()
121
169
  expect(gs!.content).toContain("first listed value");
122
170
  });
123
171
 
124
- test("A3 (reshaped): the welcome web's wikilinks resolve; Getting Started has no dangling Surface Starter link", async () => {
172
+ test("A3 (reshaped): the welcome ring resolves to real link edges; Getting Started has no dangling Surface Starter link", async () => {
125
173
  await seedOnboardingNotes(store);
126
174
 
127
- // welcome → try-linking, try-linking → welcome, connect-AI → welcome.
128
175
  const welcome = await store.getNoteByPath(WELCOME_PATH);
129
- const tryLinking = await store.getNoteByPath(TRY_LINKING_PATH);
176
+ const capture = await store.getNoteByPath(CAPTURE_ANYTHING_PATH);
177
+ const tags = await store.getNoteByPath(TAGS_GRAPH_PATH);
130
178
  const connectAi = await store.getNoteByPath(CONNECT_AI_PATH);
179
+ const yours = await store.getNoteByPath(YOURS_TO_KEEP_PATH);
180
+ const gs = await store.getNoteByPath(GETTING_STARTED_PATH);
131
181
 
132
- const welcomeOut = await store.getLinks(welcome!.id, { direction: "outbound" });
133
- expect(welcomeOut.some((l) => l.targetId === tryLinking!.id)).toBe(true);
134
- const tryOut = await store.getLinks(tryLinking!.id, { direction: "outbound" });
135
- expect(tryOut.some((l) => l.targetId === welcome!.id)).toBe(true);
136
- const connectOut = await store.getLinks(connectAi!.id, { direction: "outbound" });
137
- expect(connectOut.some((l) => l.targetId === welcome!.id)).toBe(true);
182
+ const outbound = async (id: string) =>
183
+ (await store.getLinks(id, { direction: "outbound" })).map((l) => l.targetId);
184
+
185
+ // Welcome all four siblings.
186
+ const welcomeOut = await outbound(welcome!.id);
187
+ for (const t of [capture!.id, tags!.id, connectAi!.id, yours!.id]) {
188
+ expect(welcomeOut).toContain(t);
189
+ }
190
+ // The chain forward: capture → tags → connect → yours → welcome.
191
+ expect(await outbound(capture!.id)).toContain(tags!.id);
192
+ expect(await outbound(tags!.id)).toContain(connectAi!.id);
193
+ const connectOut = await outbound(connectAi!.id);
194
+ expect(connectOut).toContain(yours!.id);
195
+ // Connect your AI also links the AI-facing Getting Started note (seeded by
196
+ // the getting-started pack) — the ring's cross-link resolves, no dangle.
197
+ expect(connectOut).toContain(gs!.id);
198
+ expect(await outbound(yours!.id)).toContain(welcome!.id);
138
199
 
139
200
  // Getting Started mentions the surface-starter PACK — not a wikilink to a
140
201
  // note that the default seed no longer creates.
141
- const gs = await store.getNoteByPath(GETTING_STARTED_PATH);
142
202
  expect(gs!.content).not.toContain("[[Surface Starter]]");
143
203
  expect(gs!.content).toContain("add-pack surface-starter");
144
204
  });
@@ -179,7 +239,8 @@ describe("applySeedPack — surface-starter via add-pack", () => {
179
239
  expect(result.pack).toBe("surface-starter");
180
240
  expect(result.seededNotes).toEqual([SURFACE_STARTER_PATH]);
181
241
  expect(result.skippedNotes).toEqual([]);
182
- expect(result.tags).toEqual([]);
242
+ // surface-starter now declares GUIDE_TAG (it's a guide, written_for ai).
243
+ expect(result.tags).toEqual(["guide"]);
183
244
 
184
245
  const ss = await store.getNoteByPath(SURFACE_STARTER_PATH);
185
246
  expect(ss).not.toBeNull();
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Seed the default packs on vault creation.
3
3
  *
4
- * A freshly-created vault gets the `welcome` pack (the person-voiced
5
- * three-note welcome web + the `capture` tag the Notes surface expects) and
4
+ * A freshly-created vault gets the `welcome` pack (the five-guide welcome ring
5
+ * + the `capture` / `guide` / `pinned` tags the Notes surface expects) and
6
6
  * the `getting-started` pack (the AI-legible start-here doctrine) — so the
7
7
  * first minute shows a small living graph, and a connected AI can read the
8
8
  * guide and help the operator set the vault up.
@@ -358,11 +358,11 @@ describe("vault create — services.json registration (#208)", () => {
358
358
  * Default-pack seeding on create (originally demo-prep Workstream A — A1/A3;
359
359
  * reshaped for named seed packs).
360
360
  *
361
- * A freshly-created vault must contain the `welcome` pack (three-note welcome
362
- * web + the one capture tag Notes requires) and the `getting-started` guide — and
363
- * NOT the `surface-starter` pack, which is opt-in via `add-pack` (ratified
364
- * 2026-07-02). Idempotent + best-effort: the seed never fails a create and
365
- * never clobbers an edited note.
361
+ * A freshly-created vault must contain the `welcome` pack (the five-guide
362
+ * welcome ring + the capture/guide/pinned tags) and the `getting-started`
363
+ * guide — and NOT the `surface-starter` pack, which is opt-in via `add-pack`
364
+ * (ratified 2026-07-02). Idempotent + best-effort: the seed never fails a
365
+ * create and never clobbers an edited note.
366
366
  */
367
367
  describe("vault create — default pack seeding (welcome + getting-started)", () => {
368
368
  /** Read all (path, content) rows from a created vault's SQLite DB. */
@@ -400,23 +400,28 @@ describe("vault create — default pack seeding (welcome + getting-started)", ()
400
400
  const notes = readNotes("guided");
401
401
  const gs = notes.find((n) => n.path === "Getting Started");
402
402
  const welcome = notes.find((n) => n.path === "Welcome to your vault 🪂");
403
- const tryLinking = notes.find((n) => n.path === "Try linking notes");
403
+ const captureAnything = notes.find((n) => n.path === "Capture anything");
404
+ const tagsGraph = notes.find((n) => n.path === "Tags and the graph");
404
405
  const connectAi = notes.find((n) => n.path === "Connect your AI");
406
+ const yoursToKeep = notes.find((n) => n.path === "Yours to keep");
405
407
  expect(gs).toBeDefined();
406
408
  expect(welcome).toBeDefined();
407
- expect(tryLinking).toBeDefined();
409
+ expect(captureAnything).toBeDefined();
410
+ expect(tagsGraph).toBeDefined();
408
411
  expect(connectAi).toBeDefined();
412
+ expect(yoursToKeep).toBeDefined();
409
413
  expect(gs!.content).toContain("# Getting Started");
410
414
  // Surface Starter is out of the default seed — no note, no dangling link.
411
415
  expect(notes.find((n) => n.path === "Surface Starter")).toBeUndefined();
412
416
  expect(gs!.content).not.toContain("[[Surface Starter]]");
413
417
  expect(gs!.content).toContain("add-pack surface-starter");
414
418
 
415
- // The ONE capture tag Notes requires arrives with the welcome pack
416
- // and nothing else (fresh-vault seed = 4 notes + 1 tag; the retired
419
+ // The capture tag Notes requires arrives with the welcome pack, alongside
420
+ // the guide (skill-file) + pinned tags and nothing else (fresh-vault
421
+ // seed = 6 notes: the 5-guide ring + Getting Started. The retired
417
422
  // capture/text + capture/voice subtypes are no longer seeded).
418
- expect(readTagNames("guided")).toEqual(["capture"]);
419
- expect(notes).toHaveLength(4);
423
+ expect(readTagNames("guided").sort()).toEqual(["capture", "guide", "pinned"]);
424
+ expect(notes).toHaveLength(6);
420
425
  });
421
426
 
422
427
  test("seeding doesn't break --json stdout (notes seeded silently)", () => {