@openparachute/vault 0.7.3-rc.1 → 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 (69) 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 +521 -4
  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 +381 -10
  26. package/core/src/notes.semantic-search.test.ts +131 -0
  27. package/core/src/notes.ts +313 -6
  28. package/core/src/query-warnings.ts +20 -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 +55 -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/cli.ts +5 -1
  48. package/src/contract-errors.test.ts +2 -1
  49. package/src/embedding/capability.test.ts +33 -0
  50. package/src/embedding/capability.ts +34 -0
  51. package/src/embedding/external-api.test.ts +154 -0
  52. package/src/embedding/external-api.ts +144 -0
  53. package/src/embedding/onnx-transformers.test.ts +113 -0
  54. package/src/embedding/onnx-transformers.ts +141 -0
  55. package/src/embedding/select.test.ts +99 -0
  56. package/src/embedding/select.ts +92 -0
  57. package/src/embedding-worker.test.ts +300 -0
  58. package/src/embedding-worker.ts +226 -0
  59. package/src/mcp-http.ts +13 -1
  60. package/src/mcp-tools.ts +49 -14
  61. package/src/onboarding-seed.test.ts +64 -0
  62. package/src/routes.ts +146 -92
  63. package/src/routing.ts +17 -0
  64. package/src/semantic-search-routes.test.ts +161 -0
  65. package/src/server.ts +21 -2
  66. package/src/vault-embeddings-capability.test.ts +82 -0
  67. package/src/vault-store-embedding-wiring.test.ts +69 -0
  68. package/src/vault-store.ts +53 -2
  69. package/src/vault.test.ts +35 -11
@@ -0,0 +1,69 @@
1
+ /**
2
+ * End-to-end wiring test for the `EMBEDDINGS_ENABLED=false` off switch's
3
+ * error hint (review nano flag on #602): `getVaultStore` should thread
4
+ * "explicitly disabled" through to `Store.semanticSearch`'s
5
+ * `semantic_unavailable` hint, not the generic provider-setup message —
6
+ * see `core/src/store.ts`'s `embeddingDisabledReason` doc comment for why
7
+ * core can't determine this on its own. Unit coverage for the underlying
8
+ * `Store` behavior lives in `core/src/store.semantic-search.test.ts`; this
9
+ * file exercises the REAL `src/vault-store.ts` call site.
10
+ */
11
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
12
+ import { mkdirSync, rmSync } from "fs";
13
+ import { join } from "path";
14
+ import { tmpdir } from "os";
15
+ import { QueryError } from "../core/src/query-operators.ts";
16
+ import {
17
+ getVaultStore,
18
+ clearVaultStoreCache,
19
+ resetSharedEmbeddingProviderForTests,
20
+ } from "./vault-store.ts";
21
+
22
+ let tmpHome: string;
23
+ let prevHome: string | undefined;
24
+ let prevEnabled: string | undefined;
25
+
26
+ beforeEach(() => {
27
+ tmpHome = join(tmpdir(), `vault-embed-wiring-${Date.now()}-${Math.random().toString(36).slice(2)}`);
28
+ mkdirSync(join(tmpHome, "vault", "data"), { recursive: true });
29
+ prevHome = process.env.PARACHUTE_HOME;
30
+ prevEnabled = process.env.EMBEDDINGS_ENABLED;
31
+ process.env.PARACHUTE_HOME = tmpHome;
32
+ clearVaultStoreCache();
33
+ resetSharedEmbeddingProviderForTests();
34
+ });
35
+
36
+ afterEach(() => {
37
+ if (prevHome === undefined) delete process.env.PARACHUTE_HOME;
38
+ else process.env.PARACHUTE_HOME = prevHome;
39
+ if (prevEnabled === undefined) delete process.env.EMBEDDINGS_ENABLED;
40
+ else process.env.EMBEDDINGS_ENABLED = prevEnabled;
41
+ clearVaultStoreCache();
42
+ resetSharedEmbeddingProviderForTests();
43
+ rmSync(tmpHome, { recursive: true, force: true });
44
+ });
45
+
46
+ describe("getVaultStore — EMBEDDINGS_ENABLED=false hint wiring", () => {
47
+ test("store.embeddingDisabledReason is set, and semanticSearch's hint names the off switch", async () => {
48
+ process.env.EMBEDDINGS_ENABLED = "false";
49
+ const store = getVaultStore("test");
50
+ expect(store.embeddingDisabledReason).toBe("semantic search is disabled by EMBEDDINGS_ENABLED=false");
51
+
52
+ let caught: unknown;
53
+ try {
54
+ await store.semanticSearch("anything");
55
+ } catch (e) {
56
+ caught = e;
57
+ }
58
+ expect(caught).toBeInstanceOf(QueryError);
59
+ expect((caught as QueryError).error_type).toBe("semantic_unavailable");
60
+ expect((caught as QueryError).hint).toBe("semantic search is disabled by EMBEDDINGS_ENABLED=false");
61
+ });
62
+
63
+ test("no reason set when EMBEDDINGS_ENABLED is unset — the store still has a (bundled-floor) provider", async () => {
64
+ delete process.env.EMBEDDINGS_ENABLED;
65
+ const store = getVaultStore("test");
66
+ expect(store.embeddingDisabledReason).toBeUndefined();
67
+ expect(store.embeddingProvider).toBeDefined();
68
+ });
69
+ });
@@ -8,6 +8,8 @@
8
8
  import { SqliteStore } from "../core/src/store.ts";
9
9
  import { defaultHookRegistry } from "../core/src/hooks.ts";
10
10
  import type { Store } from "../core/src/types.ts";
11
+ import type { EmbeddingProvider } from "../core/src/embedding/provider.ts";
12
+ import { buildEmbeddingProvider, embeddingsExplicitlyDisabled } from "./embedding/select.ts";
11
13
  import { openVaultDb } from "./db.ts";
12
14
 
13
15
  export { SqliteStore as BunStore };
@@ -20,14 +22,63 @@ const stores = new Map<string, SqliteStore>();
20
22
  * resolve the vault's assets directory from the store handle. */
21
23
  const storeToVault = new WeakMap<SqliteStore, string>();
22
24
 
25
+ /**
26
+ * The one shared `EmbeddingProvider` instance for this process (semantic
27
+ * search MVP, EXPERIMENTAL). Unlike `defaultTranscriptionProvider()`
28
+ * (resolved fresh per call — transcription providers are cheap stateless
29
+ * wrappers), this is memoized deliberately: the bundled floor tier
30
+ * (`onnx-transformers.ts`) lazy-loads a ~34MB ONNX model on first use, and
31
+ * every vault this server hosts should share that ONE loaded model rather
32
+ * than each vault's `Store` triggering its own separate load. Resolved
33
+ * from `process.env` on first access (so `.env` loaded via
34
+ * `loadEnvFile()` in `server.ts` is visible) and reused for every
35
+ * subsequent vault opened this process.
36
+ *
37
+ * `undefined` is a legitimate, cacheable resolution — `EMBEDDINGS_ENABLED=
38
+ * false` (the off switch, see `select.ts`) means "no provider," not
39
+ * "not resolved yet." A separate `resolved` flag (rather than null-
40
+ * checking the provider itself) distinguishes the two, so an explicitly-
41
+ * disabled vault doesn't re-run `buildEmbeddingProvider()` on every call.
42
+ */
43
+ let sharedEmbeddingProvider: EmbeddingProvider | undefined;
44
+ let sharedEmbeddingProviderResolved = false;
45
+
46
+ /** The shared embedding provider (see `sharedEmbeddingProvider`'s doc comment). `undefined` when `EMBEDDINGS_ENABLED=false`. */
47
+ export function getSharedEmbeddingProvider(): EmbeddingProvider | undefined {
48
+ if (!sharedEmbeddingProviderResolved) {
49
+ sharedEmbeddingProvider = buildEmbeddingProvider();
50
+ sharedEmbeddingProviderResolved = true;
51
+ }
52
+ return sharedEmbeddingProvider;
53
+ }
54
+
55
+ /** Test-only: force a fresh provider on the next `getSharedEmbeddingProvider()` call. */
56
+ export function resetSharedEmbeddingProviderForTests(): void {
57
+ sharedEmbeddingProvider = undefined;
58
+ sharedEmbeddingProviderResolved = false;
59
+ }
60
+
23
61
  /** Get or create a store for a vault. */
24
62
  export function getVaultStore(name: string): SqliteStore {
25
63
  let store = stores.get(name);
26
64
  if (!store) {
27
65
  const db = openVaultDb(name);
28
66
  // Share the process-wide hook registry so features can register
29
- // handlers once at startup and have them fire for every vault.
30
- store = new SqliteStore(db, { hooks: defaultHookRegistry });
67
+ // handlers once at startup and have them fire for every vault. Same
68
+ // for the embedding provider (see `getSharedEmbeddingProvider`'s doc
69
+ // comment) — every vault's semantic search resolves the SAME provider
70
+ // instance. `embeddingDisabledReason` is only ever set here (core can't
71
+ // read env vars itself — see `Store.embeddingDisabledReason`'s doc
72
+ // comment), so `semanticSearch`'s no-provider hint can say "explicitly
73
+ // disabled" instead of generic provider-setup instructions when that's
74
+ // actually why.
75
+ store = new SqliteStore(db, {
76
+ hooks: defaultHookRegistry,
77
+ embeddingProvider: getSharedEmbeddingProvider(),
78
+ embeddingDisabledReason: embeddingsExplicitlyDisabled()
79
+ ? "semantic search is disabled by EMBEDDINGS_ENABLED=false"
80
+ : undefined,
81
+ });
31
82
  stores.set(name, store);
32
83
  storeToVault.set(store, name);
33
84
  }
package/src/vault.test.ts CHANGED
@@ -2139,6 +2139,22 @@ describe("HTTP /notes", async () => {
2139
2139
  expect(body[0]).not.toHaveProperty("content");
2140
2140
  expect(body[0]).toHaveProperty("byteSize");
2141
2141
  expect(body[0]).toHaveProperty("preview");
2142
+ expect(body[0]).toHaveProperty("displayTitle");
2143
+ });
2144
+
2145
+ // Title axis (ratified 2026-07-17) — displayTitle on the REST lean shape.
2146
+ test("GET /notes lean shape carries the computed displayTitle", async () => {
2147
+ await store.createNote("# Meeting Notes\nagenda: budget", { path: "meeting" });
2148
+ const res = await handleNotes(mkReq("GET", "/notes"), store, "");
2149
+ const body = await res.json() as any[];
2150
+ expect(body[0].displayTitle).toBe("Meeting Notes");
2151
+ });
2152
+
2153
+ test("GET /notes lean shape reports null displayTitle for an empty note", async () => {
2154
+ await store.createNote("", { path: "empty" });
2155
+ const res = await handleNotes(mkReq("GET", "/notes"), store, "");
2156
+ const body = await res.json() as any[];
2157
+ expect(body[0].displayTitle).toBeNull();
2142
2158
  });
2143
2159
 
2144
2160
  test("GET /notes?include_content=true returns full notes", async () => {
@@ -6609,8 +6625,12 @@ describe("stateless MCP transport", async () => {
6609
6625
  expect(toolNames).not.toContain("merge-tags");
6610
6626
  // Admin tools (vault#376) are hidden too
6611
6627
  expect(toolNames).not.toContain("manage-token");
6612
- // Read tier is exactly 5 tools (doctor added by the re-tier).
6613
- expect(toolNames.length).toBe(5);
6628
+ // request-attachment-download is read-tier (upload is write-tier).
6629
+ expect(toolNames).toContain("request-attachment-download");
6630
+ expect(toolNames).not.toContain("request-attachment-upload");
6631
+ // Read tier is exactly 6 tools (doctor added by the re-tier;
6632
+ // request-attachment-download added by the attachment-tickets design).
6633
+ expect(toolNames.length).toBe(6);
6614
6634
 
6615
6635
  closeAllStores();
6616
6636
  });
@@ -6891,15 +6911,15 @@ describe("MCP tools/list scope tiers (vault#376)", () => {
6891
6911
  return names;
6892
6912
  }
6893
6913
 
6894
- test("vault:read sees exactly the 5 read tools (doctor moved admin → read)", async () => {
6914
+ test("vault:read sees exactly the 6 read tools (doctor moved admin → read; request-attachment-download is read-tier)", async () => {
6895
6915
  const names = await listToolNames(["vault:read"]);
6896
6916
  expect(new Set(names)).toEqual(
6897
- new Set(["query-notes", "list-tags", "find-path", "vault-info", "doctor"]),
6917
+ new Set(["query-notes", "list-tags", "find-path", "vault-info", "doctor", "request-attachment-download"]),
6898
6918
  );
6899
- expect(names.length).toBe(5);
6919
+ expect(names.length).toBe(6);
6900
6920
  });
6901
6921
 
6902
- test("vault:read + vault:write sees the 8 read+write tools (tag-schema tools moved write → admin)", async () => {
6922
+ test("vault:read + vault:write sees the 10 read+write tools (tag-schema tools moved write → admin; request-attachment-upload is write-tier)", async () => {
6903
6923
  const names = await listToolNames(["vault:read", "vault:write"]);
6904
6924
  expect(new Set(names)).toEqual(
6905
6925
  new Set([
@@ -6911,9 +6931,11 @@ describe("MCP tools/list scope tiers (vault#376)", () => {
6911
6931
  "create-note",
6912
6932
  "update-note",
6913
6933
  "delete-note",
6934
+ "request-attachment-upload",
6935
+ "request-attachment-download",
6914
6936
  ]),
6915
6937
  );
6916
- expect(names.length).toBe(8);
6938
+ expect(names.length).toBe(10);
6917
6939
  expect(names).not.toContain("manage-token");
6918
6940
  // Re-tier (this PR): update-tag/delete-tag/rename-tag/merge-tags are now
6919
6941
  // admin-tier — structure/taxonomy curation, not content authorship.
@@ -6925,7 +6947,7 @@ describe("MCP tools/list scope tiers (vault#376)", () => {
6925
6947
  expect(names).toContain("delete-note");
6926
6948
  });
6927
6949
 
6928
- test("vault:admin sees all 14 tools including manage-token + prune-schema + the tag-schema tools", async () => {
6950
+ test("vault:admin sees all 16 tools including manage-token + prune-schema + the tag-schema tools + both attachment-ticket tools", async () => {
6929
6951
  const names = await listToolNames(["vault:read", "vault:write", "vault:admin"]);
6930
6952
  expect(names).toContain("manage-token");
6931
6953
  expect(names).toContain("prune-schema");
@@ -6934,10 +6956,12 @@ describe("MCP tools/list scope tiers (vault#376)", () => {
6934
6956
  expect(names).toContain("delete-tag");
6935
6957
  expect(names).toContain("rename-tag");
6936
6958
  expect(names).toContain("merge-tags");
6937
- expect(names.length).toBe(14);
6959
+ expect(names).toContain("request-attachment-upload");
6960
+ expect(names).toContain("request-attachment-download");
6961
+ expect(names.length).toBe(16);
6938
6962
  });
6939
6963
 
6940
- test("legacy-derived full token sees all 14 tools (back-compat)", async () => {
6964
+ test("legacy-derived full token sees all 16 tools (back-compat)", async () => {
6941
6965
  const { handleScopedMcp } = await import("./mcp-http.ts");
6942
6966
  const { writeVaultConfig } = await import("./config.ts");
6943
6967
  const { closeAllStores } = await import("./vault-store.ts");
@@ -6970,7 +6994,7 @@ describe("MCP tools/list scope tiers (vault#376)", () => {
6970
6994
  } as any);
6971
6995
  const body = await res.json() as any;
6972
6996
  const names: string[] = body.result.tools.map((t: any) => t.name);
6973
- expect(names.length).toBe(14);
6997
+ expect(names.length).toBe(16);
6974
6998
  expect(names).toContain("manage-token");
6975
6999
  expect(names).toContain("prune-schema");
6976
7000
  expect(names).toContain("doctor");