@openparachute/vault 0.6.4 → 0.6.5-rc.2
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/__fixtures__/golden-vault.ts +125 -0
- package/core/src/__fixtures__/portable-export-golden.json +10 -0
- package/core/src/mcp.ts +306 -314
- package/core/src/notes.ts +21 -47
- package/core/src/portable-md-batching.test.ts +67 -0
- package/core/src/portable-md-golden.test.ts +55 -0
- package/core/src/portable-md.ts +579 -435
- package/core/src/schema.ts +21 -43
- package/core/src/store.ts +14 -9
- package/core/src/txn.test.ts +229 -0
- package/core/src/txn.ts +105 -0
- package/core/src/types.ts +9 -0
- package/package.json +1 -1
- package/src/routes.ts +8 -7
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared deterministic vault builder + export-tree serializer for the
|
|
3
|
+
* portable-md "old bytes == new bytes" golden fixture (Phase-1 streaming
|
|
4
|
+
* export refactor).
|
|
5
|
+
*
|
|
6
|
+
* Both the one-shot capture script (scripts/capture-portable-golden.ts,
|
|
7
|
+
* run against the PRE-refactor code) and the regression test
|
|
8
|
+
* (portable-md-golden.test.ts, run against the POST-refactor code) build
|
|
9
|
+
* the SAME vault here and serialize the export tree the SAME way — so a
|
|
10
|
+
* byte diff between the committed golden JSON and a fresh export is a real
|
|
11
|
+
* drift, not a fixture-vs-test mismatch.
|
|
12
|
+
*
|
|
13
|
+
* Determinism requirements met here:
|
|
14
|
+
* - explicit note ids + pinned created_at/updated_at (no wall-clock)
|
|
15
|
+
* - export called with `exportedAt` + `caseSensitiveOverride: true`
|
|
16
|
+
* (so the bytes don't depend on the runner's filesystem)
|
|
17
|
+
* - content that exercises every format field: pathed / unpathed /
|
|
18
|
+
* empty-content notes, a non-md (csv) note that forces a notes-meta
|
|
19
|
+
* sidecar, tags with schema + relationships, typed links, multi-line
|
|
20
|
+
* + control-character metadata, and created_at != updated_at.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
24
|
+
import { join } from "path";
|
|
25
|
+
import type { Store } from "../types.js";
|
|
26
|
+
|
|
27
|
+
/** Fixed export timestamp — pins vault.yaml's `exported_at`. */
|
|
28
|
+
export const GOLDEN_EXPORTED_AT = "2026-07-02T00:00:00.000Z";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Populate `store` with a deterministic, format-exhaustive vault. Callers
|
|
32
|
+
* export it with `{ exportedAt: GOLDEN_EXPORTED_AT, caseSensitiveOverride: true }`.
|
|
33
|
+
*/
|
|
34
|
+
export async function buildGoldenVault(store: Store): Promise<void> {
|
|
35
|
+
// Tag schema + opaque relationship vocabulary (vault#428 shape).
|
|
36
|
+
await store.upsertTagSchema("project", {
|
|
37
|
+
description: "A long-running effort",
|
|
38
|
+
fields: { status: { type: "string", enum: ["active", "done"], indexed: true } },
|
|
39
|
+
});
|
|
40
|
+
await store.upsertTagRecord("project", {
|
|
41
|
+
relationships: {
|
|
42
|
+
"works-on": { from: "person", to: "project" },
|
|
43
|
+
"based-at": { from: "project", to: "place", note: "freeform" },
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const t0 = "2026-01-01T00:00:00.000Z";
|
|
48
|
+
const t1 = "2026-01-01T00:01:00.000Z";
|
|
49
|
+
|
|
50
|
+
await store.createNote("alpha body", {
|
|
51
|
+
id: "01HX001",
|
|
52
|
+
path: "Inbox/alpha",
|
|
53
|
+
tags: ["project", "z-other"],
|
|
54
|
+
metadata: {
|
|
55
|
+
priority: "high",
|
|
56
|
+
notes: "line1\nline2\nline3",
|
|
57
|
+
// control character exercises the \xNN escape path (vault#317 F1)
|
|
58
|
+
ctrl: "a\tb",
|
|
59
|
+
status: "active",
|
|
60
|
+
},
|
|
61
|
+
created_at: t0,
|
|
62
|
+
});
|
|
63
|
+
await store.createNote("beta body", {
|
|
64
|
+
id: "01HX002",
|
|
65
|
+
path: "Inbox/beta",
|
|
66
|
+
tags: ["project"],
|
|
67
|
+
metadata: { status: "done" },
|
|
68
|
+
created_at: t0,
|
|
69
|
+
});
|
|
70
|
+
await store.createNote("unpathed jot", { id: "01HX003", created_at: t0 });
|
|
71
|
+
// Empty-content skeleton note (vault#323).
|
|
72
|
+
await store.createNote("", {
|
|
73
|
+
id: "01HX004",
|
|
74
|
+
path: "Inbox/skeleton",
|
|
75
|
+
tags: ["project"],
|
|
76
|
+
created_at: t0,
|
|
77
|
+
});
|
|
78
|
+
// Non-md note → forces a .parachute/notes-meta/<id>.yaml sidecar.
|
|
79
|
+
await store.createNote("col1,col2\n1,2\n", {
|
|
80
|
+
id: "01HX005",
|
|
81
|
+
path: "Data/table",
|
|
82
|
+
extension: "csv",
|
|
83
|
+
tags: ["z-other"],
|
|
84
|
+
metadata: { rows: 2 },
|
|
85
|
+
created_at: t0,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await store.createLink("01HX001", "01HX002", "derived-from", { source: "git://example" });
|
|
89
|
+
|
|
90
|
+
// Pin every timestamp so the export bytes are wall-clock-independent.
|
|
91
|
+
// n1 gets a divergent updated_at to exercise restoreNoteTimestamps.
|
|
92
|
+
await store.restoreNoteTimestamps("01HX001", t0, t1);
|
|
93
|
+
for (const id of ["01HX002", "01HX003", "01HX004", "01HX005"]) {
|
|
94
|
+
await store.restoreNoteTimestamps(id, t0, t0);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Recursively serialize an export directory into a sorted map of
|
|
100
|
+
* `<relative-posix-path>` → file content. Directories are recorded as
|
|
101
|
+
* entries with a trailing `/` and an empty value so an intentionally
|
|
102
|
+
* empty dir (e.g. `.parachute/schemas/` on a schema-less vault) still
|
|
103
|
+
* shows up in the byte comparison.
|
|
104
|
+
*/
|
|
105
|
+
export function serializeExportTree(dir: string): Record<string, string> {
|
|
106
|
+
const out: Record<string, string> = {};
|
|
107
|
+
const walk = (abs: string, rel: string): void => {
|
|
108
|
+
const entries = readdirSync(abs).sort();
|
|
109
|
+
if (entries.length === 0 && rel !== "") {
|
|
110
|
+
out[rel + "/"] = "";
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
const childAbs = join(abs, entry);
|
|
115
|
+
const childRel = rel === "" ? entry : `${rel}/${entry}`;
|
|
116
|
+
if (statSync(childAbs).isDirectory()) {
|
|
117
|
+
walk(childAbs, childRel);
|
|
118
|
+
} else {
|
|
119
|
+
out[childRel] = readFileSync(childAbs, "utf-8");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
walk(dir, "");
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
".parachute/notes-meta/01HX005.yaml": "id: 01HX005\npath: Data/table\nextension: csv\ntags:\n - z-other\nmetadata:\n rows: 2\ncreated_at: 2026-01-01T00:00:00.000Z\nupdated_at: 2026-01-01T00:00:00.000Z\n",
|
|
3
|
+
".parachute/schemas/project.yaml": "description: A long-running effort\nfields:\n status:\n enum:\n - active\n - done\n indexed: true\n type: string\nname: project\nrelationships:\n based-at:\n from: project\n note: freeform\n to: place\n works-on:\n from: person\n to: project\n",
|
|
4
|
+
".parachute/vault.yaml": "description: portable-md byte-stability fixture\nexport_format_version: 1\nexported_at: 2026-07-02T00:00:00.000Z\nname: golden\n",
|
|
5
|
+
"Data/table.csv": "col1,col2\n1,2\n",
|
|
6
|
+
"Inbox/alpha.md": "---\nid: 01HX001\npath: Inbox/alpha\ntags:\n - project\n - z-other\nmetadata:\n ctrl: \"a\\tb\"\n notes: \"line1\\nline2\\nline3\"\n priority: high\n status: active\nlinks:\n - metadata:\n source: git://example\n relationship: derived-from\n target: 01HX002\ncreated_at: 2026-01-01T00:00:00.000Z\nupdated_at: 2026-01-01T00:01:00.000Z\n---\nalpha body\n",
|
|
7
|
+
"Inbox/beta.md": "---\nid: 01HX002\npath: Inbox/beta\ntags:\n - project\nmetadata:\n status: done\ncreated_at: 2026-01-01T00:00:00.000Z\nupdated_at: 2026-01-01T00:00:00.000Z\n---\nbeta body\n",
|
|
8
|
+
"Inbox/skeleton.md": "---\nid: 01HX004\npath: Inbox/skeleton\ntags:\n - project\ncreated_at: 2026-01-01T00:00:00.000Z\nupdated_at: 2026-01-01T00:00:00.000Z\n---\n",
|
|
9
|
+
"_unpathed/01HX003.md": "---\nid: 01HX003\ncreated_at: 2026-01-01T00:00:00.000Z\nupdated_at: 2026-01-01T00:00:00.000Z\n---\nunpathed jot\n"
|
|
10
|
+
}
|