@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.
@@ -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
- export type FieldType = "string" | "integer" | "boolean";
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,