🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@openparachute/vault

Package Overview
Dependencies
Maintainers
1
Versions
121
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@openparachute/vault - npm Package Compare versions

Comparing version
0.7.3-rc.2
to
0.7.3-rc.3
+2
-1
core/src/conformance.ts

@@ -81,3 +81,4 @@ /**

spec.type === "array" ||
spec.type === "object"
spec.type === "object" ||
spec.type === "date"
) {

@@ -84,0 +85,0 @@ out.type = spec.type;

@@ -223,3 +223,3 @@ /**

describe("contract: typed indexes — Decision C: honest type list (#553, flipped from todo)", () => {
it("the update-tag field-type description clarifies only string/integer/boolean/reference are indexable", () => {
it("the update-tag field-type description clarifies only string/integer/boolean/reference/date are indexable", () => {
const updateTag = generateMcpTools(store).find((t) => t.name === "update-tag")!;

@@ -230,4 +230,5 @@ const typeDesc = (updateTag.inputSchema as any).properties.fields.additionalProperties.properties.type.description as string;

// vault#typed-reference-field: `reference` joined the indexable subset
// alongside string/integer/boolean.
expect(typeDesc).toContain("Only string/integer/boolean/reference are INDEXABLE");
// alongside string/integer/boolean. vault#date-field-type: `date` joined
// the same subset — it stores TEXT (ISO-8601), same as string/reference.
expect(typeDesc).toContain("Only string/integer/boolean/reference/date are INDEXABLE");
});

@@ -234,0 +235,0 @@

@@ -70,3 +70,3 @@ import { describe, it, expect, beforeEach } from "bun:test";

it("TYPE_MAP covers string/integer/boolean/reference", () => {
it("TYPE_MAP covers string/integer/boolean/reference/date", () => {
expect(TYPE_MAP.string).toBe("TEXT");

@@ -78,2 +78,5 @@ expect(TYPE_MAP.integer).toBe("INTEGER");

expect(TYPE_MAP.reference).toBe("TEXT");
// vault#date-field-type: `date` also stores like `string` — an ISO-8601
// string, which sorts correctly under a plain TEXT comparison.
expect(TYPE_MAP.date).toBe("TEXT");
});

@@ -217,8 +220,11 @@

it("unsupported field type for indexing throws", async () => {
// vault#date-field-type: `date` JOINED the indexable subset (stores TEXT,
// same as string/reference) — `number` (a float) is the current example
// of a recognized-but-unindexable type; see TYPE_MAP.
expect(() =>
findTool("update-tag").execute({
tag: "project",
fields: { weird: { type: "date", indexed: true } },
fields: { weird: { type: "number", indexed: true } },
}),
).toThrow(/unsupported type "date"/);
).toThrow(/unsupported type "number"/);
});

@@ -225,0 +231,0 @@

@@ -30,3 +30,10 @@ /**

// `core/src/store.ts`'s write path. See tag-schemas.ts's `VALID_FIELD_TYPES`.
export type FieldType = "string" | "integer" | "boolean" | "reference";
// `date` is also stored as TEXT: an indexed `date` field holds an ISO-8601
// string (validated by `tag-schemas.ts`'s `defaultMatchesType` /
// `schema-defaults.ts`'s `valueMatchesType`), and ISO-8601 strings are
// lexicographically comparable — the same reason TEXT-stored `updated_at`
// range queries already work — so `gt`/`gte`/`lt`/`lte`, `date_filter`, and
// `order_by` all fall out of the generic TEXT-column machinery with no
// type-specific SQL.
export type FieldType = "string" | "integer" | "boolean" | "reference" | "date";

@@ -38,2 +45,3 @@ export const TYPE_MAP: Record<FieldType, SqliteType> = {

reference: "TEXT",
date: "TEXT",
};

@@ -40,0 +48,0 @@

@@ -44,2 +44,3 @@ /**

import { Database } from "bun:sqlite";
import { timestampToMs } from "./cursor.js";

@@ -61,3 +62,3 @@ // ---------------------------------------------------------------------------

*/
type?: "string" | "number" | "integer" | "boolean" | "array" | "object" | "reference";
type?: "string" | "number" | "integer" | "boolean" | "array" | "object" | "reference" | "date";
enum?: string[];

@@ -325,2 +326,78 @@ description?: string;

/**
* Normalize `date`-typed field VALUES in `metadata` to canonical UTC ISO
* form (`Z`-suffixed) BEFORE persisting (vault#date-field-type — mixed-
* offset corruption, caught in review).
*
* The bug: every consumer of an indexed `date` field — `buildOperatorClause`
* (query-operators.ts), `date_filter` and `order_by` (notes.ts) — compares
* the generated `meta_<field>` column as raw TEXT. ISO-8601 strings sort
* correctly under a TEXT compare ONLY when every value shares the same
* offset representation. `timestampToMs` (cursor.ts) — the validator both
* `defaultMatchesType` and `valueMatchesType` reuse — correctly ACCEPTS an
* explicit `±HH:MM` offset (validation was never the gap), but a value like
* `"2026-07-16T10:00:00+02:00"` (= `08:00Z`) persisted VERBATIM sorts AFTER
* `"2026-07-16T09:00:00Z"` (= `09:00Z`) under a raw string compare, even
* though the actual instant is earlier — a mixed-offset vault silently gets
* wrong range-query/order_by/date_filter results. This is the exact bug
* class `updated_at` got a dedicated ms-mirror column for (vault#585/#586,
* see `notes.ts`'s `dateFilter` block) that never extended to user-declared
* `date` fields.
*
* The fix is normalize-on-write, not reject-on-write (matching the
* "paths are normalized on write" precedent — instant preserved,
* representation canonicalized) — rejecting offsets would defeat the
* type's own motivation (calendar integrations and other emitters commonly
* produce offset timestamps, not always `Z`). Only a FULL timestamp (has a
* time component) is rewritten, to `new Date(ms).toISOString()` — always
* UTC, millisecond precision, `Z`-suffixed. A bare `YYYY-MM-DD` value is
* left untouched: it has no offset to normalize, is already canonical, and
* prefix-sorts correctly against full timestamps sharing the same calendar
* day. A value that fails `timestampToMs` is left untouched too — that's a
* `type_mismatch` for `valueMatchesType` to catch, not this function's job.
*
* COPY-ON-WRITE, never mutates the input `metadata` object (vault#date-
* field-type review round 2) — `core/` is a published library; a direct
* embedder holding a reference to the object THEY passed in must never see
* it change out from under them. Returns the SAME reference when nothing
* needs rewriting (the common case — no unnecessary allocation on the
* no-op path) or `metadata` is undefined; returns a freshly shallow-copied
* object, with only the rewritten field(s) replaced, the first time a
* rewrite is actually needed. Every call site must use the RETURNED value
* (not assume its input was mutated) — see `store.createNote`/`updateNote`/
* `createNotes`/`createNoteRaw`, all of which reassign their local
* `opts`/`updates`/`input` binding from the return rather than relying on a
* side effect.
*
* Called from `store.createNote`/`updateNote`/`createNotes`/`createNoteRaw`
* — the lowest chokepoint every write path (MCP, REST, import) funnels
* through — BEFORE the row is persisted, so the value that's validated by
* `validateNote`/`valueMatchesType` upstream (offset-tolerant, so
* normalization can't newly fail a write) and the value that's written +
* echoed back on the response are the SAME normalized string.
*/
export function normalizeDateFields(
resolved: ResolvedSchemas,
note: { tags?: string[]; metadata?: Record<string, unknown> },
): Record<string, unknown> | undefined {
const metadata = note.metadata;
if (!metadata) return metadata;
const { mergedFields } = resolveNoteSchemas(resolved, note);
let copy: Record<string, unknown> | undefined;
for (const [fieldName, { spec }] of mergedFields) {
if (spec.type !== "date") continue;
const value = metadata[fieldName];
if (typeof value !== "string") continue;
// Bare `YYYY-MM-DD` (no time component) has no offset to normalize.
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) continue;
const ms = timestampToMs(value);
if (ms === null) continue; // unparseable — valueMatchesType's job, not ours
const canonical = new Date(ms).toISOString();
if (canonical === value) continue;
if (!copy) copy = { ...metadata }; // lazy: only copy once a rewrite is due
copy[fieldName] = canonical;
}
return copy ?? metadata;
}
function fieldSpecsEqual(a: SchemaField, b: SchemaField): boolean {

@@ -396,2 +473,9 @@ if (a.type !== b.type) return false;

return typeof value === "string";
// `date` validates like `string` — an ISO-8601 date (`YYYY-MM-DD`) or
// full RFC3339 timestamp. Reuses `cursor.ts`'s `timestampToMs` (the SAME
// UTC-correct parser `date_filter`'s `updated_at` bound uses), not a
// second date parser — see tag-schemas.ts's `VALID_FIELD_TYPES` doc
// comment.
case "date":
return typeof value === "string" && timestampToMs(value) !== null;
}

@@ -398,0 +482,0 @@ }

@@ -21,2 +21,3 @@ /**

import { loadTagHierarchy, findParentCycle } from "./tag-hierarchy.js";
import { timestampToMs } from "./cursor.js";

@@ -196,3 +197,3 @@ // ---------------------------------------------------------------------------

* Thrown by `upsertTagRecord` (core/src/store.ts's chokepoint) when a
* field's declared `type` isn't one of the six recognized values (vault#555
* field's declared `type` isn't one of the recognized values (vault#555
* — `update-tag{fields:{weird:{type:"frobnicator"}}}` used to be accepted

@@ -260,9 +261,9 @@ * and persisted verbatim, no error, for any NON-indexed field: the only

* The full recognized vocabulary for `TagFieldSchema.type` — storage/
* advisory validation accepts all seven; only `string`/`integer`/`boolean`/
* `reference` are INDEXABLE (that narrower subset is `indexed-fields.ts`'s
* `TYPE_MAP`, enforced separately via `mapFieldType` for `indexed: true`
* fields). Matches `defaultMatchesType`'s switch and `schema-defaults.ts`'s
* `SchemaField.type` union — kept in lockstep by hand across the two
* deliberately-decoupled modules (see `validateFieldDefault`'s doc comment
* for why they don't cross-import).
* advisory validation accepts all eight; only `string`/`integer`/`boolean`/
* `reference`/`date` are INDEXABLE (that narrower subset is
* `indexed-fields.ts`'s `TYPE_MAP`, enforced separately via `mapFieldType`
* for `indexed: true` fields). Matches `defaultMatchesType`'s switch and
* `schema-defaults.ts`'s `SchemaField.type` union — kept in lockstep by hand
* across the two deliberately-decoupled modules (see `validateFieldDefault`'s
* doc comment for why they don't cross-import).
*

@@ -276,7 +277,14 @@ * `reference` (vault#typed-reference-field) is a dual-write field type: the

* `docs/design/typed-reference-field.md` for the full design + known gaps.
*
* `date` stores/validates like `string` (an ISO-8601 date or timestamp) so
* indexed `date` fields sort correctly under a plain TEXT comparison — see
* `defaultMatchesType`'s `"date"` case and `schema-defaults.ts`'s
* `valueMatchesType`, both of which reuse `cursor.ts`'s `timestampToMs`
* (the SAME UTC-correct parser `date_filter`'s `updated_at` bound uses) so
* there's exactly one ISO-parsing implementation in the codebase, not two.
*/
export const VALID_FIELD_TYPES = ["string", "number", "integer", "boolean", "array", "object", "reference"] as const;
export const VALID_FIELD_TYPES = ["string", "number", "integer", "boolean", "array", "object", "reference", "date"] as const;
/**
* Validate that a field's declared `type` is one of the six recognized
* Validate that a field's declared `type` is one of the recognized
* values (vault#555). Returns `null` when `type` is unset (own-field checks

@@ -335,3 +343,3 @@ * elsewhere already treat an unset type as "nothing to check against") or

/** Same seven-type vocabulary as `TagFieldSchema.type`'s doc comment. Unknown/unset types pass (nothing to check against). */
/** Same eight-type vocabulary as `TagFieldSchema.type`'s doc comment. Unknown/unset types pass (nothing to check against). */
function defaultMatchesType(value: unknown, type: string): boolean {

@@ -356,2 +364,9 @@ switch (type) {

return typeof value === "string";
// `date` accepts an ISO-8601 date (`YYYY-MM-DD`) or full RFC3339
// timestamp — the SAME grammar `date_filter`'s `updated_at` bound
// parses (`timestampToMs`, imported from cursor.ts), not a second,
// independently-drifting date parser. See VALID_FIELD_TYPES's doc
// comment.
case "date":
return typeof value === "string" && timestampToMs(value) !== null;
default:

@@ -776,3 +791,3 @@ return true;

reason: "unsupported_indexed_type",
message: `field "${fieldName}" has unsupported type "${spec.type}" for indexing (supported: string, integer, boolean, reference)`,
message: `field "${fieldName}" has unsupported type "${spec.type}" for indexing (supported: string, integer, boolean, reference, date)`,
});

@@ -779,0 +794,0 @@ } else {

@@ -453,3 +453,3 @@ import type { Database } from "bun:sqlite";

getNotes(ids: string[]): Promise<Note[]>;
updateNote(id: string, updates: { content?: string; append?: string; prepend?: string; path?: string; extension?: string; metadata?: Record<string, unknown>; created_at?: string; skipUpdatedAt?: boolean; actor?: string | null; via?: string | null; if_updated_at?: string }): Promise<Note>;
updateNote(id: string, updates: { content?: string; append?: string; prepend?: string; path?: string; extension?: string; metadata?: Record<string, unknown>; created_at?: string; skipUpdatedAt?: boolean; actor?: string | null; via?: string | null; if_updated_at?: string; tagsForSchemaResolution?: string[] }): Promise<Note>;
/**

@@ -456,0 +456,0 @@ * Set a note's `created_at` and `updated_at` explicitly. Import-only:

{
"name": "@openparachute/vault",
"version": "0.7.3-rc.2",
"version": "0.7.3-rc.3",
"description": "Agent-native knowledge graph. Notes, tags, links over MCP.",

@@ -5,0 +5,0 @@ "module": "src/cli.ts",

@@ -318,3 +318,3 @@ /**

// Positive control — every one of the seven recognized types (indexable or
// Positive control — every one of the recognized types (indexable or
// not) is accepted without complaint.

@@ -333,2 +333,3 @@ it("REST PUT /api/tags/:name accepts every recognized field type", async () => {

g: { type: "reference" },
h: { type: "date" },
},

@@ -335,0 +336,0 @@ }),

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display