@happyvertical/smrt-core 0.55.2 → 0.55.4

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.
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-core",
5
- "packageVersion": "0.55.2",
5
+ "packageVersion": "0.55.4",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-core:SmrtClass": {
8
8
  "name": "smrtclass",
@@ -16,6 +16,7 @@ export { type DropFrameworkBaseTablesOptions, type DropFrameworkBaseTablesResult
16
16
  export type { GeneratedMigration, GenerateMigrationOptions, } from './generator.js';
17
17
  export { createMigrationDefinition, generateMigrationSequence, generateMigrationTimestamp, MigrationGenerator, } from './generator.js';
18
18
  export { buildIntegerWidthStatements, buildIntegerWidthTableStatements, collectIntegerWidthTargets, type IntegerWidthColumnReport, type IntegerWidthColumnState, type IntegerWidthOptions, type IntegerWidthPreflightResult, type IntegerWidthTableReport, type IntegerWidthTarget, type IntegerWidthWidenOptions, type IntegerWidthWidenResult, preflightIntegerWidthWidening, widenIntegerColumnsToBigInt, } from './integer-width.js';
19
+ export { type AcquireMigrationLockOptions, acquireMigrationLock, DEFAULT_MIGRATION_LOCK_WAIT_TIMEOUT_MS, MIGRATION_ADVISORY_LOCK_KEYS, type MigrationLock, MigrationLockLostError, MigrationLockTimeoutError, MigrationLockUnsupportedError, } from './migration-lock.js';
19
20
  export { buildMinorUnitsStatements, type MinorUnitsColumnReport, type MinorUnitsColumnState, type MinorUnitsOptions, MinorUnitsPreflightError, type MinorUnitsPreflightResult, type MinorUnitsProblemRow, type MinorUnitsRescaleOptions, type MinorUnitsRescaleResult, type MoneyColumnTarget, preflightMinorUnitsRescale, rescaleMoneyColumnsToMinorUnits, } from './minor-units.js';
20
21
  export { collectNullEqualIndexTargets, migrateNullEqualIndexes, type NullEqualIndexOptions, type NullEqualIndexPreflight, type NullEqualIndexReport, type NullEqualIndexTarget, nullEqualDuplicateDetector, nullEqualIndexStatements, preflightNullEqualIndexes, } from './null-equal-indexes.js';
21
22
  export { getPendingSchemaStatements, type MigrateSmrtSchemasOptions, type MigrateSmrtSchemasResult, migrateSmrtSchemas, type PendingSchemaStatementsResult, } from './orchestrate.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"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"}
@@ -6,9 +6,10 @@ import { buildIntegerWidthStatements, buildIntegerWidthTableStatements, collectI
6
6
  import { computeChecksum, normalizeSQL, shortChecksum, verifyChecksum } from "./checksum.js";
7
7
  import { FRAMEWORK_BASE_TABLE_NAMES, dropFrameworkBaseTables, planFrameworkBaseTableDrop } from "./framework-base-tables.js";
8
8
  import { MigrationGenerator, createMigrationDefinition, generateMigrationSequence, generateMigrationTimestamp } from "./generator.js";
9
+ import { DEFAULT_MIGRATION_LOCK_WAIT_TIMEOUT_MS, MIGRATION_ADVISORY_LOCK_KEYS, MigrationLockLostError, MigrationLockTimeoutError, MigrationLockUnsupportedError, acquireMigrationLock } from "./migration-lock.js";
9
10
  import { MinorUnitsPreflightError, buildMinorUnitsStatements, preflightMinorUnitsRescale, rescaleMoneyColumnsToMinorUnits } from "./minor-units.js";
10
11
  import { collectNullEqualIndexTargets, migrateNullEqualIndexes, nullEqualDuplicateDetector, nullEqualIndexStatements, preflightNullEqualIndexes } from "./null-equal-indexes.js";
11
12
  import { MigrationTracker, buildConcurrentIndexPlan, extractCreatedIndexName, planPostgresStatements } from "./tracker.js";
12
13
  import { getPendingSchemaStatements, migrateSmrtSchemas } from "./orchestrate.js";
13
14
  import { LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX, backfillLegacyQualifiedNames, collectLegacyQualifiedNameTargets, countLegacyQualifiedNameReferences, legacyQualifiedNamesBackfillName } from "./qualified-name-aliases.js";
14
- export { BackfillTableUnavailableError, BackfillTracker, FRAMEWORK_BASE_TABLE_NAMES, LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX, MigrationGenerator, MigrationTracker, MinorUnitsPreflightError, REQUIRED_COLUMN_NOT_ADDED, SQLITE_REBUILD_TABLE_PREFIX, SchemaComparer, backfillLegacyQualifiedNames, buildConcurrentIndexPlan, buildIntegerWidthStatements, buildIntegerWidthTableStatements, buildMinorUnitsStatements, buildSqliteRebuildStatements, canonicalizeDefault, collectIntegerWidthTargets, collectLegacyQualifiedNameTargets, collectNullEqualIndexTargets, computeChecksum, countLegacyQualifiedNameReferences, createMigrationDefinition, dropFrameworkBaseTables, extractCreatedIndexName, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, isSqliteRebuildPlaceholder, legacyQualifiedNamesBackfillName, migrateNullEqualIndexes, migrateSmrtSchemas, normalizeSQL, nullEqualDuplicateDetector, nullEqualIndexStatements, parsePostgresTimeoutMs, planFrameworkBaseTableDrop, planPostgresStatements, planSqliteTableRebuilds, preflightIntegerWidthWidening, preflightMinorUnitsRescale, preflightNullEqualIndexes, rescaleMoneyColumnsToMinorUnits, rewriteSqliteCreateTable, shortChecksum, sqliteRebuildPlaceholderSql, uniqueColumnIndexName, verifyChecksum, widenIntegerColumnsToBigInt };
15
+ export { BackfillTableUnavailableError, BackfillTracker, DEFAULT_MIGRATION_LOCK_WAIT_TIMEOUT_MS, FRAMEWORK_BASE_TABLE_NAMES, LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX, MIGRATION_ADVISORY_LOCK_KEYS, MigrationGenerator, MigrationLockLostError, MigrationLockTimeoutError, MigrationLockUnsupportedError, MigrationTracker, MinorUnitsPreflightError, REQUIRED_COLUMN_NOT_ADDED, SQLITE_REBUILD_TABLE_PREFIX, SchemaComparer, acquireMigrationLock, backfillLegacyQualifiedNames, buildConcurrentIndexPlan, buildIntegerWidthStatements, buildIntegerWidthTableStatements, buildMinorUnitsStatements, buildSqliteRebuildStatements, canonicalizeDefault, collectIntegerWidthTargets, collectLegacyQualifiedNameTargets, collectNullEqualIndexTargets, computeChecksum, countLegacyQualifiedNameReferences, createMigrationDefinition, dropFrameworkBaseTables, extractCreatedIndexName, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, isSqliteRebuildPlaceholder, legacyQualifiedNamesBackfillName, migrateNullEqualIndexes, migrateSmrtSchemas, normalizeSQL, nullEqualDuplicateDetector, nullEqualIndexStatements, parsePostgresTimeoutMs, planFrameworkBaseTableDrop, planPostgresStatements, planSqliteTableRebuilds, preflightIntegerWidthWidening, preflightMinorUnitsRescale, preflightNullEqualIndexes, rescaleMoneyColumnsToMinorUnits, rewriteSqliteCreateTable, shortChecksum, sqliteRebuildPlaceholderSql, uniqueColumnIndexName, verifyChecksum, widenIntegerColumnsToBigInt };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Cross-process mutual exclusion for schema migration runs (issue #3634).
3
+ *
4
+ * Two processes that run `smrt db:migrate` against one PostgreSQL database at
5
+ * the same time — two workloads gating their startup on the migration, a
6
+ * rolling restart, a break-glass Job beside running pods — both compare the
7
+ * live schema, both plan the same batch, and the second one fails part way
8
+ * with `42701 column ... already exists` or `42P07 relation ... already
9
+ * exists`. The tracker's per-migration `running` rows cannot prevent this:
10
+ * they are written after the diff, so both runners have already planned.
11
+ *
12
+ * The lock below covers the whole diff → plan → apply window. It is a
13
+ * session-level advisory lock on a pinned connection, so it is held across
14
+ * the run's many statements and transactions, and PostgreSQL frees it the
15
+ * moment that connection ends: a migrator that crashes or is killed never
16
+ * leaves the lock behind for the next one. A waiting runner acquires it after
17
+ * the holder finishes, compares a schema that is already current, and has
18
+ * nothing to apply.
19
+ *
20
+ * SQLite and DuckDB are single-writer files whose engines already serialize
21
+ * schema changes, so the lock is a no-op there.
22
+ */
23
+ import type { DatabaseInterface } from './types.js';
24
+ /**
25
+ * The two-key advisory lock id every `db:migrate` run takes. The `smrt`
26
+ * class id matches the framework's other advisory locks (system-table
27
+ * bootstrap uses `hashtext('smrt'), hashtext('system-tables')`), so the
28
+ * object id alone separates them. Changing either key would let a run of the
29
+ * new release and a run of an older release migrate concurrently.
30
+ */
31
+ export declare const MIGRATION_ADVISORY_LOCK_KEYS = "hashtext('smrt'), hashtext('db:migrate')";
32
+ /** Default bound on waiting for another run to finish: 15 minutes. */
33
+ export declare const DEFAULT_MIGRATION_LOCK_WAIT_TIMEOUT_MS: number;
34
+ /** Thrown when another run still holds the migration lock at the deadline. */
35
+ export declare class MigrationLockTimeoutError extends Error {
36
+ readonly waitedMs: number;
37
+ constructor(waitedMs: number);
38
+ }
39
+ /** Thrown when a PostgreSQL adapter cannot pin the connection the lock needs. */
40
+ /**
41
+ * Thrown when the lock's pinned connection has died mid-run. PostgreSQL frees
42
+ * a session lock when its backend ends, so another migrator may already be
43
+ * running; the run must stop rather than keep applying unserialized.
44
+ */
45
+ export declare class MigrationLockLostError extends Error {
46
+ constructor(cause?: unknown);
47
+ }
48
+ export declare class MigrationLockUnsupportedError extends Error {
49
+ constructor();
50
+ }
51
+ export interface AcquireMigrationLockOptions {
52
+ /**
53
+ * How long to wait for another run to release the lock, in milliseconds.
54
+ * `0` waits indefinitely, matching PostgreSQL's meaning of a zero timeout.
55
+ * @default DEFAULT_MIGRATION_LOCK_WAIT_TIMEOUT_MS
56
+ */
57
+ timeoutMs?: number;
58
+ /** Delay between attempts while another run holds the lock. @default 1000 */
59
+ pollIntervalMs?: number;
60
+ /** Engine override; otherwise detected from the database URL. */
61
+ engineHint?: string;
62
+ /** Called once, when the first attempt finds the lock held. */
63
+ onWait?: () => void;
64
+ /** Injectable clock and sleep for tests. */
65
+ now?: () => number;
66
+ sleep?: (ms: number) => Promise<void>;
67
+ }
68
+ export interface MigrationLock {
69
+ /** False when the engine needs no lock (SQLite, DuckDB). */
70
+ readonly held: boolean;
71
+ /** Milliseconds spent waiting for another run; 0 when uncontended. */
72
+ readonly waitedMs: number;
73
+ /** Release the lock and its pinned connection. Idempotent. */
74
+ /**
75
+ * Confirm on the pinned connection that this run still holds the lock, and
76
+ * throw {@link MigrationLockLostError} if it does not. Call it before each
77
+ * phase that writes schema. A no-op when the engine needs no lock.
78
+ */
79
+ assertHeld(): Promise<void>;
80
+ release(): Promise<void>;
81
+ }
82
+ /**
83
+ * Take the migration lock, waiting for a concurrent run to finish.
84
+ *
85
+ * Call it after connecting and before anything that reads or writes schema
86
+ * (system-table bootstrap, schema comparison, apply), and release it in a
87
+ * `finally` after the run. On PostgreSQL the lock lives on its own pinned
88
+ * connection, separate from the pool the migration itself uses.
89
+ */
90
+ export declare function acquireMigrationLock(db: DatabaseInterface, options?: AcquireMigrationLockOptions): Promise<MigrationLock>;
91
+ //# sourceMappingURL=migration-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration-lock.d.ts","sourceRoot":"","sources":["../../src/migrations/migration-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,6CACG,CAAC;AAE7C,sEAAsE;AACtE,eAAO,MAAM,sCAAsC,QAAiB,CAAC;AAIrE,8EAA8E;AAC9E,qBAAa,yBAA0B,SAAQ,KAAK;aACtB,QAAQ,EAAE,MAAM;gBAAhB,QAAQ,EAAE,MAAM;CAQ7C;AAED,iFAAiF;AACjF;;;;GAIG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,KAAK,CAAC,EAAE,OAAO;CAO5B;AAED,qBAAa,6BAA8B,SAAQ,KAAK;;CAOvD;AAED,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,4CAA4C;IAC5C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8DAA8D;IAC9D;;;;OAIG;IACH,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,iBAAiB,EACrB,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,aAAa,CAAC,CA6ExB"}
@@ -0,0 +1,158 @@
1
+ import { detectEngine } from "../schema/ddl/index.js";
2
+ //#region src/migrations/migration-lock.ts
3
+ /**
4
+ * Cross-process mutual exclusion for schema migration runs (issue #3634).
5
+ *
6
+ * Two processes that run `smrt db:migrate` against one PostgreSQL database at
7
+ * the same time — two workloads gating their startup on the migration, a
8
+ * rolling restart, a break-glass Job beside running pods — both compare the
9
+ * live schema, both plan the same batch, and the second one fails part way
10
+ * with `42701 column ... already exists` or `42P07 relation ... already
11
+ * exists`. The tracker's per-migration `running` rows cannot prevent this:
12
+ * they are written after the diff, so both runners have already planned.
13
+ *
14
+ * The lock below covers the whole diff → plan → apply window. It is a
15
+ * session-level advisory lock on a pinned connection, so it is held across
16
+ * the run's many statements and transactions, and PostgreSQL frees it the
17
+ * moment that connection ends: a migrator that crashes or is killed never
18
+ * leaves the lock behind for the next one. A waiting runner acquires it after
19
+ * the holder finishes, compares a schema that is already current, and has
20
+ * nothing to apply.
21
+ *
22
+ * SQLite and DuckDB are single-writer files whose engines already serialize
23
+ * schema changes, so the lock is a no-op there.
24
+ */
25
+ /**
26
+ * The two-key advisory lock id every `db:migrate` run takes. The `smrt`
27
+ * class id matches the framework's other advisory locks (system-table
28
+ * bootstrap uses `hashtext('smrt'), hashtext('system-tables')`), so the
29
+ * object id alone separates them. Changing either key would let a run of the
30
+ * new release and a run of an older release migrate concurrently.
31
+ */
32
+ var MIGRATION_ADVISORY_LOCK_KEYS = "hashtext('smrt'), hashtext('db:migrate')";
33
+ /** Default bound on waiting for another run to finish: 15 minutes. */
34
+ var DEFAULT_MIGRATION_LOCK_WAIT_TIMEOUT_MS = 900 * 1e3;
35
+ var DEFAULT_POLL_INTERVAL_MS = 1e3;
36
+ /** Thrown when another run still holds the migration lock at the deadline. */
37
+ var MigrationLockTimeoutError = class extends Error {
38
+ waitedMs;
39
+ constructor(waitedMs) {
40
+ super(`Another db:migrate run held the migration lock for ${Math.round(waitedMs / 1e3)}s; giving up. The lock is released when that run's database connection ends. Raise migrations.postgres.migrationLockTimeout if a migration legitimately runs longer.`);
41
+ this.waitedMs = waitedMs;
42
+ this.name = "MigrationLockTimeoutError";
43
+ }
44
+ };
45
+ /** Thrown when a PostgreSQL adapter cannot pin the connection the lock needs. */
46
+ /**
47
+ * Thrown when the lock's pinned connection has died mid-run. PostgreSQL frees
48
+ * a session lock when its backend ends, so another migrator may already be
49
+ * running; the run must stop rather than keep applying unserialized.
50
+ */
51
+ var MigrationLockLostError = class extends Error {
52
+ constructor(cause) {
53
+ super("The db:migrate migration lock was lost: its database connection ended mid-run, so another run may be migrating concurrently. Stopping; rerun db:migrate to converge.", cause === void 0 ? void 0 : { cause });
54
+ this.name = "MigrationLockLostError";
55
+ }
56
+ };
57
+ var MigrationLockUnsupportedError = class extends Error {
58
+ constructor() {
59
+ super("This PostgreSQL adapter does not expose acquireSession(), so db:migrate cannot hold the migration lock on one connection. Upgrade @happyvertical/sql.");
60
+ this.name = "MigrationLockUnsupportedError";
61
+ }
62
+ };
63
+ /**
64
+ * Take the migration lock, waiting for a concurrent run to finish.
65
+ *
66
+ * Call it after connecting and before anything that reads or writes schema
67
+ * (system-table bootstrap, schema comparison, apply), and release it in a
68
+ * `finally` after the run. On PostgreSQL the lock lives on its own pinned
69
+ * connection, separate from the pool the migration itself uses.
70
+ */
71
+ async function acquireMigrationLock(db, options = {}) {
72
+ if (resolveEngine(db, options.engineHint) !== "postgres") return {
73
+ held: false,
74
+ waitedMs: 0,
75
+ assertHeld: async () => {},
76
+ release: async () => {}
77
+ };
78
+ if (typeof db.acquireSession !== "function") throw new MigrationLockUnsupportedError();
79
+ const timeoutMs = options.timeoutMs ?? 9e5;
80
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
81
+ const now = options.now ?? Date.now;
82
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
83
+ const session = await db.acquireSession();
84
+ const startedAt = now();
85
+ let notifiedWait = false;
86
+ try {
87
+ await exemptFromIdleSessionTimeout(session);
88
+ while (true) {
89
+ if (isTrue((await session.query(`SELECT pg_try_advisory_lock(hashtext('smrt'), hashtext('db:migrate')) AS acquired`)).rows?.[0]?.acquired)) {
90
+ const waitedMs = notifiedWait ? now() - startedAt : 0;
91
+ let released = false;
92
+ return {
93
+ held: true,
94
+ waitedMs,
95
+ assertHeld: async () => {
96
+ if (released) throw new MigrationLockLostError();
97
+ let stillHeld;
98
+ try {
99
+ stillHeld = isTrue((await session.query(HELD_PROBE_SQL)).rows?.[0]?.held);
100
+ } catch (error) {
101
+ throw new MigrationLockLostError(error);
102
+ }
103
+ if (!stillHeld) throw new MigrationLockLostError();
104
+ },
105
+ release: async () => {
106
+ if (released) return;
107
+ released = true;
108
+ await session.release();
109
+ }
110
+ };
111
+ }
112
+ const waitedMs = now() - startedAt;
113
+ if (timeoutMs > 0 && waitedMs >= timeoutMs) throw new MigrationLockTimeoutError(waitedMs);
114
+ if (!notifiedWait) {
115
+ notifiedWait = true;
116
+ options.onWait?.();
117
+ }
118
+ await sleep(timeoutMs > 0 ? Math.min(pollIntervalMs, Math.max(1, timeoutMs - waitedMs)) : pollIntervalMs);
119
+ }
120
+ } catch (error) {
121
+ await session.release();
122
+ throw error;
123
+ }
124
+ }
125
+ /**
126
+ * Whether this backend still holds the db:migrate lock. Two-key advisory
127
+ * locks appear in `pg_locks` with `objsubid = 2`; `hashtext` returns int4, and
128
+ * the `::oid` cast maps a negative hash onto the same unsigned value the
129
+ * catalog stores.
130
+ */
131
+ var HELD_PROBE_SQL = `SELECT EXISTS (
132
+ SELECT 1 FROM pg_locks
133
+ WHERE locktype = 'advisory' AND granted AND objsubid = 2
134
+ AND pid = pg_backend_pid()
135
+ AND classid = (hashtext('smrt'))::oid
136
+ AND objid = (hashtext('db:migrate'))::oid
137
+ ) AS held`;
138
+ /**
139
+ * The lock session runs one statement and then sits idle for the whole
140
+ * migration, so a role- or database-level `idle_session_timeout` (PostgreSQL
141
+ * 14+) would reap it first and silently free the lock. Exempt this one
142
+ * session; older servers have no such setting.
143
+ */
144
+ async function exemptFromIdleSessionTimeout(session) {
145
+ const version = await session.query("SELECT current_setting('server_version_num')::int AS version");
146
+ if (Number(version.rows?.[0]?.version) >= 14e4) await session.query("SET idle_session_timeout = 0");
147
+ }
148
+ function resolveEngine(db, engineHint) {
149
+ const dbWithConfig = db;
150
+ return detectEngine(db.url || dbWithConfig.config?.url || "", engineHint);
151
+ }
152
+ function isTrue(value) {
153
+ return value === true || value === "t" || value === "true" || value === 1;
154
+ }
155
+ //#endregion
156
+ export { DEFAULT_MIGRATION_LOCK_WAIT_TIMEOUT_MS, MIGRATION_ADVISORY_LOCK_KEYS, MigrationLockLostError, MigrationLockTimeoutError, MigrationLockUnsupportedError, acquireMigrationLock };
157
+
158
+ //# sourceMappingURL=migration-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migration-lock.js","names":[],"sources":["../../src/migrations/migration-lock.ts"],"sourcesContent":["/**\n * Cross-process mutual exclusion for schema migration runs (issue #3634).\n *\n * Two processes that run `smrt db:migrate` against one PostgreSQL database at\n * the same time — two workloads gating their startup on the migration, a\n * rolling restart, a break-glass Job beside running pods — both compare the\n * live schema, both plan the same batch, and the second one fails part way\n * with `42701 column ... already exists` or `42P07 relation ... already\n * exists`. The tracker's per-migration `running` rows cannot prevent this:\n * they are written after the diff, so both runners have already planned.\n *\n * The lock below covers the whole diff → plan → apply window. It is a\n * session-level advisory lock on a pinned connection, so it is held across\n * the run's many statements and transactions, and PostgreSQL frees it the\n * moment that connection ends: a migrator that crashes or is killed never\n * leaves the lock behind for the next one. A waiting runner acquires it after\n * the holder finishes, compares a schema that is already current, and has\n * nothing to apply.\n *\n * SQLite and DuckDB are single-writer files whose engines already serialize\n * schema changes, so the lock is a no-op there.\n */\n\nimport { detectEngine } from '../schema/ddl/index.js';\nimport type { DatabaseEngine } from '../schema/ddl/types.js';\nimport type { DatabaseInterface } from './types.js';\n\n/**\n * The two-key advisory lock id every `db:migrate` run takes. The `smrt`\n * class id matches the framework's other advisory locks (system-table\n * bootstrap uses `hashtext('smrt'), hashtext('system-tables')`), so the\n * object id alone separates them. Changing either key would let a run of the\n * new release and a run of an older release migrate concurrently.\n */\nexport const MIGRATION_ADVISORY_LOCK_KEYS =\n \"hashtext('smrt'), hashtext('db:migrate')\";\n\n/** Default bound on waiting for another run to finish: 15 minutes. */\nexport const DEFAULT_MIGRATION_LOCK_WAIT_TIMEOUT_MS = 15 * 60 * 1000;\n\nconst DEFAULT_POLL_INTERVAL_MS = 1000;\n\n/** Thrown when another run still holds the migration lock at the deadline. */\nexport class MigrationLockTimeoutError extends Error {\n constructor(public readonly waitedMs: number) {\n super(\n `Another db:migrate run held the migration lock for ${Math.round(\n waitedMs / 1000,\n )}s; giving up. The lock is released when that run's database connection ends. Raise migrations.postgres.migrationLockTimeout if a migration legitimately runs longer.`,\n );\n this.name = 'MigrationLockTimeoutError';\n }\n}\n\n/** Thrown when a PostgreSQL adapter cannot pin the connection the lock needs. */\n/**\n * Thrown when the lock's pinned connection has died mid-run. PostgreSQL frees\n * a session lock when its backend ends, so another migrator may already be\n * running; the run must stop rather than keep applying unserialized.\n */\nexport class MigrationLockLostError extends Error {\n constructor(cause?: unknown) {\n super(\n 'The db:migrate migration lock was lost: its database connection ended mid-run, so another run may be migrating concurrently. Stopping; rerun db:migrate to converge.',\n cause === undefined ? undefined : { cause },\n );\n this.name = 'MigrationLockLostError';\n }\n}\n\nexport class MigrationLockUnsupportedError extends Error {\n constructor() {\n super(\n 'This PostgreSQL adapter does not expose acquireSession(), so db:migrate cannot hold the migration lock on one connection. Upgrade @happyvertical/sql.',\n );\n this.name = 'MigrationLockUnsupportedError';\n }\n}\n\nexport interface AcquireMigrationLockOptions {\n /**\n * How long to wait for another run to release the lock, in milliseconds.\n * `0` waits indefinitely, matching PostgreSQL's meaning of a zero timeout.\n * @default DEFAULT_MIGRATION_LOCK_WAIT_TIMEOUT_MS\n */\n timeoutMs?: number;\n /** Delay between attempts while another run holds the lock. @default 1000 */\n pollIntervalMs?: number;\n /** Engine override; otherwise detected from the database URL. */\n engineHint?: string;\n /** Called once, when the first attempt finds the lock held. */\n onWait?: () => void;\n /** Injectable clock and sleep for tests. */\n now?: () => number;\n sleep?: (ms: number) => Promise<void>;\n}\n\nexport interface MigrationLock {\n /** False when the engine needs no lock (SQLite, DuckDB). */\n readonly held: boolean;\n /** Milliseconds spent waiting for another run; 0 when uncontended. */\n readonly waitedMs: number;\n /** Release the lock and its pinned connection. Idempotent. */\n /**\n * Confirm on the pinned connection that this run still holds the lock, and\n * throw {@link MigrationLockLostError} if it does not. Call it before each\n * phase that writes schema. A no-op when the engine needs no lock.\n */\n assertHeld(): Promise<void>;\n release(): Promise<void>;\n}\n\n/**\n * Take the migration lock, waiting for a concurrent run to finish.\n *\n * Call it after connecting and before anything that reads or writes schema\n * (system-table bootstrap, schema comparison, apply), and release it in a\n * `finally` after the run. On PostgreSQL the lock lives on its own pinned\n * connection, separate from the pool the migration itself uses.\n */\nexport async function acquireMigrationLock(\n db: DatabaseInterface,\n options: AcquireMigrationLockOptions = {},\n): Promise<MigrationLock> {\n const engine = resolveEngine(db, options.engineHint);\n if (engine !== 'postgres') {\n return {\n held: false,\n waitedMs: 0,\n assertHeld: async () => {},\n release: async () => {},\n };\n }\n if (typeof db.acquireSession !== 'function') {\n throw new MigrationLockUnsupportedError();\n }\n\n const timeoutMs = options.timeoutMs ?? DEFAULT_MIGRATION_LOCK_WAIT_TIMEOUT_MS;\n const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;\n const now = options.now ?? Date.now;\n const sleep =\n options.sleep ??\n ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));\n\n const session = await db.acquireSession();\n const startedAt = now();\n let notifiedWait = false;\n\n try {\n await exemptFromIdleSessionTimeout(session);\n while (true) {\n const result = await session.query(\n `SELECT pg_try_advisory_lock(${MIGRATION_ADVISORY_LOCK_KEYS}) AS acquired`,\n );\n if (isTrue(result.rows?.[0]?.acquired)) {\n const waitedMs = notifiedWait ? now() - startedAt : 0;\n let released = false;\n return {\n held: true,\n waitedMs,\n assertHeld: async () => {\n if (released) throw new MigrationLockLostError();\n let stillHeld: boolean;\n try {\n const probe = await session.query(HELD_PROBE_SQL);\n stillHeld = isTrue(probe.rows?.[0]?.held);\n } catch (error) {\n throw new MigrationLockLostError(error);\n }\n if (!stillHeld) throw new MigrationLockLostError();\n },\n release: async () => {\n if (released) return;\n released = true;\n // The adapter's release() runs pg_advisory_unlock_all() and then\n // destroys the connection, which frees the lock even if the\n // unlock could not be sent.\n await session.release();\n },\n };\n }\n\n const waitedMs = now() - startedAt;\n if (timeoutMs > 0 && waitedMs >= timeoutMs) {\n throw new MigrationLockTimeoutError(waitedMs);\n }\n if (!notifiedWait) {\n notifiedWait = true;\n options.onWait?.();\n }\n await sleep(\n timeoutMs > 0\n ? Math.min(pollIntervalMs, Math.max(1, timeoutMs - waitedMs))\n : pollIntervalMs,\n );\n }\n } catch (error) {\n await session.release();\n throw error;\n }\n}\n\n/**\n * Whether this backend still holds the db:migrate lock. Two-key advisory\n * locks appear in `pg_locks` with `objsubid = 2`; `hashtext` returns int4, and\n * the `::oid` cast maps a negative hash onto the same unsigned value the\n * catalog stores.\n */\nconst HELD_PROBE_SQL = `SELECT EXISTS (\n SELECT 1 FROM pg_locks\n WHERE locktype = 'advisory' AND granted AND objsubid = 2\n AND pid = pg_backend_pid()\n AND classid = (hashtext('smrt'))::oid\n AND objid = (hashtext('db:migrate'))::oid\n) AS held`;\n\n/**\n * The lock session runs one statement and then sits idle for the whole\n * migration, so a role- or database-level `idle_session_timeout` (PostgreSQL\n * 14+) would reap it first and silently free the lock. Exempt this one\n * session; older servers have no such setting.\n */\nasync function exemptFromIdleSessionTimeout(\n session: Awaited<\n ReturnType<NonNullable<DatabaseInterface['acquireSession']>>\n >,\n): Promise<void> {\n const version = await session.query(\n \"SELECT current_setting('server_version_num')::int AS version\",\n );\n if (Number(version.rows?.[0]?.version) >= 140000) {\n await session.query('SET idle_session_timeout = 0');\n }\n}\n\nfunction resolveEngine(\n db: DatabaseInterface,\n engineHint: string | undefined,\n): DatabaseEngine {\n const dbWithConfig = db as DatabaseInterface & { config?: { url?: string } };\n const url = db.url || dbWithConfig.config?.url || '';\n return detectEngine(url, engineHint);\n}\n\nfunction isTrue(value: unknown): boolean {\n return value === true || value === 't' || value === 'true' || value === 1;\n}\n"],"mappings":"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"}
@@ -242,6 +242,8 @@ export interface MigrationsConfig {
242
242
  lockTimeout?: string;
243
243
  /** `statement_timeout` for every PostgreSQL migration statement (default: '60s') */
244
244
  statementTimeout?: string;
245
+ /** Wait for a concurrent `db:migrate` to release the migration lock (default: '15min') */
246
+ migrationLockTimeout?: string;
245
247
  };
246
248
  }
247
249
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/migrations/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/migrations/types.ts"],"names":[],"mappings":"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"}
@@ -6,10 +6,11 @@ import { buildIntegerWidthStatements, buildIntegerWidthTableStatements, collectI
6
6
  import { computeChecksum, normalizeSQL, shortChecksum, verifyChecksum } from "./migrations/checksum.js";
7
7
  import { FRAMEWORK_BASE_TABLE_NAMES, dropFrameworkBaseTables, planFrameworkBaseTableDrop } from "./migrations/framework-base-tables.js";
8
8
  import { MigrationGenerator, createMigrationDefinition, generateMigrationSequence, generateMigrationTimestamp } from "./migrations/generator.js";
9
+ import { DEFAULT_MIGRATION_LOCK_WAIT_TIMEOUT_MS, MIGRATION_ADVISORY_LOCK_KEYS, MigrationLockLostError, MigrationLockTimeoutError, MigrationLockUnsupportedError, acquireMigrationLock } from "./migrations/migration-lock.js";
9
10
  import { MinorUnitsPreflightError, buildMinorUnitsStatements, preflightMinorUnitsRescale, rescaleMoneyColumnsToMinorUnits } from "./migrations/minor-units.js";
10
11
  import { collectNullEqualIndexTargets, migrateNullEqualIndexes, nullEqualDuplicateDetector, nullEqualIndexStatements, preflightNullEqualIndexes } from "./migrations/null-equal-indexes.js";
11
12
  import { MigrationTracker, buildConcurrentIndexPlan, extractCreatedIndexName, planPostgresStatements } from "./migrations/tracker.js";
12
13
  import { getPendingSchemaStatements, migrateSmrtSchemas } from "./migrations/orchestrate.js";
13
14
  import { LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX, backfillLegacyQualifiedNames, collectLegacyQualifiedNameTargets, countLegacyQualifiedNameReferences, legacyQualifiedNamesBackfillName } from "./migrations/qualified-name-aliases.js";
14
15
  import "./migrations/index.js";
15
- export { BackfillTableUnavailableError, BackfillTracker, FRAMEWORK_BASE_TABLE_NAMES, LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX, MigrationGenerator, MigrationTracker, MinorUnitsPreflightError, REQUIRED_COLUMN_NOT_ADDED, SQLITE_REBUILD_TABLE_PREFIX, SchemaComparer, backfillLegacyQualifiedNames, buildConcurrentIndexPlan, buildIntegerWidthStatements, buildIntegerWidthTableStatements, buildMinorUnitsStatements, buildSqliteRebuildStatements, canonicalizeDefault, collectIntegerWidthTargets, collectLegacyQualifiedNameTargets, collectNullEqualIndexTargets, computeChecksum, countLegacyQualifiedNameReferences, createMigrationDefinition, dropFrameworkBaseTables, extractCreatedIndexName, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, isSqliteRebuildPlaceholder, legacyQualifiedNamesBackfillName, migrateNullEqualIndexes, migrateSmrtSchemas, normalizeSQL, nullEqualDuplicateDetector, nullEqualIndexStatements, parsePostgresTimeoutMs, planFrameworkBaseTableDrop, planPostgresStatements, planSqliteTableRebuilds, preflightIntegerWidthWidening, preflightMinorUnitsRescale, preflightNullEqualIndexes, rescaleMoneyColumnsToMinorUnits, rewriteSqliteCreateTable, shortChecksum, sqliteRebuildPlaceholderSql, uniqueColumnIndexName, verifyChecksum, widenIntegerColumnsToBigInt };
16
+ export { BackfillTableUnavailableError, BackfillTracker, DEFAULT_MIGRATION_LOCK_WAIT_TIMEOUT_MS, FRAMEWORK_BASE_TABLE_NAMES, LEGACY_QUALIFIED_NAMES_BACKFILL_PREFIX, MIGRATION_ADVISORY_LOCK_KEYS, MigrationGenerator, MigrationLockLostError, MigrationLockTimeoutError, MigrationLockUnsupportedError, MigrationTracker, MinorUnitsPreflightError, REQUIRED_COLUMN_NOT_ADDED, SQLITE_REBUILD_TABLE_PREFIX, SchemaComparer, acquireMigrationLock, backfillLegacyQualifiedNames, buildConcurrentIndexPlan, buildIntegerWidthStatements, buildIntegerWidthTableStatements, buildMinorUnitsStatements, buildSqliteRebuildStatements, canonicalizeDefault, collectIntegerWidthTargets, collectLegacyQualifiedNameTargets, collectNullEqualIndexTargets, computeChecksum, countLegacyQualifiedNameReferences, createMigrationDefinition, dropFrameworkBaseTables, extractCreatedIndexName, generateMigrationSequence, generateMigrationTimestamp, generateSchemaDiff, getPendingSchemaStatements, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, isSqliteRebuildPlaceholder, legacyQualifiedNamesBackfillName, migrateNullEqualIndexes, migrateSmrtSchemas, normalizeSQL, nullEqualDuplicateDetector, nullEqualIndexStatements, parsePostgresTimeoutMs, planFrameworkBaseTableDrop, planPostgresStatements, planSqliteTableRebuilds, preflightIntegerWidthWidening, preflightMinorUnitsRescale, preflightNullEqualIndexes, rescaleMoneyColumnsToMinorUnits, rewriteSqliteCreateTable, shortChecksum, sqliteRebuildPlaceholderSql, uniqueColumnIndexName, verifyChecksum, widenIntegerColumnsToBigInt };
@@ -3,13 +3,13 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-core",
6
- "packageVersion": "0.55.2",
6
+ "packageVersion": "0.55.4",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "e73abc58c7f470eaf362cbe6c226d78155b82926c60cbdc8215492bd4c15b3df",
11
- "packageJson": "42b1b4b740965693dfaceb4c2e053ef3aaafb0b8cdae6a0712d0eb2acb78e688",
12
- "agents": "18be0a7299730add4b09bf14144ab217f7f1fb60dd68afbe08be775266ead19f",
10
+ "manifest": "9dc6a50400c93c8c52706b8e210a459f5766db6d62feb1191fa04cf1e7e8f71b",
11
+ "packageJson": "0d1cb2a735db71dd538cbe088b4ae71aa84305f8eee70d58e0825bb8c7877fa9",
12
+ "agents": "9e34f2a3e85666f8ba64d1bdd8e8ed55384421d7bffc263ce0fb3109dfdcafaa",
13
13
  "moduleDoc:agents/object-runtime.md": "652352cdf1eddd0dcc91f6589b8241d9d99581f54bcc2318520a3ff97cd22cf5",
14
14
  "moduleDoc:agents/revision-guard.md": "aa6b1ddb5b6b49fa27ebbe575ec7fcd6d5ceb35ee25acc702f877a226eb9a99a",
15
15
  "moduleDoc:agents/collection-reads.md": "e5bb79b59c17e79a85a71580a019e6b0b641e498ac4d850073fd5ad634b57cb7",
@@ -1122,7 +1122,7 @@
1122
1122
  "polymorphicAssociations": 1,
1123
1123
  "uuidColumns": 3
1124
1124
  },
1125
- "agentDoc": "# @happyvertical/smrt-core\n\nFoundation ORM, registry, schema/code generation, AI integration, and DispatchBus.\nRead the module for the subsystem being edited; root AGENTS covers shared model\nand repository rules.\n\n## Modules\n\n| Source | Scope | Module doc |\n|---|---|---|\n| `src/object.ts`, `src/collection.ts`, `src/child-accessors.ts` | Lifecycle, hydration, operators, STI, child accessors, dispatch | [agents/object-runtime.md](agents/object-runtime.md) |\n| `src/revision-guard.ts` | Guarded writes and PostgreSQL revision precision | [agents/revision-guard.md](agents/revision-guard.md) |\n| `src/collection.ts` | Projections, latest-related, facets, counts, read plans | [agents/collection-reads.md](agents/collection-reads.md) |\n| `src/collection.ts` | Limits, sort whitelist, generated list order | [agents/query-bounds.md](agents/query-bounds.md) |\n| `src/data-query.ts` | Transport-neutral bounded query normalization | [agents/data-query.md](agents/data-query.md) |\n| `src/schema/`, `src/migrations/`, `src/cascade.ts`, `src/system/` | DDL parity, indexes, migrations, delete integrity, retention | [agents/schema-paths.md](agents/schema-paths.md) |\n| `src/postgres-permissions.ts` | Explicit PostgreSQL ACL plans and atomic reconciliation | [deployment permission contract](../../docs/content/postgres-permissions.md) |\n| `src/change-feed.ts` | Durable changes, cursors, table versions, retention | [agents/change-feed.md](agents/change-feed.md) |\n| `src/change-feed-authz.ts` | Consumer table/row change-feed authorization hooks (#3020) | [agents/change-feed.md](agents/change-feed.md) |\n| `src/change-signals.ts` | Signal bus, replica fan-out, SSE | [agents/change-signals.md](agents/change-signals.md) |\n| `src/generators/`, `src/vite-plugin/web-collections.ts` | REST/CLI/MCP generation, manifest hashes, ETags | [agents/generators.md](agents/generators.md) |\n| `src/vite-plugin/`, `src/consumer-plugin/`, `src/knowledge.ts` | Decorator UI hints, knowledge projection, generation snapshots | [agents/build-knowledge.md](agents/build-knowledge.md) |\n| `src/knowledge-graph.ts` | Cross-package knowledge graph merge and freshness check (#2863) — see [standards](../../docs/content/standards.md) | — |\n| `src/object.ts`, `src/collection.ts`, `src/learning/memory.ts` | Context memory and semantic search | [agents/memory.md](agents/memory.md) |\n| `src/system/diagnostics.ts` | SELECT-only `_smrt_*` diagnostics reader behind smrt-dev-mcp runtime tools (#1824) | [agents/system-diagnostics.md](agents/system-diagnostics.md) |\n| `src/system/registry-snapshot.ts` | Sanitized plain-JSON projection of the booted `ObjectRegistry` for the smrt-dev-mcp runtime dev-plane (#1831); never constructors, validators, values, or absolute paths | [agents/registry-snapshot.md](agents/registry-snapshot.md) |\n| `src/registry/qualified-name-aliases.ts`, `src/migrations/qualified-name-aliases.ts` | Model moves: `previousQualifiedNames` aliases, deprecation, doctor counts, opt-in backfill (#3338) | [agents/model-moves.md](agents/model-moves.md) |\n| `src/run-once.ts` | Shared idempotency seam: insert-only claim + caller work in one transaction, replay on retry, typed in-flight/unknown-outcome answers (#3080) | [agents/run-once.md](agents/run-once.md) |\n| `src/recipe.ts`, `../scanner/src/recipes.ts` | `SmrtRecipe`: declared user-facing units (models, nav, `requires`, curation `options`) collected into manifest and knowledge `recipes` (#3590) | [agents/recipes.md](agents/recipes.md) |\n\n## Cross-module invariants\n\n- `ObjectRegistry` is a `globalThis` singleton so registration survives HMR.\n- Any output that names a database URL (CLI banners, logs, errors) renders it\n with `redactDatabaseUrl` / `redactDatabaseUrlsInText`\n (`src/utils/database-url.ts`, #3527): never the password or query string,\n and never the raw string when it cannot be parsed.\n- Per-class registry work on the hydration path (manifest reconciliation in\n `ensureManifestLoaded()`, the simple-name index behind `findClass()`) is\n memoized per registry generation (`src/registry/generation.ts`, #3047). Any\n new writer of registered classes, their field/method metadata, or loaded\n manifests must go through the tracked maps or call\n `bumpRegistryGeneration()`; `issue-3047-hydration-registry-work.test.ts`\n guards that hydration does not repeat this work per row.\n\n- Production DDL uses manifest generators; `getTestDatabase()` uses registry\n generators. Keep columns, indexes, FK actions, and runtime conflict targets in\n parity (`src/schema/schema-path-parity.test.ts`). Every new query predicate\n needs its index or an explicit reason none is needed.\n- Tenant scoping covers every read and every unique/conflict key. Explicit\n conflict columns are not rewritten; their author must include tenant scope.\n A `tenantId` field makes rows tenant-OWNED without any tenancy declaration:\n the default key leads with `tenant_id`. A new object's natural-key save reads\n the row its upsert would hit and refuses a different owner\n (`TENANT_ISOLATION_VIOLATION`) or adopts a same-owner row's id, so\n `DO UPDATE` never rewrites a primary key or owner. Only an explicit natural\n key adopts: a slug `getSlug()` derived from `name`/`title`/`label` moves to\n a free `-2`, `-3`, … slug and INSERTs. See `agents/schema-paths.md`\n (\"Tenant OWNERSHIP\").\n- Persisted saves use `id` and loaded `updated_at`; new saves use natural keys.\n Preserve revision compare-and-swap ordering through public `save()`,\n `claimRevision()`, and transaction APIs. Embedded saves, deletes, and complete\n `withTransaction()` callbacks share a process-local write queue.\n- Collection model hydration is serial in result order: initialization may query\n the same transaction-bound PostgreSQL client. Use projections for plain rows.\n- Native DuckDB UUIDs must be cast coherently on read before identity reuse;\n custom embedded revision paths use `getCanonicalPersistedRow()`. Never replay\n a mutation to discover result types.\n- `withDatabase(db, callback)` restores only database bindings (including public\n `options.db`); `withTransaction(callback)` also restores identity/revision\n metadata after rollback. Do not use a bound instance concurrently.\n- `runOnce({ db, tenantId, actor, token, content }, work)` (`src/run-once.ts`,\n #3080) claims a key derived from all four inputs — never the token alone,\n which collapses distinct submissions — via `INSERT ... ON CONFLICT\n (claim_key) DO NOTHING RETURNING claim_key` into the hand-DDL\n `_smrt_run_once_claims` system table, and runs `work` in the SAME\n transaction; a completed claim replays its stored result instead of\n re-running, a failed `work` rolls the claim back with the transaction, and\n an unresolved claim raises typed `RunOnceClaimError`\n (`RUN_ONCE_IN_FLIGHT` / `RUN_ONCE_OUTCOME_UNKNOWN`) rather than guessing.\n The claim insert must never raise on conflict: PostgreSQL aborts the whole\n transaction on any raised error, which would take the recovery `SELECT`\n down with it too — this is why it is `DO NOTHING`, not a plain `INSERT`\n caught for `isUniqueViolationError`. Completed claims are bounded by\n `runRetentionSweep()`'s `run-once-claims` task (`system/retention.ts`,\n `DEFAULT_RETENTION_POLICY.runOnceClaims`, default 30 days on\n `completed_at`; `in_progress` rows are never matched) — the window is a\n replay deadline, not just disk hygiene: a token replayed after its claim\n is swept runs `work()` again. See `agents/run-once.md`.\n- `ensureSystemTables(db, typeHint?)` provisions framework tables idempotently;\n call it on a base PostgreSQL connection before caller-owned transactions.\n Bootstrap uses a transaction-scoped advisory lock, taken only when the\n PostgreSQL schema version is not yet stamped: inside a caller's transaction it\n is held until that transaction ends (#2868). Application tables still require migrations;\n runtime table verification checks existence only.\n- Manifest generation fails closed on scanner errors, including unresolved\n decorator spreads. Never emit partial, default-open registration.\n- Generated registration repairs bundled class identity using the exact imported\n constructor, explicit package, and isolated one-object manifest. Never infer\n ownership from paths, simple names, or table names; packages can share names.\n Consumer regression gate: `packages/bundle-gate/src/__tests__/registry-identity.spec.ts`.\n- Decorator registration trusts a class's stack-derived package only when that\n package declares the class (stub, own manifest entry, or explicit\n `packageName`); it then matches the qualified key and never adopts another\n package's same-named registration. Otherwise a loaded (or the cwd project's)\n manifest entry describing the declaring file supplies the identity — apps\n scan workspace-package sources under the app's name (#3110). A class of\n another known package coexists under its qualified key\n (`decorator-different-packages-qualified-coexist`); bundled output needs\n a resolved table that differs first (#3106). Stack\n attribution skips smrt-core (including source-mapped installed\n `smrt-core/src` frames, #3109), decorator-helper, and module-runner frames,\n and drops a frame's Vite `?v=` query (#3490). Library builds stamp each\n decorated class with `static __smrtPackage__` (own property, read at\n decoration as its explicit package) and publish `stampsConstructors`; an\n unstamped class never takes a stamping package's entry by name and table,\n in either order (#3490, `issue-3490-*.test.ts`, `package-stamp.test.ts`).\n Qualified names are `@scope/pkg:Class`; the scope is a deliberate\n discriminator, so manifest generation (`generateManifest`,\n `applyGenerationPasses`, hence `pnpm build`) throws\n `CONFIG_UNSCOPED_PACKAGE_NAME` for an unscoped `package.json` name (#3408).\n Instance and collection registry lookups pass the qualified name. The schema\n planner rejects a table claimed by two unrelated classes\n (`CONFIG_TABLE_NAME_COLLISION`): only one STI family shares a table (#3098);\n collections (including `SmrtReportCollection`) never claim one. Consumer\n gates: `registry/__tests__/issue-3110-*.test.ts`, `issue-3109-*.test.ts`,\n `packages/bundle-gate/src/__tests__/consumer-same-name.spec.ts`.\n- API custom-action eligibility has ONE resolver, `resolveApiMethodExposure()`\n in `generators/custom-action.ts`: both SvelteKit route emitters,\n `resolveApiActionSet`, and `knowledge.ts` read it, and a new consumer must too\n — a local mirror is how a method gets reported unavailable while its route\n file is still written. A public method routes by default only when every\n parameter is JSON-shaped; `@method({ expose })` overrides in both directions,\n `expose: true` bypasses the heuristic ALONE, and an explicit `api.include` or\n `api.routes` entry keeps its pre-#2686 route. Fail closed on scanner\n uncertainty: read `parameters[].typeUnresolved`, never the `'any'` it\n substitutes. Accepting `Date` as wire-able and hydrating it\n (`toCustomActionDate`) are one decision — changing either breaks the other.\n The runtime `APIGenerator` transport stays declaration-gated: `rest.ts`\n dispatch and `preflight-route.ts` prediction both read\n `declaresRuntimeRestRoute()`, which must accept every `ApiCustomRouteConfig`\n option so a sweep moving one onto its method cannot delete the endpoint. Its\n twin `declaresRuntimeRestRouteShape()` deliberately ignores `expose: false`:\n the dispatcher must still SEE a withheld declaration to answer 404, because\n `POST /<collection>/<segment>` resolves to `create` when nothing claims the\n segment. Split unions and type arguments with `splitTopLevel()` — a naive\n `split('|')` truncates `Record<string, Asset | null>` into fragments that\n match no rule and are then accepted, widening the gate. `extractTypeName`\n returns `null` for a generic with an unresolvable ARGUMENT or a union with an\n unresolvable BRANCH so those reach that fail-closed path instead of arriving\n as a bare `'Array'`. Runtime transports read\n `ObjectRegistry.resolveRuntimeMethod()`, which tries the item class's manifest\n entry, then the COLLECTION class's (where a collection-hosted action's\n parameters live), then the live `@method()` store — keyed by CONSTRUCTOR,\n never by simple name (`Account` exists in two packages), and recording\n `isStatic` because an unscanned runtime has no manifest to recover the\n receiver from. `isRestActionRoutable` ANDs \"declared\" with that receiver, so\n it never predicts `allow` for an action dispatch refuses. A declared action\n with no receiver is refused (501 collection-scoped, 404 item-scoped), never\n allowed to fall through into `create`. All eight consumers read the resolver,\n including `packages/smrt-workbench/src/discovery.ts`; it imports the\n `./generators/custom-action` LEAF subpath, not `./generators`, because that\n barrel value-re-exports `MCPGenerator` and so drags `@happyvertical/ai` and\n `@happyvertical/sql` into a build-time helper (2 modules vs 109). Keep\n `custom-action.ts` free of value imports outside `tools/tool-generator`.\n\n## Gotchas\n\n- Optional filesystem support stays lazy: use `createFilesystemAdapter()` in\n `src/filesystem-loader.ts`, not a static files-SDK import. Fully bundled apps\n import `@happyvertical/smrt-core/filesystem` at startup. Use\n `importOptionalDependency()` for similarly heavy optional dependencies.\n- `exports['.'].browser` (`dist/browser.js`) is the browser surface. Model\n package roots are bundled for browsers by the bundle-gate browser fixture\n (#3621): anything a model root imports from core must be exported by the\n browser entry and must not reach `node:` built-ins, `pg`, `cosmiconfig`, or\n `jiti`. Keep Node-only code behind a subpath or a lazy import; when a fix\n makes a package pass, delete its entry in\n `packages/bundle-gate/src/browser-gate/expected-failures.ts`.\n- Database retries are transient-only, four attempts total for `get`/`upsert`.\n Use `src/db-errors.ts` classifiers through the cause chain, never message\n matching (SDK driver text can live in `context.originalError`). Constraints,\n bad input, missing tables, and aborted PostgreSQL transactions fail immediately.\n Unique/PK violations become `VALIDATION_UNIQUE_CONSTRAINT`, NOT NULL becomes\n `VALIDATION_REQUIRED_FIELD`; other failures keep the driver error as `cause`.\n- Property initializers precede option values; options win. Arrays/objects are\n shallow-cloned. Collection creation caches fields; table verification caches\n by DB URL and table. Preserve these scopes when changing initialization.\n- The Vite plugin loads scanner/schema code from `dist/` when present. Rebuild\n core after editing those sources before testing consumer manifest generation.\n Vite 8 requires `oxc.decorator: { legacy: true, emitDecoratorMetadata: true }`.\n\n## Validation\n\n`typecheck` checks production source first, then the test project in\n`tsconfig.test.json`. Test coverage and the shared relaxed config\ncontract are documented in [agents/test-types.md](agents/test-types.md).\n\nRun focused tests first, then applicable package checks:\n\n```bash\npnpm --filter @happyvertical/smrt-core test\npnpm --filter @happyvertical/smrt-core typecheck\npnpm --filter @happyvertical/smrt-core build\npnpm --filter @happyvertical/smrt-core test:postgres\npnpm check:agents-chain\npnpm smrt dev:knowledge-check\n```\n\nThe PostgreSQL lane is required for numeric types, UUID casts, conflict targets,\ntimestamps, and migrations. Tests generate their manifest before Vitest; restart\nwatch mode after adding decorated classes. Documentation-only changes need\ninstruction-chain and knowledge freshness checks, not the runtime test suite.\n",
1125
+ "agentDoc": "# @happyvertical/smrt-core\n\nFoundation ORM, registry, schema/code generation, AI integration, and DispatchBus.\nRead the module for the subsystem being edited; root AGENTS covers shared model\nand repository rules.\n\n## Modules\n\n| Source | Scope | Module doc |\n|---|---|---|\n| `src/object.ts`, `src/collection.ts`, `src/child-accessors.ts` | Lifecycle, hydration, operators, STI, child accessors, dispatch | [agents/object-runtime.md](agents/object-runtime.md) |\n| `src/revision-guard.ts` | Guarded writes and PostgreSQL revision precision | [agents/revision-guard.md](agents/revision-guard.md) |\n| `src/collection.ts` | Projections, latest-related, facets, counts, read plans | [agents/collection-reads.md](agents/collection-reads.md) |\n| `src/collection.ts` | Limits, sort whitelist, generated list order | [agents/query-bounds.md](agents/query-bounds.md) |\n| `src/data-query.ts` | Transport-neutral bounded query normalization | [agents/data-query.md](agents/data-query.md) |\n| `src/schema/`, `src/migrations/`, `src/cascade.ts`, `src/system/` | DDL parity, indexes, migrations, delete integrity, retention | [agents/schema-paths.md](agents/schema-paths.md) |\n| `src/postgres-permissions.ts` | Explicit PostgreSQL ACL plans and atomic reconciliation | [deployment permission contract](../../docs/content/postgres-permissions.md) |\n| `src/change-feed.ts` | Durable changes, cursors, table versions, retention | [agents/change-feed.md](agents/change-feed.md) |\n| `src/change-feed-authz.ts` | Consumer table/row change-feed authorization hooks (#3020) | [agents/change-feed.md](agents/change-feed.md) |\n| `src/change-signals.ts` | Signal bus, replica fan-out, SSE | [agents/change-signals.md](agents/change-signals.md) |\n| `src/generators/`, `src/vite-plugin/web-collections.ts` | REST/CLI/MCP generation, manifest hashes, ETags | [agents/generators.md](agents/generators.md) |\n| `src/vite-plugin/`, `src/consumer-plugin/`, `src/knowledge.ts` | Decorator UI hints, knowledge projection, generation snapshots | [agents/build-knowledge.md](agents/build-knowledge.md) |\n| `src/knowledge-graph.ts` | Cross-package knowledge graph merge and freshness check (#2863) — see [standards](../../docs/content/standards.md) | — |\n| `src/object.ts`, `src/collection.ts`, `src/learning/memory.ts` | Context memory and semantic search | [agents/memory.md](agents/memory.md) |\n| `src/system/diagnostics.ts` | SELECT-only `_smrt_*` diagnostics reader behind smrt-dev-mcp runtime tools (#1824) | [agents/system-diagnostics.md](agents/system-diagnostics.md) |\n| `src/system/registry-snapshot.ts` | Sanitized plain-JSON projection of the booted `ObjectRegistry` for the smrt-dev-mcp runtime dev-plane (#1831); never constructors, validators, values, or absolute paths | [agents/registry-snapshot.md](agents/registry-snapshot.md) |\n| `src/registry/qualified-name-aliases.ts`, `src/migrations/qualified-name-aliases.ts` | Model moves: `previousQualifiedNames` aliases, deprecation, doctor counts, opt-in backfill (#3338) | [agents/model-moves.md](agents/model-moves.md) |\n| `src/run-once.ts` | Shared idempotency seam: insert-only claim + caller work in one transaction, replay on retry, typed in-flight/unknown-outcome answers (#3080) | [agents/run-once.md](agents/run-once.md) |\n| `src/recipe.ts`, `../scanner/src/recipes.ts` | `SmrtRecipe`: declared user-facing units (models, nav, `requires`, curation `options`) collected into manifest and knowledge `recipes` (#3590) | [agents/recipes.md](agents/recipes.md) |\n\n## Cross-module invariants\n\n- `ObjectRegistry` is a `globalThis` singleton so registration survives HMR.\n- Any output that names a database URL (CLI banners, logs, errors) renders it\n with `redactDatabaseUrl` / `redactDatabaseUrlsInText`\n (`src/utils/database-url.ts`, #3527): never the password or query string,\n and never the raw string when it cannot be parsed.\n- Per-class registry work on the hydration path (manifest reconciliation in\n `ensureManifestLoaded()`, the simple-name index behind `findClass()`) is\n memoized per registry generation (`src/registry/generation.ts`, #3047). Any\n new writer of registered classes, their field/method metadata, or loaded\n manifests must go through the tracked maps or call\n `bumpRegistryGeneration()`; `issue-3047-hydration-registry-work.test.ts`\n guards that hydration does not repeat this work per row.\n\n- Production DDL uses manifest generators; `getTestDatabase()` uses registry\n generators. Keep columns, indexes, FK actions, and runtime conflict targets in\n parity (`src/schema/schema-path-parity.test.ts`). Every new query predicate\n needs its index or an explicit reason none is needed.\n- Tenant scoping covers every read and every unique/conflict key. Explicit\n conflict columns are not rewritten; their author must include tenant scope.\n A `tenantId` field makes rows tenant-OWNED without any tenancy declaration:\n the default key leads with `tenant_id`. A new object's natural-key save reads\n the row its upsert would hit and refuses a different owner\n (`TENANT_ISOLATION_VIOLATION`) or adopts a same-owner row's id, so\n `DO UPDATE` never rewrites a primary key or owner. Only an explicit natural\n key adopts: a slug `getSlug()` derived from `name`/`title`/`label` moves to\n a free `-2`, `-3`, … slug and INSERTs. See `agents/schema-paths.md`\n (\"Tenant OWNERSHIP\").\n- Persisted saves use `id` and loaded `updated_at`; new saves use natural keys.\n Preserve revision compare-and-swap ordering through public `save()`,\n `claimRevision()`, and transaction APIs. Embedded saves, deletes, and complete\n `withTransaction()` callbacks share a process-local write queue.\n- Collection model hydration is serial in result order: initialization may query\n the same transaction-bound PostgreSQL client. Use projections for plain rows.\n- Native DuckDB UUIDs must be cast coherently on read before identity reuse;\n custom embedded revision paths use `getCanonicalPersistedRow()`. Never replay\n a mutation to discover result types.\n- `withDatabase(db, callback)` restores only database bindings (including public\n `options.db`); `withTransaction(callback)` also restores identity/revision\n metadata after rollback. Do not use a bound instance concurrently.\n- `runOnce({ db, tenantId, actor, token, content }, work)` (`src/run-once.ts`,\n #3080) claims a key derived from all four inputs — never the token alone,\n which collapses distinct submissions — via `INSERT ... ON CONFLICT\n (claim_key) DO NOTHING RETURNING claim_key` into the hand-DDL\n `_smrt_run_once_claims` system table, and runs `work` in the SAME\n transaction; a completed claim replays its stored result instead of\n re-running, a failed `work` rolls the claim back with the transaction, and\n an unresolved claim raises typed `RunOnceClaimError`\n (`RUN_ONCE_IN_FLIGHT` / `RUN_ONCE_OUTCOME_UNKNOWN`) rather than guessing.\n The claim insert must never raise on conflict: PostgreSQL aborts the whole\n transaction on any raised error, which would take the recovery `SELECT`\n down with it too — this is why it is `DO NOTHING`, not a plain `INSERT`\n caught for `isUniqueViolationError`. Completed claims are bounded by\n `runRetentionSweep()`'s `run-once-claims` task (`system/retention.ts`,\n `DEFAULT_RETENTION_POLICY.runOnceClaims`, default 30 days on\n `completed_at`; `in_progress` rows are never matched) — the window is a\n replay deadline, not just disk hygiene: a token replayed after its claim\n is swept runs `work()` again. See `agents/run-once.md`.\n- `ensureSystemTables(db, typeHint?)` provisions framework tables idempotently;\n call it on a base PostgreSQL connection before caller-owned transactions.\n Bootstrap uses a transaction-scoped advisory lock, taken only when the\n PostgreSQL schema version is not yet stamped: inside a caller's transaction it\n is held until that transaction ends (#2868). Application tables still require migrations;\n runtime table verification checks existence only.\n- Manifest generation fails closed on scanner errors, including unresolved\n decorator spreads. Never emit partial, default-open registration.\n- Generated registration repairs bundled class identity using the exact imported\n constructor, explicit package, and isolated one-object manifest. Never infer\n ownership from paths, simple names, or table names; packages can share names.\n Consumer regression gate: `packages/bundle-gate/src/__tests__/registry-identity.spec.ts`.\n- Decorator registration trusts a class's stack-derived package only when that\n package declares the class (stub, own manifest entry, or explicit\n `packageName`); it then matches the qualified key and never adopts another\n package's same-named registration. Otherwise a loaded (or the cwd project's)\n manifest entry describing the declaring file supplies the identity — apps\n scan workspace-package sources under the app's name (#3110). A class of\n another known package coexists under its qualified key\n (`decorator-different-packages-qualified-coexist`); bundled output needs\n a resolved table that differs first (#3106). Stack\n attribution skips smrt-core (including source-mapped installed\n `smrt-core/src` frames, #3109), decorator-helper, and module-runner frames,\n and drops a frame's Vite `?v=` query (#3490). Library builds stamp each\n decorated class with `static __smrtPackage__` (own property, read at\n decoration as its explicit package) and publish `stampsConstructors`; an\n unstamped class never takes a stamping package's entry by name and table,\n in either order (#3490, `issue-3490-*.test.ts`, `package-stamp.test.ts`).\n Qualified names are `@scope/pkg:Class`; the scope is a deliberate\n discriminator, so manifest generation (`generateManifest`,\n `applyGenerationPasses`, hence `pnpm build`) throws\n `CONFIG_UNSCOPED_PACKAGE_NAME` for an unscoped `package.json` name (#3408).\n Instance and collection registry lookups pass the qualified name. The schema\n planner rejects a table claimed by two unrelated classes\n (`CONFIG_TABLE_NAME_COLLISION`): only one STI family shares a table (#3098);\n collections (including `SmrtReportCollection`) never claim one. Consumer\n gates: `registry/__tests__/issue-3110-*.test.ts`, `issue-3109-*.test.ts`,\n `packages/bundle-gate/src/__tests__/consumer-same-name.spec.ts`.\n- API custom-action eligibility has ONE resolver, `resolveApiMethodExposure()`\n in `generators/custom-action.ts`: both SvelteKit route emitters,\n `resolveApiActionSet`, and `knowledge.ts` read it, and a new consumer must too\n — a local mirror is how a method gets reported unavailable while its route\n file is still written. A public method routes by default only when every\n parameter is JSON-shaped; `@method({ expose })` overrides in both directions,\n `expose: true` bypasses the heuristic ALONE, and an explicit `api.include` or\n `api.routes` entry keeps its pre-#2686 route. Fail closed on scanner\n uncertainty: read `parameters[].typeUnresolved`, never the `'any'` it\n substitutes. Accepting `Date` as wire-able and hydrating it\n (`toCustomActionDate`) are one decision — changing either breaks the other.\n The runtime `APIGenerator` transport stays declaration-gated: `rest.ts`\n dispatch and `preflight-route.ts` prediction both read\n `declaresRuntimeRestRoute()`, which must accept every `ApiCustomRouteConfig`\n option so a sweep moving one onto its method cannot delete the endpoint. Its\n twin `declaresRuntimeRestRouteShape()` deliberately ignores `expose: false`:\n the dispatcher must still SEE a withheld declaration to answer 404, because\n `POST /<collection>/<segment>` resolves to `create` when nothing claims the\n segment. Split unions and type arguments with `splitTopLevel()` — a naive\n `split('|')` truncates `Record<string, Asset | null>` into fragments that\n match no rule and are then accepted, widening the gate. `extractTypeName`\n returns `null` for a generic with an unresolvable ARGUMENT or a union with an\n unresolvable BRANCH so those reach that fail-closed path instead of arriving\n as a bare `'Array'`. Runtime transports read\n `ObjectRegistry.resolveRuntimeMethod()`, which tries the item class's manifest\n entry, then the COLLECTION class's (where a collection-hosted action's\n parameters live), then the live `@method()` store — keyed by CONSTRUCTOR,\n never by simple name (`Account` exists in two packages), and recording\n `isStatic` because an unscanned runtime has no manifest to recover the\n receiver from. `isRestActionRoutable` ANDs \"declared\" with that receiver, so\n it never predicts `allow` for an action dispatch refuses. A declared action\n with no receiver is refused (501 collection-scoped, 404 item-scoped), never\n allowed to fall through into `create`. All eight consumers read the resolver,\n including `packages/smrt-workbench/src/discovery.ts`; it imports the\n `./generators/custom-action` LEAF subpath, not `./generators`, because that\n barrel value-re-exports `MCPGenerator` and so drags `@happyvertical/ai` and\n `@happyvertical/sql` into a build-time helper (2 modules vs 109). Keep\n `custom-action.ts` free of value imports outside `tools/tool-generator`.\n\n## Gotchas\n\n- Optional filesystem support stays lazy: use `createFilesystemAdapter()` in\n `src/filesystem-loader.ts`, not a static files-SDK import. Fully bundled apps\n import `@happyvertical/smrt-core/filesystem` at startup. Use\n `importOptionalDependency()` for similarly heavy optional dependencies.\n- `src/browser.ts` is the `browser`-condition entry of the package root: the\n Node entry (`index.ts`) minus HTTP servers, manifest/knowledge discovery, run-once,\n the Vite plugin, and the test helper. A new export added to `index.ts` must also\n go to `browser.ts` unless it is Node-only; `src/__tests__/browser-entry-parity.test.ts`\n fails otherwise (allowlist: `NODE_ONLY_GROUPS` in that test, each with a reason; the browser\n entry may not publish a name the Node entry lacks), and also\n fails when a workspace package imports a root value the browser entry lacks.\n- `exports['.'].browser` (`dist/browser.js`) is the browser surface. Model\n package roots are bundled for browsers by the bundle-gate browser fixture\n (#3621): anything a model root imports from core must be exported by the\n browser entry and must not reach `node:` built-ins, `pg`, `cosmiconfig`, or\n `jiti`. Keep Node-only code behind a subpath or a lazy import; when a fix\n makes a package pass, delete its entry in\n `packages/bundle-gate/src/browser-gate/expected-failures.ts`.\n- Database retries are transient-only, four attempts total for `get`/`upsert`.\n Use `src/db-errors.ts` classifiers through the cause chain, never message\n matching (SDK driver text can live in `context.originalError`). Constraints,\n bad input, missing tables, and aborted PostgreSQL transactions fail immediately.\n Unique/PK violations become `VALIDATION_UNIQUE_CONSTRAINT`, NOT NULL becomes\n `VALIDATION_REQUIRED_FIELD`; other failures keep the driver error as `cause`.\n- Property initializers precede option values; options win. Arrays/objects are\n shallow-cloned. Collection creation caches fields; table verification caches\n by DB URL and table. Preserve these scopes when changing initialization.\n- The Vite plugin loads scanner/schema code from `dist/` when present. Rebuild\n core after editing those sources before testing consumer manifest generation.\n Vite 8 requires `oxc.decorator: { legacy: true, emitDecoratorMetadata: true }`.\n\n## Validation\n\n`typecheck` checks production source first, then the test project in\n`tsconfig.test.json`. Test coverage and the shared relaxed config\ncontract are documented in [agents/test-types.md](agents/test-types.md).\n\nRun focused tests first, then applicable package checks:\n\n```bash\npnpm --filter @happyvertical/smrt-core test\npnpm --filter @happyvertical/smrt-core typecheck\npnpm --filter @happyvertical/smrt-core build\npnpm --filter @happyvertical/smrt-core test:postgres\npnpm check:agents-chain\npnpm smrt dev:knowledge-check\n```\n\nThe PostgreSQL lane is required for numeric types, UUID casts, conflict targets,\ntimestamps, and migrations. Tests generate their manifest before Vitest; restart\nwatch mode after adding decorated classes. Documentation-only changes need\ninstruction-chain and knowledge freshness checks, not the runtime test suite.\n",
1126
1126
  "moduleDocs": [
1127
1127
  {
1128
1128
  "path": "agents/object-runtime.md",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-core",
3
- "version": "0.55.2",
3
+ "version": "0.55.4",
4
4
  "description": "Core AI agent framework with standardized collections, object-relational mapping, and code generators",
5
5
  "author": "HappyVertical",
6
6
  "type": "module",
@@ -165,15 +165,15 @@
165
165
  }
166
166
  },
167
167
  "dependencies": {
168
- "@happyvertical/ai": "^0.102.2",
169
- "@happyvertical/files": "^0.102.2",
170
- "@happyvertical/json": "^0.102.2",
171
- "@happyvertical/logger": "^0.102.2",
172
- "@happyvertical/smrt-config": "0.55.2",
173
- "@happyvertical/smrt-scanner": "0.55.2",
174
- "@happyvertical/smrt-types": "0.55.2",
175
- "@happyvertical/sql": "^0.102.2",
176
- "@happyvertical/utils": "^0.102.2",
168
+ "@happyvertical/ai": "^0.102.3",
169
+ "@happyvertical/files": "^0.102.3",
170
+ "@happyvertical/json": "^0.102.3",
171
+ "@happyvertical/logger": "^0.102.3",
172
+ "@happyvertical/smrt-config": "0.55.4",
173
+ "@happyvertical/smrt-scanner": "0.55.4",
174
+ "@happyvertical/smrt-types": "0.55.4",
175
+ "@happyvertical/sql": "^0.102.3",
176
+ "@happyvertical/utils": "^0.102.3",
177
177
  "@oxc-project/runtime": "^0.150.0",
178
178
  "cheerio": "^1.2.0",
179
179
  "fast-glob": "3.3.3",