@openparachute/vault 0.7.0-rc.9 → 0.7.1
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/aggregate.test.ts +260 -0
- package/core/src/contract-taxonomy.test.ts +26 -2
- package/core/src/contract-typed-index.test.ts +4 -2
- package/core/src/core.test.ts +669 -0
- package/core/src/doctor.ts +23 -1
- package/core/src/indexed-fields.test.ts +4 -1
- package/core/src/indexed-fields.ts +6 -1
- package/core/src/links.ts +22 -0
- package/core/src/mcp.ts +310 -51
- package/core/src/notes.ts +344 -18
- package/core/src/query-warnings.ts +60 -12
- package/core/src/schema-defaults.ts +7 -1
- package/core/src/seed-packs.ts +19 -5
- package/core/src/store.ts +129 -5
- package/core/src/tag-schemas.ts +20 -7
- package/core/src/types.ts +89 -0
- package/core/src/ulid.test.ts +56 -0
- package/core/src/ulid.ts +116 -0
- package/core/src/vault-projection.ts +19 -1
- package/core/src/wikilinks.test.ts +205 -1
- package/core/src/wikilinks.ts +200 -35
- package/package.json +1 -1
- package/src/aggregate-routes.test.ts +230 -0
- package/src/contract-errors.test.ts +2 -1
- package/src/contract-search.test.ts +17 -0
- package/src/mcp-link-warnings-scope.test.ts +144 -0
- package/src/mcp-query-notes-aggregate-scope.test.ts +183 -0
- package/src/mcp-tools.ts +76 -17
- package/src/routes.ts +272 -23
- package/src/routing.test.ts +167 -0
- package/src/routing.ts +47 -11
- package/src/tag-integrity-mcp.test.ts +45 -6
- package/src/tag-scope.ts +10 -1
- package/src/vault.test.ts +469 -21
- package/web/ui/dist/assets/{index-B8pGeQam.js → index-CJ6NtIwh.js} +1 -1
- package/web/ui/dist/index.html +1 -1
package/core/src/doctor.ts
CHANGED
|
@@ -30,7 +30,14 @@
|
|
|
30
30
|
* flagged as a possible stale reference to a renamed/merged/deleted tag
|
|
31
31
|
* — the PM's `metadata.epic` drift class from the #552 write-up. Vault
|
|
32
32
|
* has no history of past tag names, so this can never be certain —
|
|
33
|
-
* hence heuristic.
|
|
33
|
+
* hence heuristic. Skips any key declared as an ENUM field on any tag
|
|
34
|
+
* schema (vault#570) — an enum is a closed, schema-governed vocabulary
|
|
35
|
+
* (drift there already surfaces as `enum_mismatch` validation, not this
|
|
36
|
+
* heuristic), so one enum value coincidentally matching an unrelated
|
|
37
|
+
* live tag name (e.g. a `status` enum with `"active"`/`"archived"`/
|
|
38
|
+
* `"done"` where some other tag happens to be named "archived") no
|
|
39
|
+
* longer drags the enum's OTHER legitimate values into a false
|
|
40
|
+
* "stale tag reference" finding.
|
|
34
41
|
*
|
|
35
42
|
* Tag-scope (vault#552): pass `allowedTags` (the caller's EXPANDED
|
|
36
43
|
* allowlist, e.g. from `expandTokenTagScope`) to restrict every finding to
|
|
@@ -44,6 +51,7 @@ import { Database } from "bun:sqlite";
|
|
|
44
51
|
import { loadTagHierarchy, findHierarchyCycles } from "./tag-hierarchy.js";
|
|
45
52
|
import { listIndexedFields } from "./indexed-fields.js";
|
|
46
53
|
import { pruneOrphanedIndexedFields } from "./indexed-fields.js";
|
|
54
|
+
import { loadSchemaConfig } from "./schema-defaults.js";
|
|
47
55
|
|
|
48
56
|
export type DoctorFindingType =
|
|
49
57
|
| "dangling_parent_name"
|
|
@@ -304,6 +312,19 @@ function scanDeadTagMetadataReferences(db: Database, allowedTags: Set<string> |
|
|
|
304
312
|
(db.prepare("SELECT name FROM tags").all() as { name: string }[]).map((r) => r.name),
|
|
305
313
|
);
|
|
306
314
|
|
|
315
|
+
// Schema-declared enum fields (vault#570) — a metadata key declared as an
|
|
316
|
+
// enum on ANY tag's schema is a closed, schema-governed vocabulary; skip
|
|
317
|
+
// it from this heuristic entirely so the enum's own values coincidentally
|
|
318
|
+
// colliding with an unrelated live tag name can't drag its OTHER
|
|
319
|
+
// legitimate values into a false "stale tag reference" finding (see the
|
|
320
|
+
// module doc comment above).
|
|
321
|
+
const enumFieldKeys = new Set<string>();
|
|
322
|
+
for (const fields of loadSchemaConfig(db).tagToFields.values()) {
|
|
323
|
+
for (const [key, spec] of Object.entries(fields)) {
|
|
324
|
+
if (Array.isArray(spec.enum) && spec.enum.length > 0) enumFieldKeys.add(key);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
307
328
|
const rows = db
|
|
308
329
|
.prepare("SELECT id, metadata FROM notes WHERE metadata IS NOT NULL AND metadata != ''")
|
|
309
330
|
.all() as { id: string; metadata: string }[];
|
|
@@ -315,6 +336,7 @@ function scanDeadTagMetadataReferences(db: Database, allowedTags: Set<string> |
|
|
|
315
336
|
try { parsed = JSON.parse(row.metadata); } catch { continue; }
|
|
316
337
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
317
338
|
for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
|
|
339
|
+
if (enumFieldKeys.has(key)) continue;
|
|
318
340
|
if (typeof value !== "string" || value.length === 0) continue;
|
|
319
341
|
let valueMap = byKey.get(key);
|
|
320
342
|
if (!valueMap) { valueMap = new Map(); byKey.set(key, valueMap); }
|
|
@@ -67,10 +67,13 @@ describe("indexed-fields: module", () => {
|
|
|
67
67
|
expect(() => validateFieldName("'; DROP TABLE notes; --")).toThrow(IndexedFieldError);
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
it("TYPE_MAP covers string/integer/boolean", () => {
|
|
70
|
+
it("TYPE_MAP covers string/integer/boolean/reference", () => {
|
|
71
71
|
expect(TYPE_MAP.string).toBe("TEXT");
|
|
72
72
|
expect(TYPE_MAP.integer).toBe("INTEGER");
|
|
73
73
|
expect(TYPE_MAP.boolean).toBe("INTEGER");
|
|
74
|
+
// vault#typed-reference-field: `reference` stores like `string` — the
|
|
75
|
+
// metadata VALUE (an id/path/title) is what's indexed here.
|
|
76
|
+
expect(TYPE_MAP.reference).toBe("TEXT");
|
|
74
77
|
});
|
|
75
78
|
|
|
76
79
|
it("declareField creates column + index on first declaration", () => {
|
|
@@ -23,12 +23,17 @@ import { Database } from "bun:sqlite";
|
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
|
|
25
25
|
export type SqliteType = "TEXT" | "INTEGER";
|
|
26
|
-
|
|
26
|
+
// `reference` (vault#typed-reference-field) is stored as TEXT — same as
|
|
27
|
+
// `string` — because the metadata VALUE (an id/path/title) is what's
|
|
28
|
+
// indexed here; the resolved graph link is a separate concern maintained by
|
|
29
|
+
// `core/src/store.ts`'s write path. See tag-schemas.ts's `VALID_FIELD_TYPES`.
|
|
30
|
+
export type FieldType = "string" | "integer" | "boolean" | "reference";
|
|
27
31
|
|
|
28
32
|
export const TYPE_MAP: Record<FieldType, SqliteType> = {
|
|
29
33
|
string: "TEXT",
|
|
30
34
|
integer: "INTEGER",
|
|
31
35
|
boolean: "INTEGER",
|
|
36
|
+
reference: "TEXT",
|
|
32
37
|
};
|
|
33
38
|
|
|
34
39
|
export interface IndexedField {
|
package/core/src/links.ts
CHANGED
|
@@ -35,6 +35,28 @@ export function deleteLink(
|
|
|
35
35
|
).run(sourceId, targetId, relationship);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Delete every outbound link from `sourceId` carrying `relationship`,
|
|
40
|
+
* regardless of target. Used by the `reference`-field auto-link sync
|
|
41
|
+
* (core/src/store.ts's `syncReferenceFieldLinks`, vault#typed-reference-field)
|
|
42
|
+
* to re-establish "this field's link" as a single source of truth: rather
|
|
43
|
+
* than tracking the PRIOR resolved target to delete exactly one edge, the
|
|
44
|
+
* sync clears every edge under the field's relationship name and recreates
|
|
45
|
+
* at most one from the field's current value. This is safe because a
|
|
46
|
+
* `reference` field's relationship name is field-owned — a hand-authored
|
|
47
|
+
* `links` entry using the SAME relationship name on the SAME note is
|
|
48
|
+
* expected to be superseded by the field, not preserved alongside it.
|
|
49
|
+
*/
|
|
50
|
+
export function deleteLinksBySourceRelationship(
|
|
51
|
+
db: Database,
|
|
52
|
+
sourceId: string,
|
|
53
|
+
relationship: string,
|
|
54
|
+
): void {
|
|
55
|
+
db.prepare(
|
|
56
|
+
"DELETE FROM links WHERE source_id = ? AND relationship = ?",
|
|
57
|
+
).run(sourceId, relationship);
|
|
58
|
+
}
|
|
59
|
+
|
|
38
60
|
export function getLinks(
|
|
39
61
|
db: Database,
|
|
40
62
|
noteId: string,
|