@openparachute/vault 0.7.0-rc.4 → 0.7.0-rc.6
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/attribution.test.ts +11 -3
- package/core/src/contract-taxonomy.test.ts +193 -17
- package/core/src/contract-typed-index.test.ts +228 -11
- package/core/src/core.test.ts +67 -14
- package/core/src/doctor-scope.test.ts +117 -0
- package/core/src/doctor.ts +360 -0
- package/core/src/mcp.ts +164 -28
- package/core/src/notes.ts +98 -24
- package/core/src/portable-md.test.ts +107 -0
- package/core/src/portable-md.ts +51 -5
- package/core/src/schema-defaults.ts +48 -7
- package/core/src/schema.ts +147 -2
- package/core/src/seed-packs.ts +20 -13
- package/core/src/store.ts +35 -3
- package/core/src/tag-hierarchy.ts +66 -0
- package/core/src/tag-schemas.ts +166 -6
- package/core/src/types.ts +23 -1
- package/package.json +1 -1
- package/src/mcp-http.ts +13 -0
- package/src/mcp-tools.ts +90 -5
- package/src/mirror-import.ts +5 -0
- package/src/onboarding-seed.test.ts +5 -2
- package/src/routes.ts +91 -52
- package/src/routing.ts +29 -0
- package/src/tag-integrity-mcp.test.ts +288 -0
- package/src/tag-integrity-scope.test.ts +97 -0
- package/src/tag-scope.ts +43 -0
- package/src/vault.test.ts +58 -35
package/src/vault.test.ts
CHANGED
|
@@ -484,7 +484,7 @@ describe("deeper link queries", async () => {
|
|
|
484
484
|
describe("MCP tools", async () => {
|
|
485
485
|
test("generates the consolidated tool set", () => {
|
|
486
486
|
const tools = generateMcpTools(store);
|
|
487
|
-
expect(tools.length).toBe(
|
|
487
|
+
expect(tools.length).toBe(13);
|
|
488
488
|
|
|
489
489
|
const names = tools.map((t) => t.name);
|
|
490
490
|
expect(names).toContain("query-notes");
|
|
@@ -498,6 +498,12 @@ describe("MCP tools", async () => {
|
|
|
498
498
|
expect(names).toContain("vault-info");
|
|
499
499
|
// prune-schema (admin) — drops orphaned indexed-field columns.
|
|
500
500
|
expect(names).toContain("prune-schema");
|
|
501
|
+
// rename-tag / merge-tags (vault#552) — MCP parity with the pre-existing
|
|
502
|
+
// REST engine.
|
|
503
|
+
expect(names).toContain("rename-tag");
|
|
504
|
+
expect(names).toContain("merge-tags");
|
|
505
|
+
// doctor (admin, vault#552) — read-only taxonomy/metadata integrity scan.
|
|
506
|
+
expect(names).toContain("doctor");
|
|
501
507
|
// Six note-schema MCP tools (list/update/delete-note-schema +
|
|
502
508
|
// list/set/delete-schema-mapping) retired in v17 — vault#267.
|
|
503
509
|
expect(names).not.toContain("list-note-schemas");
|
|
@@ -973,7 +979,7 @@ describe("scoped MCP wrapper", async () => {
|
|
|
973
979
|
closeAllStores();
|
|
974
980
|
});
|
|
975
981
|
|
|
976
|
-
test("create-note with schema tag
|
|
982
|
+
test("create-note with schema tag applies explicit defaults; undeclared fields stay absent (vault#553 Decision B)", async () => {
|
|
977
983
|
const { generateScopedMcpTools } = await import("./mcp-tools.ts");
|
|
978
984
|
const { writeVaultConfig } = await import("./config.ts");
|
|
979
985
|
const { getVaultStore, closeAllStores } = await import("./vault-store.ts");
|
|
@@ -989,8 +995,8 @@ describe("scoped MCP wrapper", async () => {
|
|
|
989
995
|
await vaultStore.upsertTagSchema("person", {
|
|
990
996
|
description: "A person",
|
|
991
997
|
fields: {
|
|
992
|
-
first_appeared: { type: "string", description: "When" },
|
|
993
|
-
relationship: { type: "string", description: "How" },
|
|
998
|
+
first_appeared: { type: "string", description: "When", default: "unknown" },
|
|
999
|
+
relationship: { type: "string", description: "How" }, // no default — stays absent
|
|
994
1000
|
},
|
|
995
1001
|
});
|
|
996
1002
|
|
|
@@ -998,19 +1004,19 @@ describe("scoped MCP wrapper", async () => {
|
|
|
998
1004
|
const createNote = tools.find((t) => t.name === "create-note")!;
|
|
999
1005
|
const queryNotes = tools.find((t) => t.name === "query-notes")!;
|
|
1000
1006
|
|
|
1001
|
-
// Create a note tagged person with no metadata —
|
|
1007
|
+
// Create a note tagged person with no metadata — the EXPLICIT default
|
|
1008
|
+
// auto-populates; the field with no `default` stays absent.
|
|
1002
1009
|
const result = await createNote.execute({
|
|
1003
1010
|
content: "Alice",
|
|
1004
1011
|
tags: ["person"],
|
|
1005
1012
|
}) as any;
|
|
1006
1013
|
expect(result.content).toBe("Alice");
|
|
1007
1014
|
|
|
1008
|
-
// Verify defaults were written
|
|
1009
1015
|
const fresh = await queryNotes.execute({ id: result.id }) as any;
|
|
1010
|
-
expect(fresh.metadata.first_appeared).toBe("");
|
|
1011
|
-
expect(fresh.metadata.relationship).
|
|
1016
|
+
expect(fresh.metadata.first_appeared).toBe("unknown");
|
|
1017
|
+
expect(fresh.metadata.relationship).toBeUndefined();
|
|
1012
1018
|
|
|
1013
|
-
// Create with explicit metadata — preserved
|
|
1019
|
+
// Create with explicit metadata — preserved (overrides the default).
|
|
1014
1020
|
const result2 = await createNote.execute({
|
|
1015
1021
|
content: "Bob",
|
|
1016
1022
|
tags: ["person"],
|
|
@@ -1023,7 +1029,7 @@ describe("scoped MCP wrapper", async () => {
|
|
|
1023
1029
|
closeAllStores();
|
|
1024
1030
|
});
|
|
1025
1031
|
|
|
1026
|
-
test("update-note tags.add with schema
|
|
1032
|
+
test("update-note tags.add with schema applies explicit defaults; undeclared fields stay absent (vault#553 Decision B)", async () => {
|
|
1027
1033
|
const { generateScopedMcpTools } = await import("./mcp-tools.ts");
|
|
1028
1034
|
const { writeVaultConfig } = await import("./config.ts");
|
|
1029
1035
|
const { getVaultStore, closeAllStores: close } = await import("./vault-store.ts");
|
|
@@ -1039,16 +1045,16 @@ describe("scoped MCP wrapper", async () => {
|
|
|
1039
1045
|
await vaultStore.upsertTagSchema("person", {
|
|
1040
1046
|
description: "A person",
|
|
1041
1047
|
fields: {
|
|
1042
|
-
first_appeared: { type: "string", description: "When" },
|
|
1043
|
-
relationship: { type: "string", description: "How" },
|
|
1048
|
+
first_appeared: { type: "string", description: "When", default: "unknown" },
|
|
1049
|
+
relationship: { type: "string", description: "How" }, // no default — stays absent
|
|
1044
1050
|
},
|
|
1045
1051
|
});
|
|
1046
1052
|
await vaultStore.upsertTagSchema("project", {
|
|
1047
1053
|
description: "A project",
|
|
1048
1054
|
fields: {
|
|
1049
|
-
status: { type: "string", enum: ["active", "completed", "abandoned"], description: "Status" },
|
|
1050
|
-
active: { type: "boolean", description: "Is active" },
|
|
1051
|
-
priority: { type: "integer", description: "Priority level" },
|
|
1055
|
+
status: { type: "string", enum: ["active", "completed", "abandoned"], description: "Status", default: "active" },
|
|
1056
|
+
active: { type: "boolean", description: "Is active", default: false },
|
|
1057
|
+
priority: { type: "integer", description: "Priority level" }, // no default — stays absent
|
|
1052
1058
|
},
|
|
1053
1059
|
});
|
|
1054
1060
|
const tools = generateScopedMcpTools(vaultName);
|
|
@@ -1060,10 +1066,11 @@ describe("scoped MCP wrapper", async () => {
|
|
|
1060
1066
|
const note = await createNote.execute({ content: "Alice" }) as any;
|
|
1061
1067
|
await updateNote.execute({ id: note.id, tags: { add: ["person"] }, force: true });
|
|
1062
1068
|
const after = await queryNotes.execute({ id: note.id }) as any;
|
|
1063
|
-
expect(after.metadata.first_appeared).toBe("");
|
|
1064
|
-
expect(after.metadata.relationship).
|
|
1069
|
+
expect(after.metadata.first_appeared).toBe("unknown");
|
|
1070
|
+
expect(after.metadata.relationship).toBeUndefined();
|
|
1065
1071
|
|
|
1066
|
-
// Tag note that already has partial metadata — only missing fields
|
|
1072
|
+
// Tag note that already has partial metadata — only missing fields WITH a
|
|
1073
|
+
// declared default get populated; the field with no default stays absent.
|
|
1067
1074
|
const note2 = await createNote.execute({
|
|
1068
1075
|
content: "Bob",
|
|
1069
1076
|
metadata: { first_appeared: "2023-11" },
|
|
@@ -1071,22 +1078,22 @@ describe("scoped MCP wrapper", async () => {
|
|
|
1071
1078
|
await updateNote.execute({ id: note2.id, tags: { add: ["person"] }, force: true });
|
|
1072
1079
|
const after2 = await queryNotes.execute({ id: note2.id }) as any;
|
|
1073
1080
|
expect(after2.metadata.first_appeared).toBe("2023-11"); // preserved
|
|
1074
|
-
expect(after2.metadata.relationship).
|
|
1081
|
+
expect(after2.metadata.relationship).toBeUndefined(); // no default — stays absent
|
|
1075
1082
|
|
|
1076
|
-
// Tag with #project —
|
|
1083
|
+
// Tag with #project — declared defaults land; `priority` (no default) stays absent.
|
|
1077
1084
|
const note4 = await createNote.execute({ content: "My Project" }) as any;
|
|
1078
1085
|
await updateNote.execute({ id: note4.id, tags: { add: ["project"] }, force: true });
|
|
1079
1086
|
const after4 = await queryNotes.execute({ id: note4.id }) as any;
|
|
1080
1087
|
expect(after4.metadata.status).toBe("active");
|
|
1081
1088
|
expect(after4.metadata.active).toBe(false);
|
|
1082
|
-
expect(after4.metadata.priority).
|
|
1089
|
+
expect(after4.metadata.priority).toBeUndefined();
|
|
1083
1090
|
|
|
1084
|
-
// Multiple schema tags at once — all defaults merged
|
|
1091
|
+
// Multiple schema tags at once — all EXPLICIT defaults merged.
|
|
1085
1092
|
const note5 = await createNote.execute({ content: "Multi" }) as any;
|
|
1086
1093
|
await updateNote.execute({ id: note5.id, tags: { add: ["person", "project"] }, force: true });
|
|
1087
1094
|
const after5 = await queryNotes.execute({ id: note5.id }) as any;
|
|
1088
|
-
expect(after5.metadata.first_appeared).toBe("");
|
|
1089
|
-
expect(after5.metadata.relationship).
|
|
1095
|
+
expect(after5.metadata.first_appeared).toBe("unknown");
|
|
1096
|
+
expect(after5.metadata.relationship).toBeUndefined();
|
|
1090
1097
|
expect(after5.metadata.status).toBe("active");
|
|
1091
1098
|
expect(after5.metadata.active).toBe(false);
|
|
1092
1099
|
|
|
@@ -1560,9 +1567,12 @@ describe("scoped MCP wrapper", async () => {
|
|
|
1560
1567
|
});
|
|
1561
1568
|
|
|
1562
1569
|
const vaultStore = getVaultStore(vaultName);
|
|
1570
|
+
// vault#553 Decision B: backfill is explicit-`default`-only — declare
|
|
1571
|
+
// one so this test still exercises an actual defaults-write (and can
|
|
1572
|
+
// assert it lands without bumping updatedAt).
|
|
1563
1573
|
await vaultStore.upsertTagSchema("person", {
|
|
1564
1574
|
description: "A person",
|
|
1565
|
-
fields: { name: { type: "string" } },
|
|
1575
|
+
fields: { name: { type: "string", default: "unknown" } },
|
|
1566
1576
|
});
|
|
1567
1577
|
|
|
1568
1578
|
const tools = generateScopedMcpTools(vaultName);
|
|
@@ -1575,7 +1585,7 @@ describe("scoped MCP wrapper", async () => {
|
|
|
1575
1585
|
await updateNote.execute({ id: note.id, tags: { add: ["person"] }, force: true });
|
|
1576
1586
|
const after = await queryNotes.execute({ id: note.id }) as any;
|
|
1577
1587
|
expect(after.updatedAt).toBe(originalUpdatedAt);
|
|
1578
|
-
expect(after.metadata.name).toBe("");
|
|
1588
|
+
expect(after.metadata.name).toBe("unknown");
|
|
1579
1589
|
|
|
1580
1590
|
close();
|
|
1581
1591
|
});
|
|
@@ -2193,8 +2203,10 @@ describe("HTTP /notes", async () => {
|
|
|
2193
2203
|
// mapped over the pre-defaults in-memory objects, so default-filled
|
|
2194
2204
|
// metadata was missing from `POST /api/notes` responses.
|
|
2195
2205
|
test("POST /notes response reflects post-applySchemaDefaults state (vault#316)", async () => {
|
|
2206
|
+
// vault#553 Decision B: backfill is explicit-`default`-only — declare one
|
|
2207
|
+
// so this test still exercises the post-defaults re-read mechanism.
|
|
2196
2208
|
await store.upsertTagSchema("task", {
|
|
2197
|
-
fields: { priority: { type: "string", enum: ["high", "low"] } },
|
|
2209
|
+
fields: { priority: { type: "string", enum: ["high", "low"], default: "high" } },
|
|
2198
2210
|
});
|
|
2199
2211
|
|
|
2200
2212
|
// Single: default lands in the returned metadata and agrees with disk.
|
|
@@ -2205,7 +2217,7 @@ describe("HTTP /notes", async () => {
|
|
|
2205
2217
|
);
|
|
2206
2218
|
expect(single.status).toBe(201);
|
|
2207
2219
|
const singleBody = await single.json() as any;
|
|
2208
|
-
expect(singleBody.metadata?.priority).toBe("high"); //
|
|
2220
|
+
expect(singleBody.metadata?.priority).toBe("high"); // explicit schema default
|
|
2209
2221
|
const onDisk = await store.getNoteByPath("Inbox/task-1");
|
|
2210
2222
|
expect((onDisk!.metadata as any)?.priority).toBe("high");
|
|
2211
2223
|
|
|
@@ -5414,8 +5426,11 @@ describe("stateless MCP transport", async () => {
|
|
|
5414
5426
|
expect(toolNames).not.toContain("delete-note");
|
|
5415
5427
|
expect(toolNames).not.toContain("update-tag");
|
|
5416
5428
|
expect(toolNames).not.toContain("delete-tag");
|
|
5429
|
+
expect(toolNames).not.toContain("rename-tag");
|
|
5430
|
+
expect(toolNames).not.toContain("merge-tags");
|
|
5417
5431
|
// Admin tools (vault#376) are hidden too
|
|
5418
5432
|
expect(toolNames).not.toContain("manage-token");
|
|
5433
|
+
expect(toolNames).not.toContain("doctor");
|
|
5419
5434
|
// Read tier is exactly 4 tools.
|
|
5420
5435
|
expect(toolNames.length).toBe(4);
|
|
5421
5436
|
|
|
@@ -5658,7 +5673,7 @@ describe("MCP tools/list scope tiers (vault#376)", () => {
|
|
|
5658
5673
|
expect(names.length).toBe(4);
|
|
5659
5674
|
});
|
|
5660
5675
|
|
|
5661
|
-
test("vault:read + vault:write sees the
|
|
5676
|
+
test("vault:read + vault:write sees the 11 read+write tools", async () => {
|
|
5662
5677
|
const names = await listToolNames(["vault:read", "vault:write"]);
|
|
5663
5678
|
expect(new Set(names)).toEqual(
|
|
5664
5679
|
new Set([
|
|
@@ -5671,24 +5686,31 @@ describe("MCP tools/list scope tiers (vault#376)", () => {
|
|
|
5671
5686
|
"delete-note",
|
|
5672
5687
|
"update-tag",
|
|
5673
5688
|
"delete-tag",
|
|
5689
|
+
// vault#552: MCP parity with the pre-existing REST rename/merge engine.
|
|
5690
|
+
"rename-tag",
|
|
5691
|
+
"merge-tags",
|
|
5674
5692
|
]),
|
|
5675
5693
|
);
|
|
5676
|
-
expect(names.length).toBe(
|
|
5694
|
+
expect(names.length).toBe(11);
|
|
5677
5695
|
expect(names).not.toContain("manage-token");
|
|
5696
|
+
expect(names).not.toContain("doctor");
|
|
5678
5697
|
// Aaron 2026-05-27: delete-* are write-tier (same destructive verb as
|
|
5679
|
-
// update). Only manage-token
|
|
5698
|
+
// update). Only manage-token/prune-schema/doctor are admin-gated.
|
|
5680
5699
|
expect(names).toContain("delete-note");
|
|
5681
5700
|
expect(names).toContain("delete-tag");
|
|
5682
5701
|
});
|
|
5683
5702
|
|
|
5684
|
-
test("vault:admin sees all
|
|
5703
|
+
test("vault:admin sees all 14 tools including manage-token + prune-schema + doctor", async () => {
|
|
5685
5704
|
const names = await listToolNames(["vault:read", "vault:write", "vault:admin"]);
|
|
5686
5705
|
expect(names).toContain("manage-token");
|
|
5687
5706
|
expect(names).toContain("prune-schema");
|
|
5688
|
-
expect(names
|
|
5707
|
+
expect(names).toContain("doctor");
|
|
5708
|
+
expect(names).toContain("rename-tag");
|
|
5709
|
+
expect(names).toContain("merge-tags");
|
|
5710
|
+
expect(names.length).toBe(14);
|
|
5689
5711
|
});
|
|
5690
5712
|
|
|
5691
|
-
test("legacy-derived full token sees all
|
|
5713
|
+
test("legacy-derived full token sees all 14 tools (back-compat)", async () => {
|
|
5692
5714
|
const { handleScopedMcp } = await import("./mcp-http.ts");
|
|
5693
5715
|
const { writeVaultConfig } = await import("./config.ts");
|
|
5694
5716
|
const { closeAllStores } = await import("./vault-store.ts");
|
|
@@ -5721,9 +5743,10 @@ describe("MCP tools/list scope tiers (vault#376)", () => {
|
|
|
5721
5743
|
} as any);
|
|
5722
5744
|
const body = await res.json() as any;
|
|
5723
5745
|
const names: string[] = body.result.tools.map((t: any) => t.name);
|
|
5724
|
-
expect(names.length).toBe(
|
|
5746
|
+
expect(names.length).toBe(14);
|
|
5725
5747
|
expect(names).toContain("manage-token");
|
|
5726
5748
|
expect(names).toContain("prune-schema");
|
|
5749
|
+
expect(names).toContain("doctor");
|
|
5727
5750
|
closeAllStores();
|
|
5728
5751
|
});
|
|
5729
5752
|
|