@openparachute/vault 0.7.0-rc.5 → 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-typed-index.test.ts +228 -11
- package/core/src/core.test.ts +38 -8
- package/core/src/doctor.ts +43 -13
- package/core/src/mcp.ts +40 -23
- 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 +18 -0
- package/core/src/tag-schemas.ts +121 -6
- package/package.json +1 -1
- package/src/onboarding-seed.test.ts +5 -2
- package/src/routes.ts +18 -52
- package/src/vault.test.ts +33 -27
package/src/vault.test.ts
CHANGED
|
@@ -979,7 +979,7 @@ describe("scoped MCP wrapper", async () => {
|
|
|
979
979
|
closeAllStores();
|
|
980
980
|
});
|
|
981
981
|
|
|
982
|
-
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 () => {
|
|
983
983
|
const { generateScopedMcpTools } = await import("./mcp-tools.ts");
|
|
984
984
|
const { writeVaultConfig } = await import("./config.ts");
|
|
985
985
|
const { getVaultStore, closeAllStores } = await import("./vault-store.ts");
|
|
@@ -995,8 +995,8 @@ describe("scoped MCP wrapper", async () => {
|
|
|
995
995
|
await vaultStore.upsertTagSchema("person", {
|
|
996
996
|
description: "A person",
|
|
997
997
|
fields: {
|
|
998
|
-
first_appeared: { type: "string", description: "When" },
|
|
999
|
-
relationship: { type: "string", description: "How" },
|
|
998
|
+
first_appeared: { type: "string", description: "When", default: "unknown" },
|
|
999
|
+
relationship: { type: "string", description: "How" }, // no default — stays absent
|
|
1000
1000
|
},
|
|
1001
1001
|
});
|
|
1002
1002
|
|
|
@@ -1004,19 +1004,19 @@ describe("scoped MCP wrapper", async () => {
|
|
|
1004
1004
|
const createNote = tools.find((t) => t.name === "create-note")!;
|
|
1005
1005
|
const queryNotes = tools.find((t) => t.name === "query-notes")!;
|
|
1006
1006
|
|
|
1007
|
-
// 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.
|
|
1008
1009
|
const result = await createNote.execute({
|
|
1009
1010
|
content: "Alice",
|
|
1010
1011
|
tags: ["person"],
|
|
1011
1012
|
}) as any;
|
|
1012
1013
|
expect(result.content).toBe("Alice");
|
|
1013
1014
|
|
|
1014
|
-
// Verify defaults were written
|
|
1015
1015
|
const fresh = await queryNotes.execute({ id: result.id }) as any;
|
|
1016
|
-
expect(fresh.metadata.first_appeared).toBe("");
|
|
1017
|
-
expect(fresh.metadata.relationship).
|
|
1016
|
+
expect(fresh.metadata.first_appeared).toBe("unknown");
|
|
1017
|
+
expect(fresh.metadata.relationship).toBeUndefined();
|
|
1018
1018
|
|
|
1019
|
-
// Create with explicit metadata — preserved
|
|
1019
|
+
// Create with explicit metadata — preserved (overrides the default).
|
|
1020
1020
|
const result2 = await createNote.execute({
|
|
1021
1021
|
content: "Bob",
|
|
1022
1022
|
tags: ["person"],
|
|
@@ -1029,7 +1029,7 @@ describe("scoped MCP wrapper", async () => {
|
|
|
1029
1029
|
closeAllStores();
|
|
1030
1030
|
});
|
|
1031
1031
|
|
|
1032
|
-
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 () => {
|
|
1033
1033
|
const { generateScopedMcpTools } = await import("./mcp-tools.ts");
|
|
1034
1034
|
const { writeVaultConfig } = await import("./config.ts");
|
|
1035
1035
|
const { getVaultStore, closeAllStores: close } = await import("./vault-store.ts");
|
|
@@ -1045,16 +1045,16 @@ describe("scoped MCP wrapper", async () => {
|
|
|
1045
1045
|
await vaultStore.upsertTagSchema("person", {
|
|
1046
1046
|
description: "A person",
|
|
1047
1047
|
fields: {
|
|
1048
|
-
first_appeared: { type: "string", description: "When" },
|
|
1049
|
-
relationship: { type: "string", description: "How" },
|
|
1048
|
+
first_appeared: { type: "string", description: "When", default: "unknown" },
|
|
1049
|
+
relationship: { type: "string", description: "How" }, // no default — stays absent
|
|
1050
1050
|
},
|
|
1051
1051
|
});
|
|
1052
1052
|
await vaultStore.upsertTagSchema("project", {
|
|
1053
1053
|
description: "A project",
|
|
1054
1054
|
fields: {
|
|
1055
|
-
status: { type: "string", enum: ["active", "completed", "abandoned"], description: "Status" },
|
|
1056
|
-
active: { type: "boolean", description: "Is active" },
|
|
1057
|
-
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
|
|
1058
1058
|
},
|
|
1059
1059
|
});
|
|
1060
1060
|
const tools = generateScopedMcpTools(vaultName);
|
|
@@ -1066,10 +1066,11 @@ describe("scoped MCP wrapper", async () => {
|
|
|
1066
1066
|
const note = await createNote.execute({ content: "Alice" }) as any;
|
|
1067
1067
|
await updateNote.execute({ id: note.id, tags: { add: ["person"] }, force: true });
|
|
1068
1068
|
const after = await queryNotes.execute({ id: note.id }) as any;
|
|
1069
|
-
expect(after.metadata.first_appeared).toBe("");
|
|
1070
|
-
expect(after.metadata.relationship).
|
|
1069
|
+
expect(after.metadata.first_appeared).toBe("unknown");
|
|
1070
|
+
expect(after.metadata.relationship).toBeUndefined();
|
|
1071
1071
|
|
|
1072
|
-
// 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.
|
|
1073
1074
|
const note2 = await createNote.execute({
|
|
1074
1075
|
content: "Bob",
|
|
1075
1076
|
metadata: { first_appeared: "2023-11" },
|
|
@@ -1077,22 +1078,22 @@ describe("scoped MCP wrapper", async () => {
|
|
|
1077
1078
|
await updateNote.execute({ id: note2.id, tags: { add: ["person"] }, force: true });
|
|
1078
1079
|
const after2 = await queryNotes.execute({ id: note2.id }) as any;
|
|
1079
1080
|
expect(after2.metadata.first_appeared).toBe("2023-11"); // preserved
|
|
1080
|
-
expect(after2.metadata.relationship).
|
|
1081
|
+
expect(after2.metadata.relationship).toBeUndefined(); // no default — stays absent
|
|
1081
1082
|
|
|
1082
|
-
// Tag with #project —
|
|
1083
|
+
// Tag with #project — declared defaults land; `priority` (no default) stays absent.
|
|
1083
1084
|
const note4 = await createNote.execute({ content: "My Project" }) as any;
|
|
1084
1085
|
await updateNote.execute({ id: note4.id, tags: { add: ["project"] }, force: true });
|
|
1085
1086
|
const after4 = await queryNotes.execute({ id: note4.id }) as any;
|
|
1086
1087
|
expect(after4.metadata.status).toBe("active");
|
|
1087
1088
|
expect(after4.metadata.active).toBe(false);
|
|
1088
|
-
expect(after4.metadata.priority).
|
|
1089
|
+
expect(after4.metadata.priority).toBeUndefined();
|
|
1089
1090
|
|
|
1090
|
-
// Multiple schema tags at once — all defaults merged
|
|
1091
|
+
// Multiple schema tags at once — all EXPLICIT defaults merged.
|
|
1091
1092
|
const note5 = await createNote.execute({ content: "Multi" }) as any;
|
|
1092
1093
|
await updateNote.execute({ id: note5.id, tags: { add: ["person", "project"] }, force: true });
|
|
1093
1094
|
const after5 = await queryNotes.execute({ id: note5.id }) as any;
|
|
1094
|
-
expect(after5.metadata.first_appeared).toBe("");
|
|
1095
|
-
expect(after5.metadata.relationship).
|
|
1095
|
+
expect(after5.metadata.first_appeared).toBe("unknown");
|
|
1096
|
+
expect(after5.metadata.relationship).toBeUndefined();
|
|
1096
1097
|
expect(after5.metadata.status).toBe("active");
|
|
1097
1098
|
expect(after5.metadata.active).toBe(false);
|
|
1098
1099
|
|
|
@@ -1566,9 +1567,12 @@ describe("scoped MCP wrapper", async () => {
|
|
|
1566
1567
|
});
|
|
1567
1568
|
|
|
1568
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).
|
|
1569
1573
|
await vaultStore.upsertTagSchema("person", {
|
|
1570
1574
|
description: "A person",
|
|
1571
|
-
fields: { name: { type: "string" } },
|
|
1575
|
+
fields: { name: { type: "string", default: "unknown" } },
|
|
1572
1576
|
});
|
|
1573
1577
|
|
|
1574
1578
|
const tools = generateScopedMcpTools(vaultName);
|
|
@@ -1581,7 +1585,7 @@ describe("scoped MCP wrapper", async () => {
|
|
|
1581
1585
|
await updateNote.execute({ id: note.id, tags: { add: ["person"] }, force: true });
|
|
1582
1586
|
const after = await queryNotes.execute({ id: note.id }) as any;
|
|
1583
1587
|
expect(after.updatedAt).toBe(originalUpdatedAt);
|
|
1584
|
-
expect(after.metadata.name).toBe("");
|
|
1588
|
+
expect(after.metadata.name).toBe("unknown");
|
|
1585
1589
|
|
|
1586
1590
|
close();
|
|
1587
1591
|
});
|
|
@@ -2199,8 +2203,10 @@ describe("HTTP /notes", async () => {
|
|
|
2199
2203
|
// mapped over the pre-defaults in-memory objects, so default-filled
|
|
2200
2204
|
// metadata was missing from `POST /api/notes` responses.
|
|
2201
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.
|
|
2202
2208
|
await store.upsertTagSchema("task", {
|
|
2203
|
-
fields: { priority: { type: "string", enum: ["high", "low"] } },
|
|
2209
|
+
fields: { priority: { type: "string", enum: ["high", "low"], default: "high" } },
|
|
2204
2210
|
});
|
|
2205
2211
|
|
|
2206
2212
|
// Single: default lands in the returned metadata and agrees with disk.
|
|
@@ -2211,7 +2217,7 @@ describe("HTTP /notes", async () => {
|
|
|
2211
2217
|
);
|
|
2212
2218
|
expect(single.status).toBe(201);
|
|
2213
2219
|
const singleBody = await single.json() as any;
|
|
2214
|
-
expect(singleBody.metadata?.priority).toBe("high"); //
|
|
2220
|
+
expect(singleBody.metadata?.priority).toBe("high"); // explicit schema default
|
|
2215
2221
|
const onDisk = await store.getNoteByPath("Inbox/task-1");
|
|
2216
2222
|
expect((onDisk!.metadata as any)?.priority).toBe("high");
|
|
2217
2223
|
|