@openparachute/vault 0.7.2 → 0.7.3-rc.10

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 (74) hide show
  1. package/README.md +5 -3
  2. package/core/src/attachment/policy.test.ts +66 -0
  3. package/core/src/attachment/policy.ts +131 -0
  4. package/core/src/attachment/tickets.test.ts +45 -0
  5. package/core/src/attachment/tickets.ts +117 -0
  6. package/core/src/attachment-tickets-tool.test.ts +286 -0
  7. package/core/src/conformance.ts +2 -1
  8. package/core/src/contract-typed-index.test.ts +4 -3
  9. package/core/src/core.test.ts +607 -11
  10. package/core/src/display-title.test.ts +190 -0
  11. package/core/src/embedding/chunker.test.ts +97 -0
  12. package/core/src/embedding/chunker.ts +180 -0
  13. package/core/src/embedding/provider.ts +108 -0
  14. package/core/src/embedding/staleness.test.ts +87 -0
  15. package/core/src/embedding/staleness.ts +83 -0
  16. package/core/src/embedding/vector-codec.test.ts +99 -0
  17. package/core/src/embedding/vector-codec.ts +60 -0
  18. package/core/src/embedding/vectors.test.ts +163 -0
  19. package/core/src/embedding/vectors.ts +135 -0
  20. package/core/src/expand.ts +11 -3
  21. package/core/src/indexed-fields.test.ts +9 -3
  22. package/core/src/indexed-fields.ts +9 -1
  23. package/core/src/lede.test.ts +96 -0
  24. package/core/src/mcp-semantic-search.test.ts +160 -0
  25. package/core/src/mcp.ts +405 -10
  26. package/core/src/notes.semantic-search.test.ts +131 -0
  27. package/core/src/notes.ts +319 -7
  28. package/core/src/query-warnings.ts +40 -0
  29. package/core/src/schema-defaults.ts +85 -1
  30. package/core/src/schema-v27-note-vectors.test.ts +178 -0
  31. package/core/src/schema.ts +81 -1
  32. package/core/src/search-fts-v25.test.ts +9 -1
  33. package/core/src/search-query.test.ts +42 -0
  34. package/core/src/search-query.ts +27 -0
  35. package/core/src/search-title-boost.test.ts +125 -0
  36. package/core/src/seed-packs.test.ts +117 -1
  37. package/core/src/seed-packs.ts +217 -1
  38. package/core/src/store.semantic-search.test.ts +236 -0
  39. package/core/src/store.ts +162 -5
  40. package/core/src/tag-schemas.ts +27 -12
  41. package/core/src/types.ts +63 -1
  42. package/core/src/vault-projection.ts +48 -0
  43. package/package.json +6 -1
  44. package/src/add-pack.test.ts +32 -0
  45. package/src/attachment-tickets.test.ts +350 -0
  46. package/src/attachment-tickets.ts +264 -0
  47. package/src/auth.ts +8 -2
  48. package/src/cli.ts +5 -1
  49. package/src/contract-errors.test.ts +2 -1
  50. package/src/contract-honest-queries.test.ts +88 -0
  51. package/src/contract-search.test.ts +41 -0
  52. package/src/embedding/capability.test.ts +33 -0
  53. package/src/embedding/capability.ts +34 -0
  54. package/src/embedding/external-api.test.ts +154 -0
  55. package/src/embedding/external-api.ts +144 -0
  56. package/src/embedding/onnx-transformers.test.ts +113 -0
  57. package/src/embedding/onnx-transformers.ts +141 -0
  58. package/src/embedding/select.test.ts +99 -0
  59. package/src/embedding/select.ts +92 -0
  60. package/src/embedding-worker.test.ts +300 -0
  61. package/src/embedding-worker.ts +226 -0
  62. package/src/mcp-http.ts +13 -1
  63. package/src/mcp-tools.ts +49 -14
  64. package/src/onboarding-seed.test.ts +64 -0
  65. package/src/routes.ts +173 -92
  66. package/src/routing.ts +17 -0
  67. package/src/scribe-discovery.test.ts +76 -1
  68. package/src/scribe-discovery.ts +28 -9
  69. package/src/semantic-search-routes.test.ts +161 -0
  70. package/src/server.ts +21 -2
  71. package/src/vault-embeddings-capability.test.ts +82 -0
  72. package/src/vault-store-embedding-wiring.test.ts +69 -0
  73. package/src/vault-store.ts +53 -2
  74. package/src/vault.test.ts +62 -13
@@ -0,0 +1,96 @@
1
+ /**
2
+ * `computeLede` — the first non-empty paragraph AFTER a note's title line,
3
+ * used by `expand_mode: "summary"` (core/src/expand.ts) as a fallback when
4
+ * `metadata.summary` is absent (summaries-as-content, 2026-07-17 dialogue).
5
+ *
6
+ * Shares its title-line rule (including the leading-frontmatter skip) with
7
+ * `computeDisplayTitle` — see `display-title.test.ts` for that function's
8
+ * own coverage. This file covers only the lede-specific behavior: where the
9
+ * paragraph starts, how it's collapsed, the length cap, and the
10
+ * title-only/no-content null cases.
11
+ */
12
+ import { describe, it, expect } from "bun:test";
13
+ import { computeLede, LEDE_MAX_LEN } from "./notes.js";
14
+
15
+ describe("computeLede", () => {
16
+ it("returns the first paragraph after a heading title, blank-line separated", () => {
17
+ expect(
18
+ computeLede("# Title\n\nThis is the lede paragraph.\n\nSecond paragraph — not the lede."),
19
+ ).toBe("This is the lede paragraph.");
20
+ });
21
+
22
+ it("joins a multi-line paragraph into one collapsed line", () => {
23
+ expect(
24
+ computeLede("# Title\n\nThis lede paragraph\nspans two lines.\n\nSecond paragraph."),
25
+ ).toBe("This lede paragraph spans two lines.");
26
+ });
27
+
28
+ it("treats text immediately following the title (no blank line) as the lede", () => {
29
+ expect(computeLede("Grocery List\nmilk, eggs\ncheese")).toBe("milk, eggs cheese");
30
+ });
31
+
32
+ it("collapses internal whitespace runs to single spaces", () => {
33
+ expect(computeLede("Title\n\nLine one with extra spaces.")).toBe(
34
+ "Line one with extra spaces.",
35
+ );
36
+ });
37
+
38
+ it("returns null for a title-only note (does not repeat the title)", () => {
39
+ expect(computeLede("Just a title")).toBeNull();
40
+ expect(computeLede("# Just a title")).toBeNull();
41
+ });
42
+
43
+ it("returns null when only blank lines follow the title", () => {
44
+ expect(computeLede("# Title\n\n\n \n")).toBeNull();
45
+ });
46
+
47
+ it("returns null for empty, null, or undefined content", () => {
48
+ expect(computeLede("")).toBeNull();
49
+ expect(computeLede(null)).toBeNull();
50
+ expect(computeLede(undefined)).toBeNull();
51
+ });
52
+
53
+ it("returns null when content is whitespace-only (no title at all)", () => {
54
+ expect(computeLede(" \n\t\n ")).toBeNull();
55
+ });
56
+
57
+ describe("leading frontmatter block", () => {
58
+ it("finds the lede after a closed frontmatter block AND its title line", () => {
59
+ expect(
60
+ computeLede("---\ntitle: X\n---\n# Real Title\n\nThe lede text."),
61
+ ).toBe("The lede text.");
62
+ });
63
+
64
+ it("finds the lede when the title immediately follows frontmatter with no blank line", () => {
65
+ expect(
66
+ computeLede("---\ntitle: X\n---\nReal Title\nlede text right after."),
67
+ ).toBe("lede text right after.");
68
+ });
69
+
70
+ it("returns null for a frontmatter-only note with no body after the title", () => {
71
+ expect(computeLede("---\ntitle: X\n---\nReal Title")).toBeNull();
72
+ });
73
+ });
74
+
75
+ describe("length cap", () => {
76
+ it("truncates to LEDE_MAX_LEN code points", () => {
77
+ const longParagraph = "a".repeat(1000);
78
+ const result = computeLede(`# Title\n\n${longParagraph}`)!;
79
+ expect(result.length).toBe(LEDE_MAX_LEN);
80
+ expect(result).toBe("a".repeat(LEDE_MAX_LEN));
81
+ });
82
+
83
+ it("does not truncate a lede at or under the cap", () => {
84
+ const exact = "a".repeat(LEDE_MAX_LEN);
85
+ expect(computeLede(`# Title\n\n${exact}`)).toBe(exact);
86
+ const short = "short lede";
87
+ expect(computeLede(`# Title\n\n${short}`)).toBe(short);
88
+ });
89
+
90
+ it("truncates by Unicode code point, not UTF-16 code unit (no split surrogate pairs)", () => {
91
+ const emojiParagraph = "😀".repeat(500);
92
+ const result = computeLede(`# Title\n\n${emojiParagraph}`)!;
93
+ expect(Array.from(result).length).toBe(LEDE_MAX_LEN);
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,160 @@
1
+ import { describe, it, expect, beforeEach } from "bun:test";
2
+ import { Database } from "bun:sqlite";
3
+ import { SqliteStore } from "./store.js";
4
+ import { generateMcpTools } from "./mcp.js";
5
+ import { QueryError } from "./query-operators.js";
6
+ import type { EmbeddingProvider, EmbedInput, EmbedResult, ProviderAvailability } from "./embedding/provider.js";
7
+ import { encodeVector, normalize } from "./embedding/vector-codec.js";
8
+
9
+ const MODEL = "mock-model";
10
+ const DIMS = 4;
11
+
12
+ class MockEmbeddingProvider implements EmbeddingProvider {
13
+ readonly name = "mock";
14
+ readonly model = MODEL;
15
+ readonly dims = DIMS;
16
+ available_ = true;
17
+ reason_: string | undefined;
18
+ vectorFor: (text: string) => number[];
19
+
20
+ constructor(vectorFor?: (text: string) => number[]) {
21
+ this.vectorFor = vectorFor ?? (() => [1, 0, 0, 0]);
22
+ }
23
+
24
+ async embed(input: EmbedInput): Promise<EmbedResult> {
25
+ return { vectors: input.texts.map((t) => new Float32Array(this.vectorFor(t))), model: this.model, dims: this.dims };
26
+ }
27
+
28
+ async available(): Promise<ProviderAvailability> {
29
+ return this.available_ ? { ok: true } : { ok: false, reason: this.reason_ };
30
+ }
31
+ }
32
+
33
+ function insertVector(db: Database, noteId: string, vector: number[]): void {
34
+ const encoded = encodeVector(normalize(new Float32Array(vector)));
35
+ db.prepare(
36
+ `INSERT INTO note_vectors (note_id, chunk_ix, vector, dims, model, content_hash, embedded_at) VALUES (?, 0, ?, ?, ?, ?, ?)`,
37
+ ).run(noteId, encoded, DIMS, MODEL, "hash", new Date().toISOString());
38
+ }
39
+
40
+ let db: Database;
41
+
42
+ beforeEach(() => {
43
+ db = new Database(":memory:");
44
+ });
45
+
46
+ describe("query-notes MCP tool — semantic search (EXPERIMENTAL)", () => {
47
+ it("throws INVALID_QUERY when semantic:true is passed without near_text", async () => {
48
+ const store = new SqliteStore(db, { embeddingProvider: new MockEmbeddingProvider() });
49
+ const tools = generateMcpTools(store);
50
+ const query = tools.find((t) => t.name === "query-notes")!;
51
+ await expect(query.execute({ semantic: true })).rejects.toThrow(QueryError);
52
+ });
53
+
54
+ it("throws INVALID_QUERY when semantic:true is combined with search", async () => {
55
+ const store = new SqliteStore(db, { embeddingProvider: new MockEmbeddingProvider() });
56
+ const tools = generateMcpTools(store);
57
+ const query = tools.find((t) => t.name === "query-notes")!;
58
+ await expect(query.execute({ semantic: true, near_text: "x", search: "y" })).rejects.toThrow(QueryError);
59
+ });
60
+
61
+ it("throws INVALID_QUERY when semantic is combined with aggregate", async () => {
62
+ const store = new SqliteStore(db, { embeddingProvider: new MockEmbeddingProvider() });
63
+ const tools = generateMcpTools(store);
64
+ const query = tools.find((t) => t.name === "query-notes")!;
65
+ await expect(
66
+ query.execute({ semantic: true, near_text: "x", aggregate: { group_by: "tag", op: "count" } }),
67
+ ).rejects.toThrow(QueryError);
68
+ });
69
+
70
+ it("throws INVALID_QUERY when semantic is combined with cursor", async () => {
71
+ const store = new SqliteStore(db, { embeddingProvider: new MockEmbeddingProvider() });
72
+ const tools = generateMcpTools(store);
73
+ const query = tools.find((t) => t.name === "query-notes")!;
74
+ await expect(query.execute({ semantic: true, near_text: "x", cursor: "" })).rejects.toThrow(QueryError);
75
+ });
76
+
77
+ it("throws a structured semantic_unavailable error when no provider is configured", async () => {
78
+ const store = new SqliteStore(db); // no embeddingProvider
79
+ const tools = generateMcpTools(store);
80
+ const query = tools.find((t) => t.name === "query-notes")!;
81
+ let caught: unknown;
82
+ try {
83
+ await query.execute({ semantic: true, near_text: "anything" });
84
+ } catch (e) {
85
+ caught = e;
86
+ }
87
+ expect(caught).toBeInstanceOf(QueryError);
88
+ expect((caught as QueryError).error_type).toBe("semantic_unavailable");
89
+ });
90
+
91
+ it("near_text without semantic:true is a no-op that carries an ignored_param warning", async () => {
92
+ const store = new SqliteStore(db, { embeddingProvider: new MockEmbeddingProvider() });
93
+ await store.createNote("hello world", { path: "n" });
94
+ const tools = generateMcpTools(store);
95
+ const query = tools.find((t) => t.name === "query-notes")!;
96
+ const result = (await query.execute({ near_text: "hello" })) as { notes: unknown[]; warnings: { code: string }[] };
97
+ // A plain structured query with no filters returns a bare array UNLESS
98
+ // warnings are present (vault#550 envelope rule) — here they are.
99
+ expect(Array.isArray((result as any))).toBe(false);
100
+ expect(result.warnings.some((w) => w.code === "ignored_param")).toBe(true);
101
+ });
102
+
103
+ it("returns notes ranked by meaning via near_text + semantic:true", async () => {
104
+ const store = new SqliteStore(db, { embeddingProvider: new MockEmbeddingProvider() });
105
+ const close = await store.createNote("close note", { path: "close" });
106
+ const far = await store.createNote("far note", { path: "far" });
107
+ insertVector(db, close.id, [1, 0, 0, 0]);
108
+ insertVector(db, far.id, [0, 1, 0, 0]);
109
+
110
+ const tools = generateMcpTools(store);
111
+ const query = tools.find((t) => t.name === "query-notes")!;
112
+ const results = (await query.execute({ semantic: true, near_text: "anything", include_content: true })) as any[];
113
+ expect(results.map((n: any) => n.id)).toEqual([close.id, far.id]);
114
+ expect(results[0].score).toBeCloseTo(1, 4);
115
+ });
116
+
117
+ it("composes with tag filters (structured filters narrow the candidate set first)", async () => {
118
+ const store = new SqliteStore(db, { embeddingProvider: new MockEmbeddingProvider() });
119
+ const tagged = await store.createNote("tagged", { path: "tagged", tags: ["project"] });
120
+ const untagged = await store.createNote("untagged", { path: "untagged" });
121
+ insertVector(db, tagged.id, [1, 0, 0, 0]);
122
+ insertVector(db, untagged.id, [1, 0, 0, 0]);
123
+
124
+ const tools = generateMcpTools(store);
125
+ const query = tools.find((t) => t.name === "query-notes")!;
126
+ const results = (await query.execute({ semantic: true, near_text: "x", tag: "project" })) as any[];
127
+ expect(results.map((n: any) => n.id)).toEqual([tagged.id]);
128
+ });
129
+
130
+ it("attaches embeddings_pending warning + still returns real results when some candidates aren't embedded yet", async () => {
131
+ const store = new SqliteStore(db, { embeddingProvider: new MockEmbeddingProvider() });
132
+ const embedded = await store.createNote("embedded", { path: "a" });
133
+ await store.createNote("pending", { path: "b" });
134
+ insertVector(db, embedded.id, [1, 0, 0, 0]);
135
+
136
+ const tools = generateMcpTools(store);
137
+ const query = tools.find((t) => t.name === "query-notes")!;
138
+ const result = (await query.execute({ semantic: true, near_text: "x", include_content: true })) as {
139
+ notes: any[];
140
+ warnings: { code: string; pending?: number; total_candidates?: number }[];
141
+ };
142
+ expect(result.notes.map((n: any) => n.id)).toEqual([embedded.id]);
143
+ const pendingWarning = result.warnings.find((w) => w.code === "embeddings_pending");
144
+ expect(pendingWarning).toBeDefined();
145
+ expect(pendingWarning!.pending).toBe(1);
146
+ expect(pendingWarning!.total_candidates).toBe(2);
147
+ });
148
+
149
+ it("no embeddings_pending warning once every candidate has a vector", async () => {
150
+ const store = new SqliteStore(db, { embeddingProvider: new MockEmbeddingProvider() });
151
+ const note = await store.createNote("note", { path: "a" });
152
+ insertVector(db, note.id, [1, 0, 0, 0]);
153
+
154
+ const tools = generateMcpTools(store);
155
+ const query = tools.find((t) => t.name === "query-notes")!;
156
+ const results = (await query.execute({ semantic: true, near_text: "x", include_content: true })) as any;
157
+ // No warnings at all -> bare array (vault#550 envelope rule).
158
+ expect(Array.isArray(results)).toBe(true);
159
+ });
160
+ });