@happyvertical/smrt-core 0.39.13 → 0.39.15

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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "version": "1.0.0",
3
- "timestamp": 1783987286059,
3
+ "timestamp": 1784008176708,
4
4
  "packageName": "@happyvertical/smrt-core",
5
- "packageVersion": "0.39.13",
5
+ "packageVersion": "0.39.15",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-core:SmrtClass": {
8
8
  "name": "smrtclass",
@@ -8,18 +8,35 @@ export interface BackfillRecord {
8
8
  description: string | null;
9
9
  packageName: string | null;
10
10
  }
11
+ /**
12
+ * A tracker query proved that its inherited root initialization is stale.
13
+ *
14
+ * Transaction coordinators must roll back the current transaction, refresh
15
+ * the root tracker outside that transaction, and retry with a fresh handle.
16
+ */
17
+ export declare class BackfillTableUnavailableError extends Error {
18
+ constructor(cause: unknown);
19
+ }
11
20
  export declare class BackfillTracker {
12
21
  private readonly db;
13
- /**
14
- * Memoized initialization promise. Storing the promise (rather than a
15
- * `boolean` flag set after the DDL completes) makes `initialize()` safe
16
- * under concurrency: multiple parallel callers all `await` the same
17
- * in-flight promise instead of independently re-running the DDL. On
18
- * error the slot is cleared so the next caller retries — a transient
19
- * failure doesn't permanently poison the instance.
20
- */
21
22
  private initializePromise;
22
23
  constructor(options: BackfillTrackerOptions);
24
+ /**
25
+ * Mark a transaction handle as inheriting a table initialized durably by its
26
+ * root adapter. Framework transaction coordinators use this after root DDL
27
+ * commits so transaction-local trackers can skip redundant `CREATE TABLE`.
28
+ */
29
+ static inheritInitialization(db: DatabaseInterface, rootDb: DatabaseInterface): void;
30
+ /**
31
+ * Suppress lazy DDL on a caller-owned transaction.
32
+ *
33
+ * Marker reads may proceed when the table already exists. A missing table
34
+ * raises `BackfillTableUnavailableError` so the caller can fail closed or
35
+ * retry with a root adapter.
36
+ */
37
+ static requireExistingTable(db: DatabaseInterface): void;
38
+ /** Forget a root initialization after a schema lifecycle reset. */
39
+ static invalidateInitialization(db: DatabaseInterface): void;
23
40
  /**
24
41
  * Create the `_smrt_backfills` table if it doesn't already exist.
25
42
  * Safe to call repeatedly and concurrently.
@@ -80,5 +97,7 @@ export declare class BackfillTracker {
80
97
  }>;
81
98
  /** List every recorded backfill, oldest first. */
82
99
  listApplied(): Promise<BackfillRecord[]>;
100
+ private queryBackfillTable;
101
+ private resetInitialization;
83
102
  }
84
103
  //# sourceMappingURL=backfill-tracker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"backfill-tracker.d.ts","sourceRoot":"","sources":["../../src/migrations/backfill-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAG5D,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,iBAAiB,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;IACvC;;;;;;;OAOG;IACH,OAAO,CAAC,iBAAiB,CAA8B;gBAE3C,OAAO,EAAE,sBAAsB;IAI3C;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAajC,iDAAiD;IAC3C,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS/C;;;;OAIG;IACG,aAAa,CACjB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAO,GAC3D,OAAO,CAAC,IAAI,CAAC;IAYhB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACG,YAAY,CAAC,CAAC,EAClB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAO,GAC3D,OAAO,CAAC;QAAE,GAAG,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,GAAG,EAAE,IAAI,CAAC;QAAC,MAAM,EAAE,CAAC,CAAA;KAAE,CAAC;IAOnE,kDAAkD;IAC5C,WAAW,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;CAY/C"}
1
+ {"version":3,"file":"backfill-tracker.d.ts","sourceRoot":"","sources":["../../src/migrations/backfill-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAG5D,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,iBAAiB,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;GAKG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;gBAC1C,KAAK,EAAE,OAAO;CAI3B;AAQD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;IACvC,OAAO,CAAC,iBAAiB,CAA8B;gBAE3C,OAAO,EAAE,sBAAsB;IAI3C;;;;OAIG;IACH,MAAM,CAAC,qBAAqB,CAC1B,EAAE,EAAE,iBAAiB,EACrB,MAAM,EAAE,iBAAiB,GACxB,IAAI;IAOP;;;;;;OAMG;IACH,MAAM,CAAC,oBAAoB,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI;IAOxD,mEAAmE;IACnE,MAAM,CAAC,wBAAwB,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI;IAI5D;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAgDjC,iDAAiD;IAC3C,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQ/C;;;;OAIG;IACG,aAAa,CACjB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAO,GAC3D,OAAO,CAAC,IAAI,CAAC;IAWhB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACG,YAAY,CAAC,CAAC,EAClB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,GAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAO,GAC3D,OAAO,CAAC;QAAE,GAAG,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,GAAG,EAAE,IAAI,CAAC;QAAC,MAAM,EAAE,CAAC,CAAA;KAAE,CAAC;IAOnE,kDAAkD;IAC5C,WAAW,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;YAYhC,kBAAkB;IAgBhC,OAAO,CAAC,mBAAmB;CAM5B"}
@@ -1,24 +1,75 @@
1
1
  import { CREATE_SMRT_BACKFILLS_TABLE } from "../system/schema.js";
2
2
  //#region src/migrations/backfill-tracker.ts
3
+ /**
4
+ * A tracker query proved that its inherited root initialization is stale.
5
+ *
6
+ * Transaction coordinators must roll back the current transaction, refresh
7
+ * the root tracker outside that transaction, and retry with a fresh handle.
8
+ */
9
+ var BackfillTableUnavailableError = class extends Error {
10
+ constructor(cause) {
11
+ super("The shared _smrt_backfills table is unavailable.", { cause });
12
+ this.name = "BackfillTableUnavailableError";
13
+ }
14
+ };
15
+ var initializationByObject = /* @__PURE__ */ new WeakMap();
16
+ var inheritedInitializationByHandle = /* @__PURE__ */ new WeakMap();
3
17
  var BackfillTracker = class {
4
18
  db;
5
- /**
6
- * Memoized initialization promise. Storing the promise (rather than a
7
- * `boolean` flag set after the DDL completes) makes `initialize()` safe
8
- * under concurrency: multiple parallel callers all `await` the same
9
- * in-flight promise instead of independently re-running the DDL. On
10
- * error the slot is cleared so the next caller retries — a transient
11
- * failure doesn't permanently poison the instance.
12
- */
13
19
  initializePromise = null;
14
20
  constructor(options) {
15
21
  this.db = options.db;
16
22
  }
17
23
  /**
24
+ * Mark a transaction handle as inheriting a table initialized durably by its
25
+ * root adapter. Framework transaction coordinators use this after root DDL
26
+ * commits so transaction-local trackers can skip redundant `CREATE TABLE`.
27
+ */
28
+ static inheritInitialization(db, rootDb) {
29
+ inheritedInitializationByHandle.set(db, databaseTargetIdentity(rootDb));
30
+ }
31
+ /**
32
+ * Suppress lazy DDL on a caller-owned transaction.
33
+ *
34
+ * Marker reads may proceed when the table already exists. A missing table
35
+ * raises `BackfillTableUnavailableError` so the caller can fail closed or
36
+ * retry with a root adapter.
37
+ */
38
+ static requireExistingTable(db) {
39
+ inheritedInitializationByHandle.set(db, databaseTargetIdentity(db));
40
+ }
41
+ /** Forget a root initialization after a schema lifecycle reset. */
42
+ static invalidateInitialization(db) {
43
+ deleteSharedInitialization(databaseTargetIdentity(db));
44
+ }
45
+ /**
18
46
  * Create the `_smrt_backfills` table if it doesn't already exist.
19
47
  * Safe to call repeatedly and concurrently.
20
48
  */
21
49
  async initialize() {
50
+ if (inheritedInitializationByHandle.has(this.db)) return;
51
+ const target = databaseTargetIdentity(this.db);
52
+ const existing = getSharedInitialization(target);
53
+ if (existing) {
54
+ await existing;
55
+ if (!isRootDatabase(this.db)) return;
56
+ try {
57
+ await this.db.query("SELECT 1 FROM _smrt_backfills LIMIT 1");
58
+ return;
59
+ } catch (error) {
60
+ if (!isMissingBackfillTableError(error)) throw error;
61
+ if (getSharedInitialization(target) === existing) deleteSharedInitialization(target);
62
+ return this.initialize();
63
+ }
64
+ }
65
+ if (isRootDatabase(this.db)) {
66
+ const initialization = this.db.query(CREATE_SMRT_BACKFILLS_TABLE).then(() => void 0).catch((error) => {
67
+ if (getSharedInitialization(target) === initialization) deleteSharedInitialization(target);
68
+ throw error;
69
+ });
70
+ setSharedInitialization(target, initialization);
71
+ return initialization;
72
+ }
22
73
  if (!this.initializePromise) this.initializePromise = this.db.query(CREATE_SMRT_BACKFILLS_TABLE).then(() => void 0).catch((error) => {
23
74
  this.initializePromise = null;
24
75
  throw error;
@@ -27,8 +78,7 @@ var BackfillTracker = class {
27
78
  }
28
79
  /** Has the backfill named `name` already run? */
29
80
  async isApplied(name) {
30
- await this.initialize();
31
- return (await this.db.query("SELECT 1 FROM _smrt_backfills WHERE name = ? LIMIT 1", name)).rows.length > 0;
81
+ return (await this.queryBackfillTable("SELECT 1 FROM _smrt_backfills WHERE name = ? LIMIT 1", name)).rows.length > 0;
32
82
  }
33
83
  /**
34
84
  * Record that the backfill named `name` ran. Uses INSERT ... ON CONFLICT
@@ -36,8 +86,7 @@ var BackfillTracker = class {
36
86
  * about distinguishing "already applied" from "just applied".
37
87
  */
38
88
  async recordApplied(name, options = {}) {
39
- await this.initialize();
40
- await this.db.query(`INSERT INTO _smrt_backfills (name, description, package_name)
89
+ await this.queryBackfillTable(`INSERT INTO _smrt_backfills (name, description, package_name)
41
90
  VALUES (?, ?, ?)
42
91
  ON CONFLICT (name) DO NOTHING`, name, options.description ?? null, options.packageName ?? null);
43
92
  }
@@ -87,15 +136,68 @@ var BackfillTracker = class {
87
136
  }
88
137
  /** List every recorded backfill, oldest first. */
89
138
  async listApplied() {
90
- await this.initialize();
91
- return (await this.db.query("SELECT name, applied_at, description, package_name FROM _smrt_backfills ORDER BY applied_at ASC, name ASC")).rows.map((row) => ({
139
+ return (await this.queryBackfillTable("SELECT name, applied_at, description, package_name FROM _smrt_backfills ORDER BY applied_at ASC, name ASC")).rows.map((row) => ({
92
140
  name: String(row.name ?? ""),
93
141
  appliedAt: normalizeAppliedAt(row.applied_at),
94
142
  description: row.description == null ? null : String(row.description),
95
143
  packageName: row.package_name == null ? null : String(row.package_name)
96
144
  }));
97
145
  }
146
+ async queryBackfillTable(sql, ...params) {
147
+ await this.initialize();
148
+ try {
149
+ return await this.db.query(sql, ...params);
150
+ } catch (error) {
151
+ if (!isMissingBackfillTableError(error)) throw error;
152
+ const inheritedRoot = inheritedInitializationByHandle.get(this.db);
153
+ this.resetInitialization(inheritedRoot);
154
+ if (inheritedRoot) throw new BackfillTableUnavailableError(error);
155
+ await this.initialize();
156
+ return this.db.query(sql, ...params);
157
+ }
158
+ }
159
+ resetInitialization(inheritedRoot) {
160
+ this.initializePromise = null;
161
+ inheritedInitializationByHandle.delete(this.db);
162
+ deleteSharedInitialization(databaseTargetIdentity(this.db));
163
+ if (inheritedRoot) deleteSharedInitialization(inheritedRoot);
164
+ }
98
165
  };
166
+ function databaseTargetIdentity(db) {
167
+ const client = db.client;
168
+ return client !== null && (typeof client === "object" || typeof client === "function") ? client : db;
169
+ }
170
+ function isRootDatabase(db) {
171
+ return typeof db.beginTransaction === "function";
172
+ }
173
+ function getSharedInitialization(target) {
174
+ return initializationByObject.get(target);
175
+ }
176
+ function setSharedInitialization(target, initialization) {
177
+ initializationByObject.set(target, initialization);
178
+ }
179
+ function deleteSharedInitialization(target) {
180
+ initializationByObject.delete(target);
181
+ }
182
+ function isMissingBackfillTableError(error) {
183
+ const pending = [error];
184
+ const seen = /* @__PURE__ */ new Set();
185
+ const missingPattern = /(?:no such table|does not exist|doesn't exist|unknown table|not found)/iu;
186
+ while (pending.length > 0) {
187
+ const current = pending.pop();
188
+ if (typeof current === "string") {
189
+ if (current.includes("_smrt_backfills") && missingPattern.test(current)) return true;
190
+ continue;
191
+ }
192
+ if (!current || typeof current !== "object" || seen.has(current)) continue;
193
+ seen.add(current);
194
+ if (current instanceof Error && current.message.includes("_smrt_backfills") && missingPattern.test(current.message)) return true;
195
+ const details = current;
196
+ pending.push(details.cause, details.context, details.details, details.originalError);
197
+ if (current instanceof AggregateError) pending.push(...current.errors);
198
+ }
199
+ return false;
200
+ }
99
201
  /**
100
202
  * Convert a raw `applied_at` cell into an ISO 8601 string.
101
203
  *
@@ -120,6 +222,6 @@ function normalizeAppliedAt(value) {
120
222
  return parsed.toISOString();
121
223
  }
122
224
  //#endregion
123
- export { BackfillTracker };
225
+ export { BackfillTableUnavailableError, BackfillTracker };
124
226
 
125
227
  //# sourceMappingURL=backfill-tracker.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"backfill-tracker.js","names":[],"sources":["../../src/migrations/backfill-tracker.ts"],"sourcesContent":["/**\n * Tracks application-specific data backfills (not schema migrations).\n *\n * Sits next to `MigrationTracker` but is intentionally minimal — backfills\n * have no checksum, no rollback semantics, no DDL. They're \"this data\n * correction has run, don't run it again\" markers, keyed by a stable\n * backfill name the app chooses.\n *\n * Backed by the `_smrt_backfills` system table (see\n * `../system/schema.ts`). The table is created lazily on first use —\n * every public method (`isApplied`, `recordApplied`, `runIfPending`,\n * `listApplied`) awaits `initialize()` itself, so consumers never need\n * to call `initialize()` explicitly.\n */\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { CREATE_SMRT_BACKFILLS_TABLE } from '../system/schema.js';\n\nexport interface BackfillTrackerOptions {\n db: DatabaseInterface;\n}\n\nexport interface BackfillRecord {\n name: string;\n appliedAt: string;\n description: string | null;\n packageName: string | null;\n}\n\nexport class BackfillTracker {\n private readonly db: DatabaseInterface;\n /**\n * Memoized initialization promise. Storing the promise (rather than a\n * `boolean` flag set after the DDL completes) makes `initialize()` safe\n * under concurrency: multiple parallel callers all `await` the same\n * in-flight promise instead of independently re-running the DDL. On\n * error the slot is cleared so the next caller retries — a transient\n * failure doesn't permanently poison the instance.\n */\n private initializePromise: Promise<void> | null = null;\n\n constructor(options: BackfillTrackerOptions) {\n this.db = options.db;\n }\n\n /**\n * Create the `_smrt_backfills` table if it doesn't already exist.\n * Safe to call repeatedly and concurrently.\n */\n async initialize(): Promise<void> {\n if (!this.initializePromise) {\n this.initializePromise = this.db\n .query(CREATE_SMRT_BACKFILLS_TABLE)\n .then(() => undefined)\n .catch((error) => {\n this.initializePromise = null;\n throw error;\n });\n }\n return this.initializePromise;\n }\n\n /** Has the backfill named `name` already run? */\n async isApplied(name: string): Promise<boolean> {\n await this.initialize();\n const result = await this.db.query(\n 'SELECT 1 FROM _smrt_backfills WHERE name = ? LIMIT 1',\n name,\n );\n return result.rows.length > 0;\n }\n\n /**\n * Record that the backfill named `name` ran. Uses INSERT ... ON CONFLICT\n * so callers don't need to check `isApplied` first if they don't care\n * about distinguishing \"already applied\" from \"just applied\".\n */\n async recordApplied(\n name: string,\n options: { description?: string; packageName?: string } = {},\n ): Promise<void> {\n await this.initialize();\n await this.db.query(\n `INSERT INTO _smrt_backfills (name, description, package_name)\n VALUES (?, ?, ?)\n ON CONFLICT (name) DO NOTHING`,\n name,\n options.description ?? null,\n options.packageName ?? null,\n );\n }\n\n /**\n * Convenience wrapper: if `name` hasn't been applied, run `fn` and\n * record the result. Returns `{ ran, result }`:\n * - `{ ran: true, result: T }` — backfill was pending and `fn` ran\n * - `{ ran: false, result: null }` — backfill was already applied\n *\n * The discriminator avoids the `T | null` ambiguity an earlier shape had\n * (a backfill that legitimately returns `null` was indistinguishable\n * from \"already applied\"). Callers check `ran` to decide whether `fn`\n * executed; `result` carries whatever `fn` returned.\n *\n * This is the common pattern an app's migrate orchestration follows:\n * a list of `(name, runner)` pairs, each guarded by isApplied/run/record.\n *\n * **Single-runner only.** This is a check-then-act pattern with no\n * locking: two processes that call `runIfPending` for the same `name`\n * concurrently can both observe `isApplied === false`, both execute\n * `fn`, and only then race to `recordApplied`. The `ON CONFLICT DO\n * NOTHING` in `recordApplied` keeps the table clean but does NOT prevent\n * `fn` from running twice. Acceptable when backfills are run from a\n * single deploy-time migration step (the common case); if you need to\n * fan out backfills across multiple workers, wrap the call in your own\n * mutex (e.g. a Postgres advisory lock keyed by backfill name).\n *\n * **`fn` MUST be idempotent.** The order is `fn` → `recordApplied`, with\n * no surrounding transaction. If `fn` succeeds but `recordApplied`\n * throws (DB connection drops, transient error), the work has happened\n * but is not recorded — the next call sees `isApplied === false` and\n * re-runs `fn`. For non-idempotent work (slug rewrites that consume\n * source rows, one-shot lookups), wrap the entire call in a transaction\n * yourself or structure `fn` so a second execution is a no-op.\n */\n async runIfPending<T>(\n name: string,\n fn: () => Promise<T>,\n options: { description?: string; packageName?: string } = {},\n ): Promise<{ ran: false; result: null } | { ran: true; result: T }> {\n if (await this.isApplied(name)) return { ran: false, result: null };\n const result = await fn();\n await this.recordApplied(name, options);\n return { ran: true, result };\n }\n\n /** List every recorded backfill, oldest first. */\n async listApplied(): Promise<BackfillRecord[]> {\n await this.initialize();\n const result = await this.db.query(\n 'SELECT name, applied_at, description, package_name FROM _smrt_backfills ORDER BY applied_at ASC, name ASC',\n );\n return result.rows.map((row) => ({\n name: String(row.name ?? ''),\n appliedAt: normalizeAppliedAt(row.applied_at),\n description: row.description == null ? null : String(row.description),\n packageName: row.package_name == null ? null : String(row.package_name),\n }));\n }\n}\n\n/**\n * Convert a raw `applied_at` cell into an ISO 8601 string.\n *\n * Different DB drivers return this column in different shapes:\n * - Postgres `node-pg` returns a JS `Date` directly.\n * - SQLite drivers typically return the `CURRENT_TIMESTAMP` literal\n * `'YYYY-MM-DD HH:MM:SS'` (no `T`, no timezone). V8's `Date`\n * constructor parses this as local time, but that's\n * implementation-defined and other runtimes may return Invalid Date.\n * - A corrupted row could return any string.\n *\n * If the value parses to a valid Date we return `toISOString()`. If it\n * doesn't (or the column is null/empty), we fall back to the raw string —\n * better to surface a non-ISO timestamp than crash `listApplied()` with a\n * `RangeError` and take down the caller.\n */\nfunction normalizeAppliedAt(value: unknown): string {\n if (value == null || value === '') return '';\n const raw = String(value);\n const parsed = new Date(raw);\n if (Number.isNaN(parsed.getTime())) {\n return raw;\n }\n return parsed.toISOString();\n}\n"],"mappings":";;AA4BA,IAAa,kBAAb,MAA6B;CAC3B;;;;;;;;;CASA,oBAAkD;CAElD,YAAY,SAAiC;EAC3C,KAAK,KAAK,QAAQ;CACpB;;;;;CAMA,MAAM,aAA4B;EAChC,IAAI,CAAC,KAAK,mBACR,KAAK,oBAAoB,KAAK,GAC3B,MAAM,2BAA2B,CAAC,CAClC,WAAW,KAAA,CAAS,CAAC,CACrB,OAAO,UAAU;GAChB,KAAK,oBAAoB;GACzB,MAAM;EACR,CAAC;EAEL,OAAO,KAAK;CACd;;CAGA,MAAM,UAAU,MAAgC;EAC9C,MAAM,KAAK,WAAW;EAKtB,QAAO,MAJc,KAAK,GAAG,MAC3B,wDACA,IACF,EAAA,CACc,KAAK,SAAS;CAC9B;;;;;;CAOA,MAAM,cACJ,MACA,UAA0D,CAAC,GAC5C;EACf,MAAM,KAAK,WAAW;EACtB,MAAM,KAAK,GAAG,MACZ;;uCAGA,MACA,QAAQ,eAAe,MACvB,QAAQ,eAAe,IACzB;CACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCA,MAAM,aACJ,MACA,IACA,UAA0D,CAAC,GACO;EAClE,IAAI,MAAM,KAAK,UAAU,IAAI,GAAG,OAAO;GAAE,KAAK;GAAO,QAAQ;EAAK;EAClE,MAAM,SAAS,MAAM,GAAG;EACxB,MAAM,KAAK,cAAc,MAAM,OAAO;EACtC,OAAO;GAAE,KAAK;GAAM;EAAO;CAC7B;;CAGA,MAAM,cAAyC;EAC7C,MAAM,KAAK,WAAW;EAItB,QAAO,MAHc,KAAK,GAAG,MAC3B,2GACF,EAAA,CACc,KAAK,KAAK,SAAS;GAC/B,MAAM,OAAO,IAAI,QAAQ,EAAE;GAC3B,WAAW,mBAAmB,IAAI,UAAU;GAC5C,aAAa,IAAI,eAAe,OAAO,OAAO,OAAO,IAAI,WAAW;GACpE,aAAa,IAAI,gBAAgB,OAAO,OAAO,OAAO,IAAI,YAAY;EACxE,EAAE;CACJ;AACF;;;;;;;;;;;;;;;;;AAkBA,SAAS,mBAAmB,OAAwB;CAClD,IAAI,SAAS,QAAQ,UAAU,IAAI,OAAO;CAC1C,MAAM,MAAM,OAAO,KAAK;CACxB,MAAM,SAAS,IAAI,KAAK,GAAG;CAC3B,IAAI,OAAO,MAAM,OAAO,QAAQ,CAAC,GAC/B,OAAO;CAET,OAAO,OAAO,YAAY;AAC5B"}
1
+ {"version":3,"file":"backfill-tracker.js","names":[],"sources":["../../src/migrations/backfill-tracker.ts"],"sourcesContent":["/**\n * Tracks application-specific data backfills (not schema migrations).\n *\n * Sits next to `MigrationTracker` but is intentionally minimal — backfills\n * have no checksum, no rollback semantics, no DDL. They're \"this data\n * correction has run, don't run it again\" markers, keyed by a stable\n * backfill name the app chooses.\n *\n * Backed by the `_smrt_backfills` system table (see\n * `../system/schema.ts`). The table is created lazily on first use —\n * every public method (`isApplied`, `recordApplied`, `runIfPending`,\n * `listApplied`) awaits `initialize()` itself, so consumers never need\n * to call `initialize()` explicitly.\n */\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { CREATE_SMRT_BACKFILLS_TABLE } from '../system/schema.js';\n\nexport interface BackfillTrackerOptions {\n db: DatabaseInterface;\n}\n\nexport interface BackfillRecord {\n name: string;\n appliedAt: string;\n description: string | null;\n packageName: string | null;\n}\n\n/**\n * A tracker query proved that its inherited root initialization is stale.\n *\n * Transaction coordinators must roll back the current transaction, refresh\n * the root tracker outside that transaction, and retry with a fresh handle.\n */\nexport class BackfillTableUnavailableError extends Error {\n constructor(cause: unknown) {\n super('The shared _smrt_backfills table is unavailable.', { cause });\n this.name = 'BackfillTableUnavailableError';\n }\n}\n\n// Only root adapters can establish a durable shared initialization. A tracker\n// created around a transaction handle may have its DDL rolled back later, so\n// transaction-local successes must never poison this cache.\nconst initializationByObject = new WeakMap<object, Promise<void>>();\nconst inheritedInitializationByHandle = new WeakMap<object, object>();\n\nexport class BackfillTracker {\n private readonly db: DatabaseInterface;\n private initializePromise: Promise<void> | null = null;\n\n constructor(options: BackfillTrackerOptions) {\n this.db = options.db;\n }\n\n /**\n * Mark a transaction handle as inheriting a table initialized durably by its\n * root adapter. Framework transaction coordinators use this after root DDL\n * commits so transaction-local trackers can skip redundant `CREATE TABLE`.\n */\n static inheritInitialization(\n db: DatabaseInterface,\n rootDb: DatabaseInterface,\n ): void {\n inheritedInitializationByHandle.set(\n db as object,\n databaseTargetIdentity(rootDb),\n );\n }\n\n /**\n * Suppress lazy DDL on a caller-owned transaction.\n *\n * Marker reads may proceed when the table already exists. A missing table\n * raises `BackfillTableUnavailableError` so the caller can fail closed or\n * retry with a root adapter.\n */\n static requireExistingTable(db: DatabaseInterface): void {\n inheritedInitializationByHandle.set(\n db as object,\n databaseTargetIdentity(db),\n );\n }\n\n /** Forget a root initialization after a schema lifecycle reset. */\n static invalidateInitialization(db: DatabaseInterface): void {\n deleteSharedInitialization(databaseTargetIdentity(db));\n }\n\n /**\n * Create the `_smrt_backfills` table if it doesn't already exist.\n * Safe to call repeatedly and concurrently.\n */\n async initialize(): Promise<void> {\n if (inheritedInitializationByHandle.has(this.db as object)) {\n return;\n }\n\n const target = databaseTargetIdentity(this.db);\n const existing = getSharedInitialization(target);\n if (existing) {\n await existing;\n if (!isRootDatabase(this.db)) return;\n try {\n await this.db.query('SELECT 1 FROM _smrt_backfills LIMIT 1');\n return;\n } catch (error) {\n if (!isMissingBackfillTableError(error)) throw error;\n if (getSharedInitialization(target) === existing) {\n deleteSharedInitialization(target);\n }\n return this.initialize();\n }\n }\n\n if (isRootDatabase(this.db)) {\n const initialization = this.db\n .query(CREATE_SMRT_BACKFILLS_TABLE)\n .then(() => undefined)\n .catch((error) => {\n if (getSharedInitialization(target) === initialization) {\n deleteSharedInitialization(target);\n }\n throw error;\n });\n setSharedInitialization(target, initialization);\n return initialization;\n }\n\n if (!this.initializePromise) {\n this.initializePromise = this.db\n .query(CREATE_SMRT_BACKFILLS_TABLE)\n .then(() => undefined)\n .catch((error) => {\n this.initializePromise = null;\n throw error;\n });\n }\n return this.initializePromise;\n }\n\n /** Has the backfill named `name` already run? */\n async isApplied(name: string): Promise<boolean> {\n const result = await this.queryBackfillTable(\n 'SELECT 1 FROM _smrt_backfills WHERE name = ? LIMIT 1',\n name,\n );\n return result.rows.length > 0;\n }\n\n /**\n * Record that the backfill named `name` ran. Uses INSERT ... ON CONFLICT\n * so callers don't need to check `isApplied` first if they don't care\n * about distinguishing \"already applied\" from \"just applied\".\n */\n async recordApplied(\n name: string,\n options: { description?: string; packageName?: string } = {},\n ): Promise<void> {\n await this.queryBackfillTable(\n `INSERT INTO _smrt_backfills (name, description, package_name)\n VALUES (?, ?, ?)\n ON CONFLICT (name) DO NOTHING`,\n name,\n options.description ?? null,\n options.packageName ?? null,\n );\n }\n\n /**\n * Convenience wrapper: if `name` hasn't been applied, run `fn` and\n * record the result. Returns `{ ran, result }`:\n * - `{ ran: true, result: T }` — backfill was pending and `fn` ran\n * - `{ ran: false, result: null }` — backfill was already applied\n *\n * The discriminator avoids the `T | null` ambiguity an earlier shape had\n * (a backfill that legitimately returns `null` was indistinguishable\n * from \"already applied\"). Callers check `ran` to decide whether `fn`\n * executed; `result` carries whatever `fn` returned.\n *\n * This is the common pattern an app's migrate orchestration follows:\n * a list of `(name, runner)` pairs, each guarded by isApplied/run/record.\n *\n * **Single-runner only.** This is a check-then-act pattern with no\n * locking: two processes that call `runIfPending` for the same `name`\n * concurrently can both observe `isApplied === false`, both execute\n * `fn`, and only then race to `recordApplied`. The `ON CONFLICT DO\n * NOTHING` in `recordApplied` keeps the table clean but does NOT prevent\n * `fn` from running twice. Acceptable when backfills are run from a\n * single deploy-time migration step (the common case); if you need to\n * fan out backfills across multiple workers, wrap the call in your own\n * mutex (e.g. a Postgres advisory lock keyed by backfill name).\n *\n * **`fn` MUST be idempotent.** The order is `fn` → `recordApplied`, with\n * no surrounding transaction. If `fn` succeeds but `recordApplied`\n * throws (DB connection drops, transient error), the work has happened\n * but is not recorded — the next call sees `isApplied === false` and\n * re-runs `fn`. For non-idempotent work (slug rewrites that consume\n * source rows, one-shot lookups), wrap the entire call in a transaction\n * yourself or structure `fn` so a second execution is a no-op.\n */\n async runIfPending<T>(\n name: string,\n fn: () => Promise<T>,\n options: { description?: string; packageName?: string } = {},\n ): Promise<{ ran: false; result: null } | { ran: true; result: T }> {\n if (await this.isApplied(name)) return { ran: false, result: null };\n const result = await fn();\n await this.recordApplied(name, options);\n return { ran: true, result };\n }\n\n /** List every recorded backfill, oldest first. */\n async listApplied(): Promise<BackfillRecord[]> {\n const result = await this.queryBackfillTable(\n 'SELECT name, applied_at, description, package_name FROM _smrt_backfills ORDER BY applied_at ASC, name ASC',\n );\n return result.rows.map((row) => ({\n name: String(row.name ?? ''),\n appliedAt: normalizeAppliedAt(row.applied_at),\n description: row.description == null ? null : String(row.description),\n packageName: row.package_name == null ? null : String(row.package_name),\n }));\n }\n\n private async queryBackfillTable(sql: string, ...params: unknown[]) {\n await this.initialize();\n try {\n return await this.db.query(sql, ...params);\n } catch (error) {\n if (!isMissingBackfillTableError(error)) throw error;\n const inheritedRoot = inheritedInitializationByHandle.get(\n this.db as object,\n );\n this.resetInitialization(inheritedRoot);\n if (inheritedRoot) throw new BackfillTableUnavailableError(error);\n await this.initialize();\n return this.db.query(sql, ...params);\n }\n }\n\n private resetInitialization(inheritedRoot?: object): void {\n this.initializePromise = null;\n inheritedInitializationByHandle.delete(this.db as object);\n deleteSharedInitialization(databaseTargetIdentity(this.db));\n if (inheritedRoot) deleteSharedInitialization(inheritedRoot);\n }\n}\n\nfunction databaseTargetIdentity(db: DatabaseInterface): object {\n const client = db.client;\n return client !== null &&\n (typeof client === 'object' || typeof client === 'function')\n ? client\n : db;\n}\n\nfunction isRootDatabase(db: DatabaseInterface): boolean {\n return typeof db.beginTransaction === 'function';\n}\n\nfunction getSharedInitialization(target: object): Promise<void> | undefined {\n return initializationByObject.get(target);\n}\n\nfunction setSharedInitialization(\n target: object,\n initialization: Promise<void>,\n): void {\n initializationByObject.set(target, initialization);\n}\n\nfunction deleteSharedInitialization(target: object): void {\n initializationByObject.delete(target);\n}\n\nfunction isMissingBackfillTableError(error: unknown): boolean {\n const pending: unknown[] = [error];\n const seen = new Set<unknown>();\n const missingPattern =\n /(?:no such table|does not exist|doesn't exist|unknown table|not found)/iu;\n while (pending.length > 0) {\n const current = pending.pop();\n if (typeof current === 'string') {\n if (current.includes('_smrt_backfills') && missingPattern.test(current)) {\n return true;\n }\n continue;\n }\n if (!current || typeof current !== 'object' || seen.has(current)) continue;\n seen.add(current);\n if (\n current instanceof Error &&\n current.message.includes('_smrt_backfills') &&\n missingPattern.test(current.message)\n ) {\n return true;\n }\n const details = current as {\n cause?: unknown;\n context?: unknown;\n details?: unknown;\n originalError?: unknown;\n };\n pending.push(\n details.cause,\n details.context,\n details.details,\n details.originalError,\n );\n if (current instanceof AggregateError) pending.push(...current.errors);\n }\n return false;\n}\n\n/**\n * Convert a raw `applied_at` cell into an ISO 8601 string.\n *\n * Different DB drivers return this column in different shapes:\n * - Postgres `node-pg` returns a JS `Date` directly.\n * - SQLite drivers typically return the `CURRENT_TIMESTAMP` literal\n * `'YYYY-MM-DD HH:MM:SS'` (no `T`, no timezone). V8's `Date`\n * constructor parses this as local time, but that's\n * implementation-defined and other runtimes may return Invalid Date.\n * - A corrupted row could return any string.\n *\n * If the value parses to a valid Date we return `toISOString()`. If it\n * doesn't (or the column is null/empty), we fall back to the raw string —\n * better to surface a non-ISO timestamp than crash `listApplied()` with a\n * `RangeError` and take down the caller.\n */\nfunction normalizeAppliedAt(value: unknown): string {\n if (value == null || value === '') return '';\n const raw = String(value);\n const parsed = new Date(raw);\n if (Number.isNaN(parsed.getTime())) {\n return raw;\n }\n return parsed.toISOString();\n}\n"],"mappings":"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"}
@@ -8,7 +8,7 @@
8
8
  * - Drift detection
9
9
  * - Rollback support
10
10
  */
11
- export { type BackfillRecord, BackfillTracker, type BackfillTrackerOptions, } from './backfill-tracker.js';
11
+ export { type BackfillRecord, BackfillTableUnavailableError, BackfillTracker, type BackfillTrackerOptions, } from './backfill-tracker.js';
12
12
  export { computeChecksum, normalizeSQL, shortChecksum, verifyChecksum, } from './checksum.js';
13
13
  export type { DiffOptions } from './differ.js';
14
14
  export { generateSchemaDiff, getSQLFromDiff, hasActionableChanges, SchemaComparer, } from './differ.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EACL,KAAK,cAAc,EACnB,eAAe,EACf,KAAK,sBAAsB,GAC5B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,eAAe,EACf,YAAY,EACZ,aAAa,EACb,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,oBAAoB,EACpB,cAAc,GACf,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,kBAAkB,EAClB,wBAAwB,GACzB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,yBAAyB,EACzB,yBAAyB,EACzB,0BAA0B,EAC1B,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,0BAA0B,EAC1B,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,kBAAkB,EAClB,KAAK,6BAA6B,GACnC,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAGxE,YAAY,EACV,sBAAsB,EACtB,wBAAwB,EACxB,UAAU,EACV,cAAc,EACd,iBAAiB,EAEjB,aAAa,EACb,mBAAmB,EACnB,sBAAsB,EACtB,WAAW,EACX,cAAc,EACd,eAAe,EACf,SAAS,EACT,mBAAmB,EACnB,aAAa,EACb,eAAe,EAEf,eAAe,EACf,gBAAgB,EAEhB,uBAAuB,EACvB,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,YAAY,EACZ,UAAU,EACV,qBAAqB,EACrB,yBAAyB,EACzB,eAAe,GAChB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EACL,KAAK,cAAc,EACnB,6BAA6B,EAC7B,eAAe,EACf,KAAK,sBAAsB,GAC5B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,eAAe,EACf,YAAY,EACZ,aAAa,EACb,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,oBAAoB,EACpB,cAAc,GACf,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,kBAAkB,EAClB,wBAAwB,GACzB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,yBAAyB,EACzB,yBAAyB,EACzB,0BAA0B,EAC1B,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,0BAA0B,EAC1B,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,EAC7B,kBAAkB,EAClB,KAAK,6BAA6B,GACnC,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAGxE,YAAY,EACV,sBAAsB,EACtB,wBAAwB,EACxB,UAAU,EACV,cAAc,EACd,iBAAiB,EAEjB,aAAa,EACb,mBAAmB,EACnB,sBAAsB,EACtB,WAAW,EACX,cAAc,EACd,eAAe,EACf,SAAS,EACT,mBAAmB,EACnB,aAAa,EACb,eAAe,EAEf,eAAe,EACf,gBAAgB,EAEhB,uBAAuB,EACvB,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,YAAY,EACZ,UAAU,EACV,qBAAqB,EACrB,yBAAyB,EACzB,eAAe,GAChB,MAAM,YAAY,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import { SchemaComparer, generateSchemaDiff, getSQLFromDiff, hasActionableChanges } from "./differ.js";
2
- import { BackfillTracker } from "./backfill-tracker.js";
2
+ import { BackfillTableUnavailableError, BackfillTracker } from "./backfill-tracker.js";
3
3
  import { computeChecksum, normalizeSQL, shortChecksum, verifyChecksum } from "./checksum.js";
4
4
  import { MigrationGenerator, createMigrationDefinition, generateMigrationSequence, generateMigrationTimestamp } from "./generator.js";
5
5
  import { MigrationTracker, planPostgresStatements } from "./tracker.js";
6
6
  import { getPendingSchemaStatements, migrateSmrtSchemas } from "./orchestrate.js";
7
- export { BackfillTracker, MigrationGenerator, MigrationTracker, SchemaComparer, computeChecksum, createMigrationDefinition, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, migrateSmrtSchemas, normalizeSQL, planPostgresStatements, shortChecksum, verifyChecksum };
7
+ export { BackfillTableUnavailableError, BackfillTracker, MigrationGenerator, MigrationTracker, SchemaComparer, computeChecksum, createMigrationDefinition, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, migrateSmrtSchemas, normalizeSQL, planPostgresStatements, shortChecksum, verifyChecksum };
@@ -1,8 +1,8 @@
1
1
  import { SchemaComparer, generateSchemaDiff, getSQLFromDiff, hasActionableChanges } from "./migrations/differ.js";
2
- import { BackfillTracker } from "./migrations/backfill-tracker.js";
2
+ import { BackfillTableUnavailableError, BackfillTracker } from "./migrations/backfill-tracker.js";
3
3
  import { computeChecksum, normalizeSQL, shortChecksum, verifyChecksum } from "./migrations/checksum.js";
4
4
  import { MigrationGenerator, createMigrationDefinition, generateMigrationSequence, generateMigrationTimestamp } from "./migrations/generator.js";
5
5
  import { MigrationTracker, planPostgresStatements } from "./migrations/tracker.js";
6
6
  import { getPendingSchemaStatements, migrateSmrtSchemas } from "./migrations/orchestrate.js";
7
7
  import "./migrations/index.js";
8
- export { BackfillTracker, MigrationGenerator, MigrationTracker, SchemaComparer, computeChecksum, createMigrationDefinition, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, migrateSmrtSchemas, normalizeSQL, planPostgresStatements, shortChecksum, verifyChecksum };
8
+ export { BackfillTableUnavailableError, BackfillTracker, MigrationGenerator, MigrationTracker, SchemaComparer, computeChecksum, createMigrationDefinition, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, migrateSmrtSchemas, normalizeSQL, planPostgresStatements, shortChecksum, verifyChecksum };
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-07-14T00:01:24.275Z",
3
+ "generatedAt": "2026-07-14T05:49:34.832Z",
4
4
  "packageName": "@happyvertical/smrt-core",
5
- "packageVersion": "0.39.13",
5
+ "packageVersion": "0.39.15",
6
6
  "sourceManifestPath": "dist/manifest.json",
7
7
  "agentDocPath": "AGENTS.md",
8
8
  "sourceHashes": {
9
- "manifest": "c0cc26e4e510fe4051922397da89a55ee6fe12d7e3650436f51dcff077e936d2",
10
- "packageJson": "5f175fea112e511d919c25da62283998c8d8f26a085b543b5355d78c585329bc",
9
+ "manifest": "73a673a7b0f68c7718f8ac05cb8bd4f0304bdd3cd27c6ab4455c1f54a9e7cc07",
10
+ "packageJson": "2747337317f2b69610177f4796f7ff90342434e5e54da9953b88b98e64ec27b4",
11
11
  "agents": "46a9a399123c7c5700076fa94b650df11e28123af899806243bc2fcb6e7295e4"
12
12
  },
13
13
  "exports": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-core",
3
- "version": "0.39.13",
3
+ "version": "0.39.15",
4
4
  "description": "Core AI agent framework with standardized collections, object-relational mapping, and code generators",
5
5
  "author": "HappyVertical",
6
6
  "type": "module",
@@ -159,9 +159,9 @@
159
159
  "tsx": "^4.23.0",
160
160
  "typescript": "^5.9.3",
161
161
  "yaml": "^2.9.0",
162
- "@happyvertical/smrt-config": "0.39.13",
163
- "@happyvertical/smrt-scanner": "0.39.13",
164
- "@happyvertical/smrt-types": "0.39.13"
162
+ "@happyvertical/smrt-config": "0.39.15",
163
+ "@happyvertical/smrt-scanner": "0.39.15",
164
+ "@happyvertical/smrt-types": "0.39.15"
165
165
  },
166
166
  "peerDependencies": {
167
167
  "@huggingface/transformers": ">=3.0.0 <4.0.0",