@happyvertical/smrt-core 0.50.0 → 0.51.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/dist/index.js +2 -1
- package/dist/knowledge-graph.d.ts +95 -0
- package/dist/knowledge-graph.d.ts.map +1 -0
- package/dist/knowledge-graph.js +268 -0
- package/dist/knowledge-graph.js.map +1 -0
- package/dist/knowledge.d.ts +1 -0
- package/dist/knowledge.d.ts.map +1 -1
- package/dist/knowledge.js +57 -4
- package/dist/knowledge.js.map +1 -1
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/migrations/differ.d.ts +127 -21
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +379 -84
- package/dist/migrations/differ.js.map +1 -1
- package/dist/schema/column-data-probes.d.ts +38 -0
- package/dist/schema/column-data-probes.d.ts.map +1 -0
- package/dist/schema/column-data-probes.js +106 -0
- package/dist/schema/column-data-probes.js.map +1 -0
- package/dist/schema/live-parity.d.ts.map +1 -1
- package/dist/schema/live-parity.js +37 -46
- package/dist/schema/live-parity.js.map +1 -1
- package/dist/smrt-knowledge.json +12 -7
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live-parity.js","names":[],"sources":["../../src/schema/live-parity.ts"],"sourcesContent":["/**\n * Live-schema parity check (#2368)\n *\n * Compares a **live** database against the shape the model layer assumes.\n * Three inputs define \"expected\":\n *\n * 1. **Declared table shapes** — the manifest schemas the caller passes in.\n * These give tables, columns, types, nullability, and declared indexes.\n * 2. **Hand-DDL system tables** — `_smrt_*` tables parsed out of\n * `src/system/schema.ts` (see `system-table-shapes.ts`). They never enter\n * the manifest, so nothing else has ever diffed them.\n * 3. **An expected-shape index policy that does not consult the manifest** —\n * every foreign-key / cross-package-ref / tenant column carries a leading\n * index, every declared upsert conflict target is backed by a matching\n * UNIQUE index, and every `unique: true` column is actually unique in the\n * live database.\n *\n * Point 3 is the reason this module exists rather than reusing the migration\n * differ: the differ compares the live database to the manifest, i.e. to the\n * artifact that dropped the index in the first place, which is why a database\n * missing 164 tenant-column indexes reported \"in sync\" (#2356).\n *\n * This module is read-only. It emits findings; it never repairs.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport {\n collectIntegerWidthTargets,\n preflightIntegerWidthWidening,\n} from '../migrations/integer-width.js';\nimport { RETIRED_SYSTEM_TABLES } from '../system/schema.js';\nimport { detectEngine, getDDLStrategy } from './ddl/index.js';\nimport type { DatabaseEngine } from './ddl/types.js';\n// The canonical PostgreSQL uuid-text shape probe (#2608): every framework\n// guard that decides whether a `text` column can be reinterpreted as `uuid`\n// tests the same pattern (the orphan probe, the FK provisioning guard, the\n// uuid convergence planner). Reused here rather than duplicated so a rename\n// candidate and a uuid-convergence candidate never disagree about \"shaped\".\nimport {\n CANONICAL_UUID_PATTERN,\n CANONICAL_UUID_SQLITE_GLOB_PATTERN,\n} from './foreign-key-ddl.js';\nimport { quoteIdentifier } from './sql-identifiers.js';\nimport { getSystemTableShapes } from './system-table-shapes.js';\nimport type { ColumnDefinition, SchemaDefinition } from './types.js';\n\n/** How badly a finding breaks the deployment. */\nexport type LiveParitySeverity = 'error' | 'warning' | 'info';\n\n/** The class of drift a finding describes. */\nexport type LiveParityFindingKind =\n | 'missing_table'\n | 'extra_table'\n | 'missing_column'\n | 'extra_column'\n | 'column_type_drift'\n | 'column_nullability_drift'\n | 'missing_index'\n | 'extra_index'\n | 'unindexed_reference'\n | 'conflict_target_unindexed'\n | 'conflict_target_not_unique'\n | 'unique_constraint_missing'\n | 'invalid_index'\n /** Pre-#2373 PostgreSQL/DuckDB int4 column needing opt-in widening. */\n | 'legacy_integer_width'\n /**\n * A declared column exists live and holds no data while an undeclared\n * column of a compatible type does (#2752) — the shape a framework field\n * rename leaves behind when `db:migrate` adds the new column additively\n * and never moves the old data into it.\n */\n | 'rename_data_pending';\n\n/** One difference between the live database and the expected shape. */\nexport interface LiveParityFinding {\n kind: LiveParityFindingKind;\n severity: LiveParitySeverity;\n /** Table the finding is about. */\n table: string;\n /** Column or index the finding is about, when it is narrower than a table. */\n target?: string;\n /** Whether the table is an application table or a `_smrt_*` system table. */\n origin: ExpectedTableOrigin;\n message: string;\n recommendation: string;\n details?: Record<string, unknown>;\n}\n\n/** Aggregate result of one parity run. */\nexport interface LiveSchemaParityReport {\n engine: DatabaseEngine;\n /** Expected tables that exist in the live database and were compared. */\n tablesChecked: number;\n /** Expected tables that are absent from the live database. */\n tablesMissing: number;\n /** Whether `_smrt_*` system tables participated. */\n systemTablesIncluded: boolean;\n /**\n * `full` when index metadata (uniqueness, partial predicates, PostgreSQL\n * validity) could be read for this engine; `unavailable` when it could not,\n * in which case every index-class check is skipped rather than guessed.\n */\n indexIntrospection: 'full' | 'unavailable';\n findings: LiveParityFinding[];\n counts: Record<LiveParitySeverity, number>;\n /** True when no `error`-severity finding was produced. */\n ok: boolean;\n}\n\n/** A declared upsert conflict target for one table. */\nexport interface ConflictTargetInput {\n columns: string[];\n /** Where the target came from, e.g. a class name — used in messages. */\n source?: string;\n}\n\nexport interface LiveSchemaParityOptions {\n db: DatabaseInterface;\n /** Expected application table shapes, keyed by table name. */\n schemas?: Record<string, SchemaDefinition>;\n /** Declared upsert conflict targets, keyed by table name. */\n conflictTargets?: Record<string, ConflictTargetInput[]>;\n /** Include `_smrt_*` system tables (default true). */\n includeSystemTables?: boolean;\n /** Explicit engine hint for adapters whose URL is empty or ambiguous. */\n engineHint?: string;\n /** Report live tables no expected shape covers (default true). */\n reportExtraTables?: boolean;\n}\n\n/** Whether an expected table is an application table or a system table. */\nexport type ExpectedTableOrigin = 'application' | 'system';\n\ninterface ExpectedColumn {\n name: string;\n /** Engine-materialized type text. */\n type: string;\n primaryKey: boolean;\n notNull: boolean;\n unique: boolean;\n hasDefault: boolean;\n reference?: ColumnDefinition['referenceKind'];\n}\n\ninterface ExpectedIndex {\n name: string;\n columns: string[];\n unique: boolean;\n}\n\ninterface ExpectedTable {\n name: string;\n origin: ExpectedTableOrigin;\n columns: ExpectedColumn[];\n indexes: ExpectedIndex[];\n conflictTargets: ConflictTargetInput[];\n}\n\ninterface LiveColumn {\n name: string;\n type: string;\n notNull: boolean;\n primaryKey: boolean;\n hasDefault: boolean;\n}\n\ninterface LiveIndex {\n name: string;\n columns: string[];\n unique: boolean;\n /** PostgreSQL `indisvalid`; always true on engines without the concept. */\n valid: boolean;\n /** True for the index backing a primary key. */\n primary: boolean;\n /** True when the index is partial (has a WHERE predicate). */\n partial: boolean;\n}\n\n/**\n * Thrown when the live database cannot be introspected. Callers fail closed:\n * an unreadable database is never reported as \"in sync\".\n */\nexport class LiveSchemaParityError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'LiveSchemaParityError';\n }\n}\n\n/**\n * Compare a live database against the expected shape and return every\n * difference found.\n *\n * @throws {LiveSchemaParityError} when the database cannot be reached or the\n * adapter cannot describe a table. Connection problems must fail closed.\n */\nexport async function checkLiveSchemaParity(\n options: LiveSchemaParityOptions,\n): Promise<LiveSchemaParityReport> {\n const {\n db,\n schemas = {},\n conflictTargets = {},\n includeSystemTables = true,\n engineHint,\n reportExtraTables = true,\n } = options;\n\n const engine = resolveEngine(db, engineHint);\n const expected = buildExpectedTables({\n schemas,\n conflictTargets,\n includeSystemTables,\n engine,\n });\n\n const liveTableNames = await listLiveTables(db, engine);\n const indexCatalog = await readIndexCatalog(db, engine);\n const findings: LiveParityFinding[] = [];\n\n let tablesChecked = 0;\n let tablesMissing = 0;\n\n for (const table of expected) {\n if (!liveTableNames.has(table.name)) {\n tablesMissing++;\n findings.push({\n kind: 'missing_table',\n severity: 'error',\n table: table.name,\n origin: table.origin,\n message: `Table \\`${table.name}\\` is declared but does not exist in the live database.`,\n recommendation:\n table.origin === 'system'\n ? 'Initialize SMRT system tables against this database (they are created on framework bootstrap).'\n : 'Run `smrt db:migrate` to create the missing table.',\n });\n continue;\n }\n\n tablesChecked++;\n const liveColumns = await readLiveColumns(db, table.name);\n findings.push(...compareColumns(table, liveColumns, engine));\n findings.push(\n ...(await detectRenameDataPending(db, engine, table, liveColumns)),\n );\n\n if (indexCatalog) {\n const liveIndexes = await indexCatalog.forTable(table.name);\n findings.push(...compareIndexes(table, liveColumns, liveIndexes));\n }\n }\n\n // #2373 intentionally leaves int4/int8 equivalent to the ordinary differ\n // (and to structural parity) so fresh BIGINT DDL does not auto-rewrite a\n // production table. Surface the remaining 32-bit columns as advisory-only\n // findings here, with row counts for a maintenance-window estimate.\n const widthPreflight = await preflightIntegerWidthWidening(\n db,\n collectIntegerWidthTargets(schemas, { includeSystemTables }),\n { engineHint },\n );\n if (widthPreflight.supported) {\n for (const table of widthPreflight.tables) {\n for (const column of table.columns) {\n if (column.state !== 'pending') continue;\n findings.push({\n kind: 'legacy_integer_width',\n severity: 'warning',\n table: table.table,\n target: column.column,\n origin: table.table.startsWith('_smrt_') ? 'system' : 'application',\n message:\n `Column \\`${table.table}.${column.column}\\` is legacy \\`${column.declaredType}\\` ` +\n `instead of BIGINT (${table.rowCount ?? 0} row(s) in the table).`,\n recommendation:\n 'Schedule a maintenance window, run `smrt db:migrate-int8 --dry-run`, then run `smrt db:migrate-int8` after reviewing the table-rewrite plan.',\n details: {\n actual: column.declaredType,\n expected: 'BIGINT',\n rowCount: table.rowCount,\n },\n });\n }\n }\n }\n\n if (reportExtraTables) {\n const expectedNames = new Set(expected.map((table) => table.name));\n for (const liveName of liveTableNames) {\n if (expectedNames.has(liveName)) continue;\n if (isInternalTableName(liveName)) continue;\n // Without system tables in scope, every `_smrt_*` table would be\n // reported as unexplained; that is a scoping artifact, not drift.\n if (!includeSystemTables && liveName.startsWith('_smrt_')) continue;\n // A retired system table is not unexplained drift — the framework\n // deliberately stopped creating it and deliberately does not drop it\n // (issue #2376). Name the exact remedy instead of the generic advice.\n const retired = RETIRED_SYSTEM_TABLES.includes(liveName);\n findings.push({\n kind: 'extra_table',\n severity: 'info',\n table: liveName,\n origin: liveName.startsWith('_smrt_') ? 'system' : 'application',\n message: retired\n ? `Live table \\`${liveName}\\` is a retired SMRT system table; nothing reads or writes it.`\n : `Live table \\`${liveName}\\` is not covered by any declared schema.`,\n recommendation: retired\n ? `Drop it once you have confirmed it is unused: DROP TABLE IF EXISTS ${liveName};`\n : 'Confirm the table belongs to another application sharing this database, or remove it once its owning model is gone.',\n });\n }\n }\n\n const counts: Record<LiveParitySeverity, number> = {\n error: 0,\n warning: 0,\n info: 0,\n };\n for (const finding of findings) {\n counts[finding.severity]++;\n }\n\n return {\n engine,\n tablesChecked,\n tablesMissing,\n systemTablesIncluded: includeSystemTables,\n indexIntrospection: indexCatalog ? 'full' : 'unavailable',\n findings,\n counts,\n ok: counts.error === 0,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Expected shape\n// ---------------------------------------------------------------------------\n\nfunction buildExpectedTables(input: {\n schemas: Record<string, SchemaDefinition>;\n conflictTargets: Record<string, ConflictTargetInput[]>;\n includeSystemTables: boolean;\n engine: DatabaseEngine;\n}): ExpectedTable[] {\n const { schemas, conflictTargets, includeSystemTables, engine } = input;\n const strategy = getDDLStrategy(engine);\n const tables: ExpectedTable[] = [];\n\n for (const [key, schema] of Object.entries(schemas)) {\n const tableName = schema?.tableName || key;\n if (!tableName) continue;\n\n const columns: ExpectedColumn[] = Object.entries(schema.columns ?? {}).map(\n ([columnName, column]) => ({\n name: columnName,\n type: strategy.mapType(column.type),\n primaryKey: column.primaryKey === true,\n notNull: column.notNull === true || column.primaryKey === true,\n unique: column.unique === true,\n hasDefault: column.defaultValue !== undefined,\n reference: column.foreignKey ? 'foreignKey' : column.referenceKind,\n }),\n );\n\n tables.push({\n name: tableName,\n origin: 'application',\n columns,\n indexes: (schema.indexes ?? [])\n // Expression indexes (`@meta({ indexed: true })`) surface with empty\n // or null column lists in introspection, so a column-based comparison\n // can only produce false drift for them.\n .filter((index) => !index.jsonPath && (index.columns?.length ?? 0) > 0)\n .map((index) => ({\n name: index.name,\n columns: index.columns,\n unique: index.unique === true,\n })),\n conflictTargets: dedupeConflictTargets(conflictTargets[tableName] ?? []),\n });\n }\n\n if (includeSystemTables) {\n for (const shape of getSystemTableShapes(engine).values()) {\n tables.push({\n name: shape.tableName,\n origin: 'system',\n columns: shape.columns.map((column) => ({\n name: column.name,\n type: column.type,\n primaryKey: column.primaryKey,\n notNull: column.notNull || column.primaryKey,\n unique: column.unique,\n hasDefault: column.hasDefault,\n })),\n indexes: shape.indexes.map((index) => ({\n name: index.name,\n columns: index.columns,\n unique: index.unique,\n })),\n // Inline `UNIQUE (...)` constraints are what the framework upserts\n // against on system tables, so they are the conflict targets here.\n conflictTargets: dedupeConflictTargets(\n shape.uniqueConstraints.map((columns) => ({\n columns,\n source: `${shape.tableName} DDL`,\n })),\n ),\n });\n }\n }\n\n return tables;\n}\n\nfunction dedupeConflictTargets(\n targets: ConflictTargetInput[],\n): ConflictTargetInput[] {\n const seen = new Map<string, ConflictTargetInput>();\n for (const target of targets) {\n const columns = (target.columns ?? []).filter(Boolean);\n if (columns.length === 0) continue;\n const key = [...columns].sort().join(',');\n if (!seen.has(key)) {\n seen.set(key, { columns, source: target.source });\n }\n }\n return [...seen.values()];\n}\n\n// ---------------------------------------------------------------------------\n// Column comparison\n// ---------------------------------------------------------------------------\n\nfunction compareColumns(\n table: ExpectedTable,\n liveColumns: Map<string, LiveColumn>,\n engine: DatabaseEngine,\n): LiveParityFinding[] {\n const findings: LiveParityFinding[] = [];\n const expectedNames = new Set(table.columns.map((column) => column.name));\n\n for (const column of table.columns) {\n const live = liveColumns.get(column.name);\n if (!live) {\n findings.push({\n kind: 'missing_column',\n severity: 'error',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is declared but missing from the live database.`,\n recommendation:\n 'Run `smrt db:migrate` to add the column before application writes reach this table.',\n details: { expectedType: column.type },\n });\n continue;\n }\n\n if (!typesAreEquivalent(column, live, engine)) {\n findings.push({\n kind: 'column_type_drift',\n severity: 'error',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is \\`${live.type}\\` in the live database but declared \\`${column.type}\\`.`,\n recommendation:\n 'Reconcile the column type with `smrt db:migrate`, or repair the declaration if the live type is correct.',\n details: { expected: column.type, actual: live.type },\n });\n } else {\n const expectedBucket = normalizeSqlType(column.type);\n const actualBucket = normalizeSqlType(live.type);\n\n // #2772: `TEXT` (live) vs `JSON`/`JSONB` (declared) is tolerated above\n // (#1335) so it never becomes an `error`, but on a table SMRT itself\n // creates this is real, repairable drift — `db:migrate` can converge\n // it (see `differ.ts`'s shape-probed `type_upgrade`). Surface it as a\n // warning rather than staying invisible; the reverse direction (a\n // native `json`/`jsonb` column backed by a text-convention manifest\n // field) stays silent — that pairing is intentional, not drift.\n //\n // Engine-gated to `postgres` to match the differ's own repair gate\n // (`jsonUpgradeCandidate` in `differ.ts`, `this.engine === 'postgres'`\n // only): on DuckDB/SQLite there is no `type_upgrade` path for this\n // pairing, so flagging it here would be a permanent, unclearable\n // warning (review finding — the same class already fixed for #2770's\n // float-width check).\n if (\n engine === 'postgres' &&\n expectedBucket === 'JSON' &&\n actualBucket === 'TEXT'\n ) {\n findings.push({\n kind: 'column_type_drift',\n severity: 'warning',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is \\`${live.type}\\` in the live database but declared \\`${column.type}\\`.`,\n recommendation:\n 'Run `smrt db:migrate` to converge this column to native jsonb once its live values are confirmed valid JSON.',\n details: { expected: column.type, actual: live.type },\n });\n } else if (\n engine === 'postgres' &&\n expectedBucket === 'UUID' &&\n actualBucket === 'TEXT' &&\n isStructuralReference(column)\n ) {\n // #2772: the reverse of the uuid tolerance above already has a\n // framework repair path (`db:migrate-uuid`, #2608) — this differs\n // from the jsonb case in staying `info`: text/uuid interop on\n // structural columns is an intentional, long-supported compatibility\n // shape, not a bug, so this is a pointer to the optional convergence\n // path rather than a warning.\n //\n // Engine-gated to `postgres` to match `db:migrate-uuid`'s own repair\n // gate (`cli/src/commands/db-migrate-uuid.ts`, `runConvert = ... &&\n // isPostgres`): on DuckDB there is no native-uuid conversion path for\n // this pairing, so flagging it here would be a permanent, unclearable\n // finding (review finding — the same class already fixed for the\n // jsonb and float-width checks above).\n findings.push({\n kind: 'column_type_drift',\n severity: 'info',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is \\`${live.type}\\` in the live database but declared \\`${column.type}\\`; SMRT tolerates text/uuid for structural identifier/reference columns.`,\n recommendation:\n 'Run `smrt db:migrate-uuid` to converge this column to native uuid, or leave it as-is — this pairing is tolerated indefinitely.',\n details: { expected: column.type, actual: live.type },\n });\n } else if (\n (engine === 'postgres' || engine === 'duckdb') &&\n expectedBucket === 'REAL' &&\n actualBucket === 'REAL'\n ) {\n // #2770: REAL/DOUBLE PRECISION/DECIMAL/NUMERIC all normalize into\n // one 'REAL' bucket above, so single- vs double-precision float\n // drift never reaches the `!typesAreEquivalent` branch — the same\n // way int4-vs-int8 drift hides behind the shared 'INTEGER' bucket\n // (see `legacy_integer_width` below). Detect it here instead.\n //\n // Engine-gated to match the differ's own repair gate (`differ.ts`,\n // `this.engine === 'postgres' || this.engine === 'duckdb'`): SQLite\n // stores every real as an 8-byte double regardless of the declared\n // type name, so there is no narrowing and no `type_upgrade` path —\n // flagging it there would be a permanent, unclearable warning\n // (review finding).\n const expectedPrecision = floatPrecisionOf(column.type, engine);\n const actualPrecision = floatPrecisionOf(live.type, engine);\n if (\n expectedPrecision &&\n actualPrecision &&\n expectedPrecision !== actualPrecision\n ) {\n const widening =\n expectedPrecision === 'double' && actualPrecision === 'single';\n findings.push({\n kind: 'column_type_drift',\n // Consistent with the other width findings (`legacy_integer_width`\n // below): a maintenance concern to schedule, not a broken write path.\n severity: 'warning',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is \\`${live.type}\\` (${actualPrecision}-precision) in the live database but declared \\`${column.type}\\` (${expectedPrecision}-precision).`,\n recommendation: widening\n ? 'Run `smrt db:migrate` to widen this column to double precision (lossless).'\n : 'Narrowing to single precision can lose data; confirm the narrower declaration is intentional before repairing it manually.',\n details: { expected: column.type, actual: live.type },\n });\n }\n }\n }\n\n // A declared-NOT NULL column that is nullable live silently accepts rows\n // the model layer believes cannot exist; the reverse breaks every insert\n // that omits the column, which is the worse failure.\n if (!column.primaryKey && column.notNull && !live.notNull) {\n findings.push({\n kind: 'column_nullability_drift',\n severity: 'warning',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is declared NOT NULL but is nullable in the live database.`,\n recommendation:\n 'Backfill any NULL values and add the NOT NULL constraint, or relax the declaration.',\n });\n } else if (!column.notNull && live.notNull && !live.hasDefault) {\n findings.push({\n kind: 'column_nullability_drift',\n severity: 'error',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is NOT NULL in the live database with no default, but is not declared NOT NULL. Inserts that omit it will fail.`,\n recommendation:\n 'Drop the orphan NOT NULL constraint, give the column a default, or declare the column required.',\n });\n }\n }\n\n for (const live of liveColumns.values()) {\n if (expectedNames.has(live.name)) continue;\n\n const breaksInserts = live.notNull && !live.hasDefault;\n findings.push({\n kind: 'extra_column',\n severity: breaksInserts ? 'error' : 'info',\n table: table.name,\n target: live.name,\n origin: table.origin,\n message: breaksInserts\n ? `Live column \\`${table.name}.${live.name}\\` is undeclared, NOT NULL, and has no default — every framework insert into this table will fail.`\n : `Live column \\`${table.name}.${live.name}\\` is not declared by the current schema.`,\n recommendation: breaksInserts\n ? 'Drop the orphan column or give it a default; this is the residue of a renamed or removed required field.'\n : 'Confirm the column is intentional legacy data, or drop it once nothing reads it.',\n details: { type: live.type },\n });\n }\n\n return findings;\n}\n\n/**\n * Whether a live column type satisfies its declaration.\n *\n * Two tolerances match the model layer's actual conventions, and mirror the\n * migration differ so the two tools never disagree:\n *\n * - `TEXT` ↔ `UUID` for structural identifier/reference columns, because\n * SQLite has no uuid type and older PostgreSQL deployments legitimately\n * still carry text ids (R11).\n * - `TEXT` ↔ `JSON`/`JSONB` in both directions, because SMRT serializes JSON\n * values into text columns and a native json column already holds exactly\n * that (#1335).\n */\nfunction typesAreEquivalent(\n expected: ExpectedColumn,\n live: LiveColumn,\n engine: DatabaseEngine,\n): boolean {\n const expectedType = normalizeSqlType(expected.type);\n const actualType = normalizeSqlType(live.type);\n if (expectedType === actualType) return true;\n\n const uuidTextPair =\n (expectedType === 'UUID' && actualType === 'TEXT') ||\n (expectedType === 'TEXT' && actualType === 'UUID');\n if (uuidTextPair && isStructuralReference(expected)) {\n return true;\n }\n\n const jsonTextPair =\n (expectedType === 'JSON' && actualType === 'TEXT') ||\n (expectedType === 'TEXT' && actualType === 'JSON');\n if (jsonTextPair) return true;\n\n // SQLite has dynamic typing: BOOLEAN/TIMESTAMP columns are declared with\n // affinities that pragma reports verbatim, and INTEGER/NUMERIC storage is\n // interchangeable for the values SMRT writes.\n if (engine === 'sqlite') {\n const numericPair =\n (expectedType === 'BOOLEAN' && actualType === 'INTEGER') ||\n (expectedType === 'INTEGER' && actualType === 'BOOLEAN');\n if (numericPair) return true;\n }\n\n return false;\n}\n\n/**\n * Single- vs double-precision float classification for #2770's float-width\n * drift detection. `null` for anything that isn't unambiguously one or the\n * other (DECIMAL/NUMERIC have no fixed binary width and are out of scope).\n *\n * Bare `FLOAT` is engine-ambiguous and must be classified per `engine`:\n * PostgreSQL's `information_schema` reports `float4`/`real` as `real` and\n * `float8`/`double precision` as `double precision` — `FLOAT` alone never\n * appears there, so treating it as double-precision was previously safe.\n * DuckDB is different: `information_schema.columns.data_type` normalizes\n * *both* spellings of its single-precision type (`REAL`, `FLOAT4`) to the\n * bare string `FLOAT`, and reports its double-precision type as `DOUBLE`\n * (never `FLOAT`). Classifying DuckDB's `FLOAT` as double-precision — as a\n * shared, engine-unaware regex previously did — made every converged DuckDB\n * `REAL` column permanently misreport as narrowing drift with no\n * `db:migrate` able to clear it (review finding on #2770).\n */\nfunction floatPrecisionOf(\n type: string,\n engine: DatabaseEngine,\n): 'single' | 'double' | null {\n const upper = String(type ?? '')\n .toUpperCase()\n .trim()\n .replace(/\\(\\s*\\d+(\\s*,\\s*\\d+)?\\s*\\)/g, '');\n if (/^(REAL|FLOAT4)$/.test(upper)) return 'single';\n if (engine === 'duckdb' && upper === 'FLOAT') return 'single';\n if (/^(FLOAT8|DOUBLE|DOUBLE PRECISION|FLOAT)$/.test(upper)) return 'double';\n return null;\n}\n\nfunction isStructuralReference(column: ExpectedColumn): boolean {\n return (\n column.primaryKey ||\n column.reference === 'id' ||\n column.reference === 'foreignKey' ||\n column.reference === 'crossPackageRef' ||\n column.reference === 'tenantId'\n );\n}\n\n/** Fold engine-specific type spellings into comparable buckets. */\nexport function normalizeSqlType(type: string): string {\n const upper = String(type ?? '')\n .toUpperCase()\n .trim()\n .replace(/\\(\\s*[^)]*\\s*\\)/g, '')\n .replace(/\\[\\]$/, '')\n .trim();\n\n if (\n /^(INTEGER|INT|INT2|INT4|INT8|BIGINT|SMALLINT|TINYINT|SERIAL|BIGSERIAL)$/.test(\n upper,\n )\n )\n return 'INTEGER';\n if (\n /^(TEXT|CLOB|STRING|VARCHAR|CHAR|CHARACTER|CHARACTER VARYING|NAME)$/.test(\n upper,\n )\n )\n return 'TEXT';\n if (upper === 'UUID') return 'UUID';\n if (\n /^(REAL|FLOAT|FLOAT4|FLOAT8|DOUBLE|DOUBLE PRECISION|DECIMAL|NUMERIC|NUMBER)$/.test(\n upper,\n )\n )\n return 'REAL';\n if (/^(BOOLEAN|BOOL)$/.test(upper)) return 'BOOLEAN';\n if (/^(TIMESTAMPTZ|TIMESTAMP WITH TIME ZONE)$/.test(upper))\n return 'TIMESTAMPTZ';\n if (\n /^(DATETIME|TIMESTAMP|TIMESTAMP WITHOUT TIME ZONE|DATE|TIME)$/.test(upper)\n )\n return 'TIMESTAMP';\n if (/^(BLOB|BINARY|BYTEA)$/.test(upper)) return 'BLOB';\n if (/^(JSON|JSONB)$/.test(upper)) return 'JSON';\n\n return upper;\n}\n\n// ---------------------------------------------------------------------------\n// Rename-data-pending detection (#2752)\n// ---------------------------------------------------------------------------\n\n/** How a candidate (undeclared, live) column's type relates to a declared column's. */\ntype RenameCompatibility = 'same-type' | 'text-to-uuid';\n\n/**\n * Whether an undeclared live column could be the pre-rename home of a\n * declared column's data. \"Compatible\" per #2752: identical normalized\n * type, or the undeclared column is `TEXT` while the declared column is\n * `UUID` (gated separately on data shape by the caller).\n */\nfunction renameCompatibility(\n declaredType: string,\n candidateType: string,\n): RenameCompatibility | null {\n if (declaredType === candidateType) return 'same-type';\n if (declaredType === 'UUID' && candidateType === 'TEXT') {\n return 'text-to-uuid';\n }\n return null;\n}\n\nasync function columnHasNonEmptyValue(\n db: DatabaseInterface,\n table: string,\n column: string,\n): Promise<boolean> {\n const quotedTable = quoteIdentifier(table);\n const quotedColumn = quoteIdentifier(column);\n const result = await db.query(\n `SELECT 1 AS present FROM ${quotedTable} ` +\n `WHERE ${quotedColumn} IS NOT NULL AND CAST(${quotedColumn} AS TEXT) <> '' LIMIT 1`,\n );\n return (result?.rows?.length ?? 0) > 0;\n}\n\nasync function allNonEmptyValuesUuidShaped(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n table: string,\n column: string,\n): Promise<boolean> {\n const quotedTable = quoteIdentifier(table);\n const quotedColumn = quoteIdentifier(column);\n const nonEmptyPredicate = `${quotedColumn} IS NOT NULL AND CAST(${quotedColumn} AS TEXT) <> ''`;\n\n if (engine === 'postgres') {\n const result = await db.query(\n `SELECT count(*) AS invalid_count FROM ${quotedTable} ` +\n `WHERE ${nonEmptyPredicate} ` +\n `AND CAST(${quotedColumn} AS TEXT) !~* '${CANONICAL_UUID_PATTERN}'`,\n );\n const row = result?.rows?.[0] as Record<string, unknown> | undefined;\n return Number(row?.invalid_count ?? 0) === 0;\n }\n\n // SQLite (the only other engine this detector runs against) has no\n // regex operator, but its case-sensitive `GLOB` can still express the\n // fixed 36-character canonical shape (against `LOWER(...)`, guarded by an\n // exact `LENGTH(...) = 36`), so this runs one server-side aggregate\n // `count(*)` too rather than fetching every non-empty value into JS to\n // test in a loop — important on a production table with many rows\n // (#2767 review).\n const result = await db.query(\n `SELECT count(*) AS invalid_count FROM ${quotedTable} ` +\n `WHERE ${nonEmptyPredicate} ` +\n `AND NOT (LENGTH(CAST(${quotedColumn} AS TEXT)) = 36 ` +\n `AND LOWER(CAST(${quotedColumn} AS TEXT)) GLOB '${CANONICAL_UUID_SQLITE_GLOB_PATTERN}')`,\n );\n const row = result?.rows?.[0] as Record<string, unknown> | undefined;\n return Number(row?.invalid_count ?? 0) === 0;\n}\n\nfunction buildRenameDataPendingFinding(\n table: ExpectedTable,\n declaredColumn: string,\n candidates: string[],\n): LiveParityFinding {\n const single = candidates.length === 1;\n const candidateList = candidates.map((name) => `\\`${name}\\``).join(', ');\n return {\n kind: 'rename_data_pending',\n severity: 'warning',\n table: table.name,\n target: declaredColumn,\n origin: table.origin,\n message: single\n ? `Column \\`${table.name}.${declaredColumn}\\` is declared but empty, while undeclared column \\`${table.name}.${candidates[0]}\\` holds data of a compatible type. Data appears to still live in \\`${candidates[0]}\\` after a field rename.`\n : `Column \\`${table.name}.${declaredColumn}\\` is declared but empty, while ${candidates.length} undeclared columns hold data of a compatible type (${candidateList}). Data may still live in one of them after a field rename, but which one is ambiguous.`,\n recommendation: single\n ? `Run \\`smrt db:diff\\` for the suggested backfill SQL, review it, then copy data from \\`${candidates[0]}\\` into \\`${declaredColumn}\\` and drop \\`${candidates[0]}\\`.`\n : 'Identify which undeclared column actually holds the pre-rename data before backfilling; SMRT will not guess among multiple candidates.',\n details: { candidates },\n };\n}\n\n/**\n * Detect a pending rename backfill (#2752): a manifest-declared column that\n * exists live but holds no data, paired with an undeclared live column of a\n * compatible type that does hold data. This is the shape a framework field\n * rename leaves behind when `db:migrate` adds the new column additively and\n * never moves data into it, nor drops the old column.\n *\n * Advisory only (`warning`): this module never repairs, and when several\n * undeclared columns qualify it lists them all rather than guessing.\n */\nasync function detectRenameDataPending(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n table: ExpectedTable,\n liveColumns: Map<string, LiveColumn>,\n): Promise<LiveParityFinding[]> {\n // The row-level probes below need plain SQL this module already assumes\n // for these two engines; DuckDB/JSON are out of scope (matches the\n // PostgreSQL/SQLite advisory SQL `db:diff` emits for the same pair).\n if (engine !== 'postgres' && engine !== 'sqlite') return [];\n\n const expectedNames = new Set(table.columns.map((column) => column.name));\n const extraColumns = [...liveColumns.values()].filter(\n (live) => !expectedNames.has(live.name),\n );\n if (extraColumns.length === 0) return [];\n\n const findings: LiveParityFinding[] = [];\n\n for (const column of table.columns) {\n const live = liveColumns.get(column.name);\n if (!live) continue; // missing_column already covers this\n\n let declaredHasData: boolean;\n try {\n declaredHasData = await columnHasNonEmptyValue(\n db,\n table.name,\n column.name,\n );\n } catch {\n continue;\n }\n if (declaredHasData) continue;\n\n const declaredType = normalizeSqlType(column.type);\n const candidates: string[] = [];\n\n for (const extra of extraColumns) {\n const compatibility = renameCompatibility(\n declaredType,\n normalizeSqlType(extra.type),\n );\n if (!compatibility) continue;\n\n let candidateHasData: boolean;\n try {\n candidateHasData = await columnHasNonEmptyValue(\n db,\n table.name,\n extra.name,\n );\n } catch {\n continue;\n }\n if (!candidateHasData) continue;\n\n if (compatibility === 'text-to-uuid') {\n let shaped: boolean;\n try {\n shaped = await allNonEmptyValuesUuidShaped(\n db,\n engine,\n table.name,\n extra.name,\n );\n } catch {\n continue;\n }\n if (!shaped) continue;\n }\n\n candidates.push(extra.name);\n }\n\n if (candidates.length === 0) continue;\n candidates.sort();\n findings.push(\n buildRenameDataPendingFinding(table, column.name, candidates),\n );\n }\n\n return findings;\n}\n\n// ---------------------------------------------------------------------------\n// Index comparison — the manifest-independent policy lives here\n// ---------------------------------------------------------------------------\n\nfunction compareIndexes(\n table: ExpectedTable,\n liveColumns: Map<string, LiveColumn>,\n liveIndexes: LiveIndex[],\n): LiveParityFinding[] {\n const findings: LiveParityFinding[] = [];\n const byName = new Map(liveIndexes.map((index) => [index.name, index]));\n const signatures = new Set(\n liveIndexes.map((index) => indexSignature(index.columns, index.unique)),\n );\n\n for (const index of liveIndexes) {\n if (index.valid) continue;\n findings.push({\n kind: 'invalid_index',\n severity: 'error',\n table: table.name,\n target: index.name,\n origin: table.origin,\n message: `Index \\`${index.name}\\` on \\`${table.name}\\` is INVALID — PostgreSQL will not use it and a unique index in this state enforces nothing.`,\n recommendation:\n 'Drop and rebuild it (`REINDEX INDEX CONCURRENTLY`), then re-run the parity check.',\n });\n }\n\n // 1. Declared indexes the live database does not carry in that shape.\n // Matching is by column/uniqueness signature rather than by name so an\n // equivalent index under a different name is not reported as missing\n // (#741), and so a same-name index whose uniqueness or columns drifted\n // still is.\n for (const index of table.indexes) {\n if (signatures.has(indexSignature(index.columns, index.unique))) continue;\n // Conflict-target coverage is reported below with a message that explains\n // the actual consequence; do not report the same index twice.\n if (\n table.conflictTargets.some((target) =>\n sameColumnSet(index.columns, target.columns),\n )\n ) {\n continue;\n }\n\n const sameName = byName.get(index.name);\n findings.push({\n kind: 'missing_index',\n severity: 'warning',\n table: table.name,\n target: index.name,\n origin: table.origin,\n message: sameName\n ? `Index \\`${index.name}\\` on \\`${table.name}\\` exists but its shape drifted: declared (${index.columns.join(', ')})${index.unique ? ' UNIQUE' : ''}, live (${sameName.columns.join(', ')})${sameName.unique ? ' UNIQUE' : ''}.`\n : `Declared index \\`${index.name}\\` on \\`${table.name}\\` (${index.columns.join(', ')}) is missing from the live database.`,\n recommendation:\n table.origin === 'system'\n ? 'Re-run SMRT system-table initialization to create the missing system index.'\n : 'Run `smrt db:migrate` to create the missing index.',\n });\n }\n\n // 2. Conflict targets. An upsert whose conflict target has no matching\n // UNIQUE index either errors outright (PostgreSQL) or silently inserts\n // duplicates, so this is the one index class that is an error.\n for (const target of table.conflictTargets) {\n if (target.columns.some((column) => !liveColumns.has(column))) {\n // A missing column is already reported; do not double-report it here.\n continue;\n }\n\n const candidates = liveIndexes.filter((index) =>\n sameColumnSet(index.columns, target.columns),\n );\n // A *partial* unique index cannot arbitrate a plain `ON CONFLICT (cols)`:\n // PostgreSQL only infers an index whose predicate the statement repeats,\n // and on other engines it simply does not constrain the excluded rows.\n if (candidates.some((index) => index.unique && !index.partial)) {\n continue;\n }\n\n const partialUnique = candidates.find(\n (index) => index.unique && index.partial,\n );\n const nonUnique = candidates.find((index) => !index.unique);\n const columnList = target.columns.join(', ');\n const suffix = target.source ? ` (declared by ${target.source})` : '';\n\n if (nonUnique) {\n findings.push({\n kind: 'conflict_target_not_unique',\n severity: 'error',\n table: table.name,\n target: nonUnique.name,\n origin: table.origin,\n message: `Upsert conflict target \\`${table.name}\\` (${columnList})${suffix} is backed by \\`${nonUnique.name}\\`, which is not UNIQUE.`,\n recommendation:\n 'Recreate the index as UNIQUE (`smrt db:migrate`); until then upserts insert duplicates instead of updating.',\n });\n continue;\n }\n\n findings.push({\n kind: 'conflict_target_unindexed',\n severity: 'error',\n table: table.name,\n target: partialUnique ? partialUnique.name : columnList,\n origin: table.origin,\n message: partialUnique\n ? `Upsert conflict target \\`${table.name}\\` (${columnList})${suffix} is backed only by the partial index \\`${partialUnique.name}\\`, which cannot arbitrate the upsert.`\n : `Upsert conflict target \\`${table.name}\\` (${columnList})${suffix} has no matching UNIQUE index in the live database.`,\n recommendation:\n 'Create a full UNIQUE index over exactly these columns (`smrt db:migrate`); PostgreSQL rejects the upsert outright and other engines duplicate rows.',\n });\n }\n\n // 3. Columns declared unique that are not actually unique live.\n for (const column of table.columns) {\n if (!column.unique) continue;\n if (!liveColumns.has(column.name)) continue;\n // A partial unique index constrains only the rows its predicate selects,\n // so it does not make the column unique as declared.\n const unique = liveIndexes.some(\n (index) =>\n index.unique &&\n !index.partial &&\n sameColumnSet(index.columns, [column.name]),\n );\n if (unique) continue;\n\n findings.push({\n kind: 'unique_constraint_missing',\n severity: 'error',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is declared unique but no UNIQUE index enforces it in the live database.`,\n recommendation:\n 'De-duplicate existing values and create the UNIQUE index; the declaration is currently decorative.',\n });\n }\n\n // 4. Manifest-independent coverage policy: every foreign-key, cross-package\n // reference and tenant column needs an index it *leads*, or every lookup\n // and every tenant-scoped list is a full scan (#2356).\n for (const column of table.columns) {\n if (!column.reference) continue;\n if (column.reference === 'id') continue;\n if (!liveColumns.has(column.name)) continue;\n\n const covered = liveIndexes.some(\n (index) => index.columns[0] === column.name,\n );\n if (covered) continue;\n\n findings.push({\n kind: 'unindexed_reference',\n severity: 'warning',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Reference column \\`${table.name}.${column.name}\\` (${column.reference}) leads no index in the live database.`,\n recommendation:\n 'Add an index led by this column; relationship loads, joins and tenant-scoped reads currently scan the whole table.',\n details: { referenceKind: column.reference },\n });\n }\n\n // 5. Live indexes nothing declares. Informational: an operator may have\n // hand-added them, and nothing will recreate them after a rebuild.\n //\n // An undeclared index whose lead column is a reference column\n // (tenantId, foreignKey, crossPackageRef) used to be silently exempted\n // here as deliberate policy. `migrations/differ.ts` disagreed and would\n // drop the same index under `--drop-indexes`, so an operator following\n // that command could remove something this check claimed to protect.\n // Decision (#2751): treat it as drift and report it — the recommendation\n // below already reads correctly (\"declare it ... or drop it if it is\n // obsolete\"), and an `info` finding costs nothing.\n const declaredNames = new Set(table.indexes.map((index) => index.name));\n const declaredSignatures = new Set(\n table.indexes.map((index) => indexSignature(index.columns, index.unique)),\n );\n for (const index of liveIndexes) {\n if (index.primary) continue;\n // Constraint-owned indexes (`sqlite_autoindex_*`, PostgreSQL `*_key` /\n // `*_pkey`) exist because of a table constraint, not because anyone\n // declared an index; reporting them as undeclared is pure noise.\n if (isConstraintOwnedIndexName(index.name)) continue;\n if (declaredNames.has(index.name)) continue;\n if (declaredSignatures.has(indexSignature(index.columns, index.unique)))\n continue;\n if (\n table.conflictTargets.some((target) =>\n sameColumnSet(index.columns, target.columns),\n )\n ) {\n continue;\n }\n\n findings.push({\n kind: 'extra_index',\n severity: 'info',\n table: table.name,\n target: index.name,\n origin: table.origin,\n message: `Live index \\`${index.name}\\` on \\`${table.name}\\` (${index.columns.join(', ') || 'expression'}) is not declared by the current schema.`,\n recommendation:\n 'Declare it so a rebuilt database keeps it, or drop it if it is obsolete.',\n details: { unique: index.unique, partial: index.partial },\n });\n }\n\n return findings;\n}\n\nfunction isConstraintOwnedIndexName(name: string): boolean {\n return (\n name.startsWith('sqlite_autoindex_') ||\n name.endsWith('_pkey') ||\n name.endsWith('_key')\n );\n}\n\nfunction indexSignature(columns: string[], unique: boolean): string {\n return `${columns.join(',')}:${unique}`;\n}\n\n/**\n * Conflict targets and unique constraints are order-insensitive: PostgreSQL\n * matches `ON CONFLICT (a, b)` against a unique index on `(b, a)`.\n */\nfunction sameColumnSet(left: string[], right: string[]): boolean {\n if (left.length !== right.length) return false;\n const sortedLeft = [...left].sort();\n const sortedRight = [...right].sort();\n return sortedLeft.every((column, index) => column === sortedRight[index]);\n}\n\n// ---------------------------------------------------------------------------\n// Live introspection\n// ---------------------------------------------------------------------------\n\nfunction resolveEngine(\n db: DatabaseInterface,\n engineHint?: string,\n): DatabaseEngine {\n if (typeof (db as { exportTable?: unknown }).exportTable === 'function') {\n return 'json';\n }\n const dbWithConfig = db as DatabaseInterface & { config?: { url?: string } };\n return detectEngine(db.url || dbWithConfig.config?.url || '', engineHint);\n}\n\nfunction isInternalTableName(name: string): boolean {\n return (\n name.startsWith('sqlite_') ||\n name.startsWith('pg_') ||\n name.startsWith('duckdb_') ||\n name === 'information_schema'\n );\n}\n\nasync function listLiveTables(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n): Promise<Set<string>> {\n const sql =\n engine === 'postgres'\n ? `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'`\n : `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;\n\n try {\n const result = await db.query(sql);\n const rows = (result?.rows ?? []) as {\n name?: string;\n table_name?: string;\n }[];\n return new Set(\n rows.map((row) => row.name || row.table_name || '').filter(Boolean),\n );\n } catch (error) {\n throw new LiveSchemaParityError(\n `Could not list tables in the live database: ${describeError(error)}`,\n { cause: error },\n );\n }\n}\n\nasync function readLiveColumns(\n db: DatabaseInterface,\n tableName: string,\n): Promise<Map<string, LiveColumn>> {\n if (typeof db.getTableSchema !== 'function') {\n throw new LiveSchemaParityError(\n 'The configured database adapter cannot describe tables (`getTableSchema` is unavailable), so live-schema parity cannot be verified.',\n );\n }\n\n let schema: Awaited<\n ReturnType<NonNullable<DatabaseInterface['getTableSchema']>>\n >;\n try {\n schema = await db.getTableSchema(tableName);\n } catch (error) {\n throw new LiveSchemaParityError(\n `Could not read the live schema of \\`${tableName}\\`: ${describeError(error)}`,\n { cause: error },\n );\n }\n\n const columns = new Map<string, LiveColumn>();\n for (const [name, column] of Object.entries(schema?.columns ?? {})) {\n columns.set(name, {\n name,\n type: String(column?.type ?? ''),\n notNull: column?.notNull === true,\n primaryKey: column?.primaryKey === true,\n hasDefault:\n column?.defaultValue !== undefined && column?.defaultValue !== null,\n });\n }\n return columns;\n}\n\ninterface IndexCatalog {\n forTable(tableName: string): Promise<LiveIndex[]>;\n}\n\n/**\n * Build an index reader for the engine, or `null` when index metadata cannot\n * be read. A null catalog disables every index-class check rather than\n * reporting indexes as missing on an engine we cannot introspect.\n */\nasync function readIndexCatalog(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n): Promise<IndexCatalog | null> {\n if (engine === 'postgres') {\n return readPostgresIndexCatalog(db);\n }\n if (engine === 'sqlite') {\n return { forTable: (tableName) => readSqliteIndexes(db, tableName) };\n }\n return readDuckDbIndexCatalog(db);\n}\n\nasync function readPostgresIndexCatalog(\n db: DatabaseInterface,\n): Promise<IndexCatalog> {\n // One catalog pass for the whole schema: `pg_index` carries uniqueness,\n // primary-key ownership and — unlike `pg_indexes` — `indisvalid`, which is\n // the only way to see an index left INVALID by a failed CONCURRENTLY build.\n const sql = `\n SELECT t.relname AS table_name,\n c.relname AS index_name,\n i.indisunique AS is_unique,\n i.indisprimary AS is_primary,\n i.indisvalid AS is_valid,\n (i.indpred IS NOT NULL) AS is_partial,\n pg_get_indexdef(i.indexrelid) AS index_def\n FROM pg_index i\n JOIN pg_class c ON c.oid = i.indexrelid\n JOIN pg_class t ON t.oid = i.indrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE n.nspname = 'public'`;\n\n let rows: Record<string, unknown>[];\n try {\n const result = await db.query(sql);\n rows = (result?.rows ?? []) as Record<string, unknown>[];\n } catch (error) {\n throw new LiveSchemaParityError(\n `Could not read the PostgreSQL index catalog: ${describeError(error)}`,\n { cause: error },\n );\n }\n\n const byTable = new Map<string, LiveIndex[]>();\n for (const row of rows) {\n const tableName = String(row.table_name ?? '');\n if (!tableName) continue;\n const bucket = byTable.get(tableName) ?? [];\n bucket.push({\n name: String(row.index_name ?? ''),\n columns: parseIndexDefColumns(String(row.index_def ?? '')),\n unique: toBoolean(row.is_unique),\n primary: toBoolean(row.is_primary),\n valid: row.is_valid === undefined ? true : toBoolean(row.is_valid),\n partial: toBoolean(row.is_partial),\n });\n byTable.set(tableName, bucket);\n }\n\n return { forTable: async (tableName) => byTable.get(tableName) ?? [] };\n}\n\n/**\n * Extract the indexed column list from a `CREATE INDEX` definition.\n *\n * Expression components (`lower(name)`, `(meta ->> 'x')`) are kept verbatim so\n * they never collide with a plain column of the same name; ordering is\n * preserved because leading-column coverage is what the policy checks.\n */\nexport function parseIndexDefColumns(indexDef: string): string[] {\n const open = indexDef.indexOf('(');\n if (open === -1) return [];\n\n let depth = 0;\n let end = -1;\n for (let i = open; i < indexDef.length; i++) {\n if (indexDef[i] === '(') depth++;\n else if (indexDef[i] === ')') {\n depth--;\n if (depth === 0) {\n end = i;\n break;\n }\n }\n }\n if (end === -1) return [];\n\n const body = indexDef.slice(open + 1, end);\n const parts: string[] = [];\n let current = '';\n depth = 0;\n for (const char of body) {\n if (char === '(') depth++;\n if (char === ')') depth--;\n if (char === ',' && depth === 0) {\n parts.push(current);\n current = '';\n continue;\n }\n current += char;\n }\n parts.push(current);\n\n return parts\n .map((part) =>\n part\n .trim()\n // Drop operator classes and per-column modifiers PostgreSQL renders.\n .replace(/\\s+(ASC|DESC|NULLS\\s+(FIRST|LAST)|[a-z_]+_ops)\\b/gi, '')\n .trim()\n .replace(/^\"(.*)\"$/, '$1'),\n )\n .filter((part) => part.length > 0);\n}\n\nasync function readSqliteIndexes(\n db: DatabaseInterface,\n tableName: string,\n): Promise<LiveIndex[]> {\n // `pragma_index_list` — unlike `getTableSchema()` — also reports the\n // implicit indexes SQLite creates for inline UNIQUE/PRIMARY KEY table\n // constraints (`sqlite_autoindex_*`), which is exactly where system-table\n // uniqueness lives.\n let listRows: Record<string, unknown>[];\n try {\n const result = await db.query(\n `SELECT * FROM pragma_index_list(${quoteLiteral(tableName)})`,\n );\n listRows = (result?.rows ?? []) as Record<string, unknown>[];\n } catch (error) {\n throw new LiveSchemaParityError(\n `Could not read SQLite indexes for \\`${tableName}\\`: ${describeError(error)}`,\n { cause: error },\n );\n }\n\n const indexes: LiveIndex[] = [];\n for (const row of listRows) {\n const name = String(row.name ?? '');\n if (!name) continue;\n\n const infoResult = await db.query(\n `SELECT * FROM pragma_index_info(${quoteLiteral(name)})`,\n );\n const infoRows = (infoResult?.rows ?? []) as Record<string, unknown>[];\n const columns = infoRows\n .slice()\n .sort((left, right) => Number(left.seqno ?? 0) - Number(right.seqno ?? 0))\n .map((infoRow) => (infoRow.name == null ? '' : String(infoRow.name)))\n .filter((column) => column.length > 0);\n\n indexes.push({\n name,\n columns,\n unique: toBoolean(row.unique),\n primary: String(row.origin ?? '') === 'pk',\n valid: true,\n partial: toBoolean(row.partial),\n });\n }\n\n return indexes;\n}\n\nasync function readDuckDbIndexCatalog(\n db: DatabaseInterface,\n): Promise<IndexCatalog | null> {\n // DuckDB (and the DuckDB-backed JSON adapter) expose declared indexes\n // through `duckdb_indexes()`; uniqueness constraints live in\n // `duckdb_constraints()`. Both are best-effort: if either catalog is\n // unavailable the caller skips index checks entirely.\n const byTable = new Map<string, LiveIndex[]>();\n try {\n const result = await db.query(\n `SELECT table_name, index_name, is_unique, sql FROM duckdb_indexes()`,\n );\n for (const row of (result?.rows ?? []) as Record<string, unknown>[]) {\n const tableName = String(row.table_name ?? '');\n if (!tableName) continue;\n const bucket = byTable.get(tableName) ?? [];\n bucket.push({\n name: String(row.index_name ?? ''),\n columns: parseIndexDefColumns(String(row.sql ?? '')),\n unique: toBoolean(row.is_unique),\n primary: false,\n valid: true,\n partial: false,\n });\n byTable.set(tableName, bucket);\n }\n } catch {\n return null;\n }\n\n try {\n const result = await db.query(\n `SELECT table_name, constraint_type, constraint_column_names FROM duckdb_constraints()`,\n );\n for (const row of (result?.rows ?? []) as Record<string, unknown>[]) {\n const constraintType = String(row.constraint_type ?? '').toUpperCase();\n if (constraintType !== 'UNIQUE' && constraintType !== 'PRIMARY KEY') {\n continue;\n }\n const tableName = String(row.table_name ?? '');\n const columns = normalizeColumnNameList(row.constraint_column_names);\n if (!tableName || columns.length === 0) continue;\n\n const bucket = byTable.get(tableName) ?? [];\n bucket.push({\n name: `${tableName}_${columns.join('_')}_${constraintType === 'UNIQUE' ? 'key' : 'pkey'}`,\n columns,\n unique: true,\n primary: constraintType === 'PRIMARY KEY',\n valid: true,\n partial: false,\n });\n byTable.set(tableName, bucket);\n }\n } catch {\n // Constraints are not optional metadata on this engine: DuckDB requires an\n // inline UNIQUE constraint for upsert, so uniqueness lives almost entirely\n // in `duckdb_constraints()` rather than in `duckdb_indexes()`. Without it,\n // `conflict_target_unindexed` and `unique_constraint_missing` would fire as\n // *errors* against a correct database. Degrade the whole catalog instead —\n // the caller then skips every index check and the report says\n // `indexIntrospection: 'unavailable'`.\n return null;\n }\n\n return { forTable: async (tableName) => byTable.get(tableName) ?? [] };\n}\n\nfunction normalizeColumnNameList(value: unknown): string[] {\n if (Array.isArray(value)) {\n return value.map((entry) => String(entry)).filter(Boolean);\n }\n if (typeof value === 'string') {\n return value\n .replace(/^[[{]|[\\]}]$/g, '')\n .split(',')\n .map((entry) => entry.trim().replace(/^[\"']|[\"']$/g, ''))\n .filter(Boolean);\n }\n return [];\n}\n\nfunction toBoolean(value: unknown): boolean {\n if (typeof value === 'boolean') return value;\n if (typeof value === 'number') return value !== 0;\n if (typeof value === 'string') {\n return value === '1' || value.toLowerCase() === 'true' || value === 't';\n }\n return false;\n}\n\nfunction quoteLiteral(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`;\n}\n\nfunction describeError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":";;;;;;;;;;;AAuLA,IAAa,wBAAb,cAA2C,MAAM;CAC/C,YAAY,SAAiB,SAA+B;EAC1D,MAAM,SAAS,OAAO;EACtB,KAAK,OAAO;CACd;AACF;;;;;;;;AASA,eAAsB,sBACpB,SACiC;CACjC,MAAM,EACJ,IACA,UAAU,CAAC,GACX,kBAAkB,CAAC,GACnB,sBAAsB,MACtB,YACA,oBAAoB,SAClB;CAEJ,MAAM,SAAS,cAAc,IAAI,UAAU;CAC3C,MAAM,WAAW,oBAAoB;EACnC;EACA;EACA;EACA;CACF,CAAC;CAED,MAAM,iBAAiB,MAAM,eAAe,IAAI,MAAM;CACtD,MAAM,eAAe,MAAM,iBAAiB,IAAI,MAAM;CACtD,MAAM,WAAgC,CAAC;CAEvC,IAAI,gBAAgB;CACpB,IAAI,gBAAgB;CAEpB,KAAK,MAAM,SAAS,UAAU;EAC5B,IAAI,CAAC,eAAe,IAAI,MAAM,IAAI,GAAG;GACnC;GACA,SAAS,KAAK;IACZ,MAAM;IACN,UAAU;IACV,OAAO,MAAM;IACb,QAAQ,MAAM;IACd,SAAS,WAAW,MAAM,KAAK;IAC/B,gBACE,MAAM,WAAW,WACb,mGACA;GACR,CAAC;GACD;EACF;EAEA;EACA,MAAM,cAAc,MAAM,gBAAgB,IAAI,MAAM,IAAI;EACxD,SAAS,KAAK,GAAG,eAAe,OAAO,aAAa,MAAM,CAAC;EAC3D,SAAS,KACP,GAAI,MAAM,wBAAwB,IAAI,QAAQ,OAAO,WAAW,CAClE;EAEA,IAAI,cAAc;GAChB,MAAM,cAAc,MAAM,aAAa,SAAS,MAAM,IAAI;GAC1D,SAAS,KAAK,GAAG,eAAe,OAAO,aAAa,WAAW,CAAC;EAClE;CACF;CAMA,MAAM,iBAAiB,MAAM,8BAC3B,IACA,2BAA2B,SAAS,EAAE,oBAAoB,CAAC,GAC3D,EAAE,WAAW,CACf;CACA,IAAI,eAAe,WACjB,KAAK,MAAM,SAAS,eAAe,QACjC,KAAK,MAAM,UAAU,MAAM,SAAS;EAClC,IAAI,OAAO,UAAU,WAAW;EAChC,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,OAAO;GACf,QAAQ,MAAM,MAAM,WAAW,QAAQ,IAAI,WAAW;GACtD,SACE,YAAY,MAAM,MAAM,GAAG,OAAO,OAAO,iBAAiB,OAAO,aAAa,wBACxD,MAAM,YAAY,EAAE;GAC5C,gBACE;GACF,SAAS;IACP,QAAQ,OAAO;IACf,UAAU;IACV,UAAU,MAAM;GAClB;EACF,CAAC;CACH;CAIJ,IAAI,mBAAmB;EACrB,MAAM,gBAAgB,IAAI,IAAI,SAAS,KAAK,UAAU,MAAM,IAAI,CAAC;EACjE,KAAK,MAAM,YAAY,gBAAgB;GACrC,IAAI,cAAc,IAAI,QAAQ,GAAG;GACjC,IAAI,oBAAoB,QAAQ,GAAG;GAGnC,IAAI,CAAC,uBAAuB,SAAS,WAAW,QAAQ,GAAG;GAI3D,MAAM,UAAU,sBAAsB,SAAS,QAAQ;GACvD,SAAS,KAAK;IACZ,MAAM;IACN,UAAU;IACV,OAAO;IACP,QAAQ,SAAS,WAAW,QAAQ,IAAI,WAAW;IACnD,SAAS,UACL,gBAAgB,SAAS,kEACzB,gBAAgB,SAAS;IAC7B,gBAAgB,UACZ,sEAAsE,SAAS,KAC/E;GACN,CAAC;EACH;CACF;CAEA,MAAM,SAA6C;EACjD,OAAO;EACP,SAAS;EACT,MAAM;CACR;CACA,KAAK,MAAM,WAAW,UACpB,OAAO,QAAQ,SAAS;CAG1B,OAAO;EACL;EACA;EACA;EACA,sBAAsB;EACtB,oBAAoB,eAAe,SAAS;EAC5C;EACA;EACA,IAAI,OAAO,UAAU;CACvB;AACF;AAMA,SAAS,oBAAoB,OAKT;CAClB,MAAM,EAAE,SAAS,iBAAiB,qBAAqB,WAAW;CAClE,MAAM,WAAW,eAAe,MAAM;CACtC,MAAM,SAA0B,CAAC;CAEjC,KAAK,MAAM,CAAC,KAAK,WAAW,OAAO,QAAQ,OAAO,GAAG;EACnD,MAAM,YAAY,QAAQ,aAAa;EACvC,IAAI,CAAC,WAAW;EAEhB,MAAM,UAA4B,OAAO,QAAQ,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,KACpE,CAAC,YAAY,aAAa;GACzB,MAAM;GACN,MAAM,SAAS,QAAQ,OAAO,IAAI;GAClC,YAAY,OAAO,eAAe;GAClC,SAAS,OAAO,YAAY,QAAQ,OAAO,eAAe;GAC1D,QAAQ,OAAO,WAAW;GAC1B,YAAY,OAAO,iBAAiB,KAAA;GACpC,WAAW,OAAO,aAAa,eAAe,OAAO;EACvD,EACF;EAEA,OAAO,KAAK;GACV,MAAM;GACN,QAAQ;GACR;GACA,UAAU,OAAO,WAAW,CAAC,EAAA,CAI1B,QAAQ,UAAU,CAAC,MAAM,aAAa,MAAM,SAAS,UAAU,KAAK,CAAC,CAAC,CACtE,KAAK,WAAW;IACf,MAAM,MAAM;IACZ,SAAS,MAAM;IACf,QAAQ,MAAM,WAAW;GAC3B,EAAE;GACJ,iBAAiB,sBAAsB,gBAAgB,cAAc,CAAC,CAAC;EACzE,CAAC;CACH;CAEA,IAAI,qBACF,KAAK,MAAM,SAAS,qBAAqB,MAAM,CAAC,CAAC,OAAO,GACtD,OAAO,KAAK;EACV,MAAM,MAAM;EACZ,QAAQ;EACR,SAAS,MAAM,QAAQ,KAAK,YAAY;GACtC,MAAM,OAAO;GACb,MAAM,OAAO;GACb,YAAY,OAAO;GACnB,SAAS,OAAO,WAAW,OAAO;GAClC,QAAQ,OAAO;GACf,YAAY,OAAO;EACrB,EAAE;EACF,SAAS,MAAM,QAAQ,KAAK,WAAW;GACrC,MAAM,MAAM;GACZ,SAAS,MAAM;GACf,QAAQ,MAAM;EAChB,EAAE;EAGF,iBAAiB,sBACf,MAAM,kBAAkB,KAAK,aAAa;GACxC;GACA,QAAQ,GAAG,MAAM,UAAU;EAC7B,EAAE,CACJ;CACF,CAAC;CAIL,OAAO;AACT;AAEA,SAAS,sBACP,SACuB;CACvB,MAAM,uBAAO,IAAI,IAAiC;CAClD,KAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,WAAW,OAAO,WAAW,CAAC,EAAA,CAAG,OAAO,OAAO;EACrD,IAAI,QAAQ,WAAW,GAAG;EAC1B,MAAM,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG;EACxC,IAAI,CAAC,KAAK,IAAI,GAAG,GACf,KAAK,IAAI,KAAK;GAAE;GAAS,QAAQ,OAAO;EAAO,CAAC;CAEpD;CACA,OAAO,CAAC,GAAG,KAAK,OAAO,CAAC;AAC1B;AAMA,SAAS,eACP,OACA,aACA,QACqB;CACrB,MAAM,WAAgC,CAAC;CACvC,MAAM,gBAAgB,IAAI,IAAI,MAAM,QAAQ,KAAK,WAAW,OAAO,IAAI,CAAC;CAExE,KAAK,MAAM,UAAU,MAAM,SAAS;EAClC,MAAM,OAAO,YAAY,IAAI,OAAO,IAAI;EACxC,IAAI,CAAC,MAAM;GACT,SAAS,KAAK;IACZ,MAAM;IACN,UAAU;IACV,OAAO,MAAM;IACb,QAAQ,OAAO;IACf,QAAQ,MAAM;IACd,SAAS,YAAY,MAAM,KAAK,GAAG,OAAO,KAAK;IAC/C,gBACE;IACF,SAAS,EAAE,cAAc,OAAO,KAAK;GACvC,CAAC;GACD;EACF;EAEA,IAAI,CAAC,mBAAmB,QAAQ,MAAM,MAAM,GAC1C,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,OAAO;GACf,QAAQ,MAAM;GACd,SAAS,YAAY,MAAM,KAAK,GAAG,OAAO,KAAK,UAAU,KAAK,KAAK,yCAAyC,OAAO,KAAK;GACxH,gBACE;GACF,SAAS;IAAE,UAAU,OAAO;IAAM,QAAQ,KAAK;GAAK;EACtD,CAAC;OACI;GACL,MAAM,iBAAiB,iBAAiB,OAAO,IAAI;GACnD,MAAM,eAAe,iBAAiB,KAAK,IAAI;GAgB/C,IACE,WAAW,cACX,mBAAmB,UACnB,iBAAiB,QAEjB,SAAS,KAAK;IACZ,MAAM;IACN,UAAU;IACV,OAAO,MAAM;IACb,QAAQ,OAAO;IACf,QAAQ,MAAM;IACd,SAAS,YAAY,MAAM,KAAK,GAAG,OAAO,KAAK,UAAU,KAAK,KAAK,yCAAyC,OAAO,KAAK;IACxH,gBACE;IACF,SAAS;KAAE,UAAU,OAAO;KAAM,QAAQ,KAAK;IAAK;GACtD,CAAC;QACI,IACL,WAAW,cACX,mBAAmB,UACnB,iBAAiB,UACjB,sBAAsB,MAAM,GAe5B,SAAS,KAAK;IACZ,MAAM;IACN,UAAU;IACV,OAAO,MAAM;IACb,QAAQ,OAAO;IACf,QAAQ,MAAM;IACd,SAAS,YAAY,MAAM,KAAK,GAAG,OAAO,KAAK,UAAU,KAAK,KAAK,yCAAyC,OAAO,KAAK;IACxH,gBACE;IACF,SAAS;KAAE,UAAU,OAAO;KAAM,QAAQ,KAAK;IAAK;GACtD,CAAC;QACI,KACJ,WAAW,cAAc,WAAW,aACrC,mBAAmB,UACnB,iBAAiB,QACjB;IAaA,MAAM,oBAAoB,iBAAiB,OAAO,MAAM,MAAM;IAC9D,MAAM,kBAAkB,iBAAiB,KAAK,MAAM,MAAM;IAC1D,IACE,qBACA,mBACA,sBAAsB,iBACtB;KACA,MAAM,WACJ,sBAAsB,YAAY,oBAAoB;KACxD,SAAS,KAAK;MACZ,MAAM;MAGN,UAAU;MACV,OAAO,MAAM;MACb,QAAQ,OAAO;MACf,QAAQ,MAAM;MACd,SAAS,YAAY,MAAM,KAAK,GAAG,OAAO,KAAK,UAAU,KAAK,KAAK,MAAM,gBAAgB,kDAAkD,OAAO,KAAK,MAAM,kBAAkB;MAC/K,gBAAgB,WACZ,+EACA;MACJ,SAAS;OAAE,UAAU,OAAO;OAAM,QAAQ,KAAK;MAAK;KACtD,CAAC;IACH;GACF;EACF;EAKA,IAAI,CAAC,OAAO,cAAc,OAAO,WAAW,CAAC,KAAK,SAChD,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,OAAO;GACf,QAAQ,MAAM;GACd,SAAS,YAAY,MAAM,KAAK,GAAG,OAAO,KAAK;GAC/C,gBACE;EACJ,CAAC;OACI,IAAI,CAAC,OAAO,WAAW,KAAK,WAAW,CAAC,KAAK,YAClD,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,OAAO;GACf,QAAQ,MAAM;GACd,SAAS,YAAY,MAAM,KAAK,GAAG,OAAO,KAAK;GAC/C,gBACE;EACJ,CAAC;CAEL;CAEA,KAAK,MAAM,QAAQ,YAAY,OAAO,GAAG;EACvC,IAAI,cAAc,IAAI,KAAK,IAAI,GAAG;EAElC,MAAM,gBAAgB,KAAK,WAAW,CAAC,KAAK;EAC5C,SAAS,KAAK;GACZ,MAAM;GACN,UAAU,gBAAgB,UAAU;GACpC,OAAO,MAAM;GACb,QAAQ,KAAK;GACb,QAAQ,MAAM;GACd,SAAS,gBACL,iBAAiB,MAAM,KAAK,GAAG,KAAK,KAAK,sGACzC,iBAAiB,MAAM,KAAK,GAAG,KAAK,KAAK;GAC7C,gBAAgB,gBACZ,6GACA;GACJ,SAAS,EAAE,MAAM,KAAK,KAAK;EAC7B,CAAC;CACH;CAEA,OAAO;AACT;;;;;;;;;;;;;;AAeA,SAAS,mBACP,UACA,MACA,QACS;CACT,MAAM,eAAe,iBAAiB,SAAS,IAAI;CACnD,MAAM,aAAa,iBAAiB,KAAK,IAAI;CAC7C,IAAI,iBAAiB,YAAY,OAAO;CAKxC,KAFG,iBAAiB,UAAU,eAAe,UAC1C,iBAAiB,UAAU,eAAe,WACzB,sBAAsB,QAAQ,GAChD,OAAO;CAMT,IAFG,iBAAiB,UAAU,eAAe,UAC1C,iBAAiB,UAAU,eAAe,QAC3B,OAAO;CAKzB,IAAI,WAAW;MAEV,iBAAiB,aAAa,eAAe,aAC7C,iBAAiB,aAAa,eAAe,WAC/B,OAAO;CAAA;CAG1B,OAAO;AACT;;;;;;;;;;;;;;;;;;AAmBA,SAAS,iBACP,MACA,QAC4B;CAC5B,MAAM,QAAQ,OAAO,QAAQ,EAAE,CAAC,CAC7B,YAAY,CAAC,CACb,KAAK,CAAC,CACN,QAAQ,+BAA+B,EAAE;CAC5C,IAAI,kBAAkB,KAAK,KAAK,GAAG,OAAO;CAC1C,IAAI,WAAW,YAAY,UAAU,SAAS,OAAO;CACrD,IAAI,2CAA2C,KAAK,KAAK,GAAG,OAAO;CACnE,OAAO;AACT;AAEA,SAAS,sBAAsB,QAAiC;CAC9D,OACE,OAAO,cACP,OAAO,cAAc,QACrB,OAAO,cAAc,gBACrB,OAAO,cAAc,qBACrB,OAAO,cAAc;AAEzB;;AAGA,SAAgB,iBAAiB,MAAsB;CACrD,MAAM,QAAQ,OAAO,QAAQ,EAAE,CAAC,CAC7B,YAAY,CAAC,CACb,KAAK,CAAC,CACN,QAAQ,oBAAoB,EAAE,CAAC,CAC/B,QAAQ,SAAS,EAAE,CAAC,CACpB,KAAK;CAER,IACE,0EAA0E,KACxE,KACF,GAEA,OAAO;CACT,IACE,qEAAqE,KACnE,KACF,GAEA,OAAO;CACT,IAAI,UAAU,QAAQ,OAAO;CAC7B,IACE,8EAA8E,KAC5E,KACF,GAEA,OAAO;CACT,IAAI,mBAAmB,KAAK,KAAK,GAAG,OAAO;CAC3C,IAAI,2CAA2C,KAAK,KAAK,GACvD,OAAO;CACT,IACE,+DAA+D,KAAK,KAAK,GAEzE,OAAO;CACT,IAAI,wBAAwB,KAAK,KAAK,GAAG,OAAO;CAChD,IAAI,iBAAiB,KAAK,KAAK,GAAG,OAAO;CAEzC,OAAO;AACT;;;;;;;AAeA,SAAS,oBACP,cACA,eAC4B;CAC5B,IAAI,iBAAiB,eAAe,OAAO;CAC3C,IAAI,iBAAiB,UAAU,kBAAkB,QAC/C,OAAO;CAET,OAAO;AACT;AAEA,eAAe,uBACb,IACA,OACA,QACkB;CAClB,MAAM,cAAc,gBAAgB,KAAK;CACzC,MAAM,eAAe,gBAAgB,MAAM;CAK3C,SAAQ,MAJa,GAAG,MACtB,4BAA4B,YAAY,SAC7B,aAAa,wBAAwB,aAAa,wBAC/D,EAAA,EACgB,MAAM,UAAU,KAAK;AACvC;AAEA,eAAe,4BACb,IACA,QACA,OACA,QACkB;CAClB,MAAM,cAAc,gBAAgB,KAAK;CACzC,MAAM,eAAe,gBAAgB,MAAM;CAC3C,MAAM,oBAAoB,GAAG,aAAa,wBAAwB,aAAa;CAE/E,IAAI,WAAW,YAAY;EAMzB,MAAM,OAAM,MALS,GAAG,MACtB,yCAAyC,YAAY,SAC1C,kBAAkB,YACf,aAAa,iBAAiB,uBAAuB,EACrE,EAAA,EACoB,OAAO;EAC3B,OAAO,OAAO,KAAK,iBAAiB,CAAC,MAAM;CAC7C;CAeA,MAAM,OAAM,MANS,GAAG,MACtB,yCAAyC,YAAY,SAC1C,kBAAkB,wBACH,aAAa,iCACnB,aAAa,mBAAmB,mCAAmC,GACzF,EAAA,EACoB,OAAO;CAC3B,OAAO,OAAO,KAAK,iBAAiB,CAAC,MAAM;AAC7C;AAEA,SAAS,8BACP,OACA,gBACA,YACmB;CACnB,MAAM,SAAS,WAAW,WAAW;CACrC,MAAM,gBAAgB,WAAW,KAAK,SAAS,KAAK,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI;CACvE,OAAO;EACL,MAAM;EACN,UAAU;EACV,OAAO,MAAM;EACb,QAAQ;EACR,QAAQ,MAAM;EACd,SAAS,SACL,YAAY,MAAM,KAAK,GAAG,eAAe,sDAAsD,MAAM,KAAK,GAAG,WAAW,GAAG,sEAAsE,WAAW,GAAG,4BAC/M,YAAY,MAAM,KAAK,GAAG,eAAe,kCAAkC,WAAW,OAAO,sDAAsD,cAAc;EACrK,gBAAgB,SACZ,yFAAyF,WAAW,GAAG,YAAY,eAAe,gBAAgB,WAAW,GAAG,OAChK;EACJ,SAAS,EAAE,WAAW;CACxB;AACF;;;;;;;;;;;AAYA,eAAe,wBACb,IACA,QACA,OACA,aAC8B;CAI9B,IAAI,WAAW,cAAc,WAAW,UAAU,OAAO,CAAC;CAE1D,MAAM,gBAAgB,IAAI,IAAI,MAAM,QAAQ,KAAK,WAAW,OAAO,IAAI,CAAC;CACxE,MAAM,eAAe,CAAC,GAAG,YAAY,OAAO,CAAC,CAAC,CAAC,QAC5C,SAAS,CAAC,cAAc,IAAI,KAAK,IAAI,CACxC;CACA,IAAI,aAAa,WAAW,GAAG,OAAO,CAAC;CAEvC,MAAM,WAAgC,CAAC;CAEvC,KAAK,MAAM,UAAU,MAAM,SAAS;EAElC,IAAI,CADS,YAAY,IAAI,OAAO,IAC/B,GAAM;EAEX,IAAI;EACJ,IAAI;GACF,kBAAkB,MAAM,uBACtB,IACA,MAAM,MACN,OAAO,IACT;EACF,QAAQ;GACN;EACF;EACA,IAAI,iBAAiB;EAErB,MAAM,eAAe,iBAAiB,OAAO,IAAI;EACjD,MAAM,aAAuB,CAAC;EAE9B,KAAK,MAAM,SAAS,cAAc;GAChC,MAAM,gBAAgB,oBACpB,cACA,iBAAiB,MAAM,IAAI,CAC7B;GACA,IAAI,CAAC,eAAe;GAEpB,IAAI;GACJ,IAAI;IACF,mBAAmB,MAAM,uBACvB,IACA,MAAM,MACN,MAAM,IACR;GACF,QAAQ;IACN;GACF;GACA,IAAI,CAAC,kBAAkB;GAEvB,IAAI,kBAAkB,gBAAgB;IACpC,IAAI;IACJ,IAAI;KACF,SAAS,MAAM,4BACb,IACA,QACA,MAAM,MACN,MAAM,IACR;IACF,QAAQ;KACN;IACF;IACA,IAAI,CAAC,QAAQ;GACf;GAEA,WAAW,KAAK,MAAM,IAAI;EAC5B;EAEA,IAAI,WAAW,WAAW,GAAG;EAC7B,WAAW,KAAK;EAChB,SAAS,KACP,8BAA8B,OAAO,OAAO,MAAM,UAAU,CAC9D;CACF;CAEA,OAAO;AACT;AAMA,SAAS,eACP,OACA,aACA,aACqB;CACrB,MAAM,WAAgC,CAAC;CACvC,MAAM,SAAS,IAAI,IAAI,YAAY,KAAK,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;CACtE,MAAM,aAAa,IAAI,IACrB,YAAY,KAAK,UAAU,eAAe,MAAM,SAAS,MAAM,MAAM,CAAC,CACxE;CAEA,KAAK,MAAM,SAAS,aAAa;EAC/B,IAAI,MAAM,OAAO;EACjB,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,SAAS,WAAW,MAAM,KAAK,UAAU,MAAM,KAAK;GACpD,gBACE;EACJ,CAAC;CACH;CAOA,KAAK,MAAM,SAAS,MAAM,SAAS;EACjC,IAAI,WAAW,IAAI,eAAe,MAAM,SAAS,MAAM,MAAM,CAAC,GAAG;EAGjE,IACE,MAAM,gBAAgB,MAAM,WAC1B,cAAc,MAAM,SAAS,OAAO,OAAO,CAC7C,GAEA;EAGF,MAAM,WAAW,OAAO,IAAI,MAAM,IAAI;EACtC,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,SAAS,WACL,WAAW,MAAM,KAAK,UAAU,MAAM,KAAK,6CAA6C,MAAM,QAAQ,KAAK,IAAI,EAAE,GAAG,MAAM,SAAS,YAAY,GAAG,UAAU,SAAS,QAAQ,KAAK,IAAI,EAAE,GAAG,SAAS,SAAS,YAAY,GAAG,KAC5N,oBAAoB,MAAM,KAAK,UAAU,MAAM,KAAK,MAAM,MAAM,QAAQ,KAAK,IAAI,EAAE;GACvF,gBACE,MAAM,WAAW,WACb,gFACA;EACR,CAAC;CACH;CAKA,KAAK,MAAM,UAAU,MAAM,iBAAiB;EAC1C,IAAI,OAAO,QAAQ,MAAM,WAAW,CAAC,YAAY,IAAI,MAAM,CAAC,GAE1D;EAGF,MAAM,aAAa,YAAY,QAAQ,UACrC,cAAc,MAAM,SAAS,OAAO,OAAO,CAC7C;EAIA,IAAI,WAAW,MAAM,UAAU,MAAM,UAAU,CAAC,MAAM,OAAO,GAC3D;EAGF,MAAM,gBAAgB,WAAW,MAC9B,UAAU,MAAM,UAAU,MAAM,OACnC;EACA,MAAM,YAAY,WAAW,MAAM,UAAU,CAAC,MAAM,MAAM;EAC1D,MAAM,aAAa,OAAO,QAAQ,KAAK,IAAI;EAC3C,MAAM,SAAS,OAAO,SAAS,iBAAiB,OAAO,OAAO,KAAK;EAEnE,IAAI,WAAW;GACb,SAAS,KAAK;IACZ,MAAM;IACN,UAAU;IACV,OAAO,MAAM;IACb,QAAQ,UAAU;IAClB,QAAQ,MAAM;IACd,SAAS,4BAA4B,MAAM,KAAK,MAAM,WAAW,GAAG,OAAO,kBAAkB,UAAU,KAAK;IAC5G,gBACE;GACJ,CAAC;GACD;EACF;EAEA,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,gBAAgB,cAAc,OAAO;GAC7C,QAAQ,MAAM;GACd,SAAS,gBACL,4BAA4B,MAAM,KAAK,MAAM,WAAW,GAAG,OAAO,yCAAyC,cAAc,KAAK,0CAC9H,4BAA4B,MAAM,KAAK,MAAM,WAAW,GAAG,OAAO;GACtE,gBACE;EACJ,CAAC;CACH;CAGA,KAAK,MAAM,UAAU,MAAM,SAAS;EAClC,IAAI,CAAC,OAAO,QAAQ;EACpB,IAAI,CAAC,YAAY,IAAI,OAAO,IAAI,GAAG;EASnC,IANe,YAAY,MACxB,UACC,MAAM,UACN,CAAC,MAAM,WACP,cAAc,MAAM,SAAS,CAAC,OAAO,IAAI,CAAC,CAE1C,GAAQ;EAEZ,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,OAAO;GACf,QAAQ,MAAM;GACd,SAAS,YAAY,MAAM,KAAK,GAAG,OAAO,KAAK;GAC/C,gBACE;EACJ,CAAC;CACH;CAKA,KAAK,MAAM,UAAU,MAAM,SAAS;EAClC,IAAI,CAAC,OAAO,WAAW;EACvB,IAAI,OAAO,cAAc,MAAM;EAC/B,IAAI,CAAC,YAAY,IAAI,OAAO,IAAI,GAAG;EAKnC,IAHgB,YAAY,MACzB,UAAU,MAAM,QAAQ,OAAO,OAAO,IAErC,GAAS;EAEb,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,OAAO;GACf,QAAQ,MAAM;GACd,SAAS,sBAAsB,MAAM,KAAK,GAAG,OAAO,KAAK,MAAM,OAAO,UAAU;GAChF,gBACE;GACF,SAAS,EAAE,eAAe,OAAO,UAAU;EAC7C,CAAC;CACH;CAaA,MAAM,gBAAgB,IAAI,IAAI,MAAM,QAAQ,KAAK,UAAU,MAAM,IAAI,CAAC;CACtE,MAAM,qBAAqB,IAAI,IAC7B,MAAM,QAAQ,KAAK,UAAU,eAAe,MAAM,SAAS,MAAM,MAAM,CAAC,CAC1E;CACA,KAAK,MAAM,SAAS,aAAa;EAC/B,IAAI,MAAM,SAAS;EAInB,IAAI,2BAA2B,MAAM,IAAI,GAAG;EAC5C,IAAI,cAAc,IAAI,MAAM,IAAI,GAAG;EACnC,IAAI,mBAAmB,IAAI,eAAe,MAAM,SAAS,MAAM,MAAM,CAAC,GACpE;EACF,IACE,MAAM,gBAAgB,MAAM,WAC1B,cAAc,MAAM,SAAS,OAAO,OAAO,CAC7C,GAEA;EAGF,SAAS,KAAK;GACZ,MAAM;GACN,UAAU;GACV,OAAO,MAAM;GACb,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,SAAS,gBAAgB,MAAM,KAAK,UAAU,MAAM,KAAK,MAAM,MAAM,QAAQ,KAAK,IAAI,KAAK,aAAa;GACxG,gBACE;GACF,SAAS;IAAE,QAAQ,MAAM;IAAQ,SAAS,MAAM;GAAQ;EAC1D,CAAC;CACH;CAEA,OAAO;AACT;AAEA,SAAS,2BAA2B,MAAuB;CACzD,OACE,KAAK,WAAW,mBAAmB,KACnC,KAAK,SAAS,OAAO,KACrB,KAAK,SAAS,MAAM;AAExB;AAEA,SAAS,eAAe,SAAmB,QAAyB;CAClE,OAAO,GAAG,QAAQ,KAAK,GAAG,EAAE,GAAG;AACjC;;;;;AAMA,SAAS,cAAc,MAAgB,OAA0B;CAC/D,IAAI,KAAK,WAAW,MAAM,QAAQ,OAAO;CACzC,MAAM,aAAa,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK;CAClC,MAAM,cAAc,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK;CACpC,OAAO,WAAW,OAAO,QAAQ,UAAU,WAAW,YAAY,MAAM;AAC1E;AAMA,SAAS,cACP,IACA,YACgB;CAChB,IAAI,OAAQ,GAAiC,gBAAgB,YAC3D,OAAO;CAET,MAAM,eAAe;CACrB,OAAO,aAAa,GAAG,OAAO,aAAa,QAAQ,OAAO,IAAI,UAAU;AAC1E;AAEA,SAAS,oBAAoB,MAAuB;CAClD,OACE,KAAK,WAAW,SAAS,KACzB,KAAK,WAAW,KAAK,KACrB,KAAK,WAAW,SAAS,KACzB,SAAS;AAEb;AAEA,eAAe,eACb,IACA,QACsB;CACtB,MAAM,MACJ,WAAW,aACP,mFACA;CAEN,IAAI;EAEF,MAAM,QAAQ,MADO,GAAG,MAAM,GAAG,EAAA,EACX,QAAQ,CAAC;EAI/B,OAAO,IAAI,IACT,KAAK,KAAK,QAAQ,IAAI,QAAQ,IAAI,cAAc,EAAE,CAAC,CAAC,OAAO,OAAO,CACpE;CACF,SAAS,OAAO;EACd,MAAM,IAAI,sBACR,+CAA+C,cAAc,KAAK,KAClE,EAAE,OAAO,MAAM,CACjB;CACF;AACF;AAEA,eAAe,gBACb,IACA,WACkC;CAClC,IAAI,OAAO,GAAG,mBAAmB,YAC/B,MAAM,IAAI,sBACR,qIACF;CAGF,IAAI;CAGJ,IAAI;EACF,SAAS,MAAM,GAAG,eAAe,SAAS;CAC5C,SAAS,OAAO;EACd,MAAM,IAAI,sBACR,uCAAuC,UAAU,MAAM,cAAc,KAAK,KAC1E,EAAE,OAAO,MAAM,CACjB;CACF;CAEA,MAAM,0BAAU,IAAI,IAAwB;CAC5C,KAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,QAAQ,WAAW,CAAC,CAAC,GAC/D,QAAQ,IAAI,MAAM;EAChB;EACA,MAAM,OAAO,QAAQ,QAAQ,EAAE;EAC/B,SAAS,QAAQ,YAAY;EAC7B,YAAY,QAAQ,eAAe;EACnC,YACE,QAAQ,iBAAiB,KAAA,KAAa,QAAQ,iBAAiB;CACnE,CAAC;CAEH,OAAO;AACT;;;;;;AAWA,eAAe,iBACb,IACA,QAC8B;CAC9B,IAAI,WAAW,YACb,OAAO,yBAAyB,EAAE;CAEpC,IAAI,WAAW,UACb,OAAO,EAAE,WAAW,cAAc,kBAAkB,IAAI,SAAS,EAAE;CAErE,OAAO,uBAAuB,EAAE;AAClC;AAEA,eAAe,yBACb,IACuB;CAIvB,MAAM,MAAM;;;;;;;;;;;;;CAcZ,IAAI;CACJ,IAAI;EAEF,QAAQ,MADa,GAAG,MAAM,GAAG,EAAA,EACjB,QAAQ,CAAC;CAC3B,SAAS,OAAO;EACd,MAAM,IAAI,sBACR,gDAAgD,cAAc,KAAK,KACnE,EAAE,OAAO,MAAM,CACjB;CACF;CAEA,MAAM,0BAAU,IAAI,IAAyB;CAC7C,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,YAAY,OAAO,IAAI,cAAc,EAAE;EAC7C,IAAI,CAAC,WAAW;EAChB,MAAM,SAAS,QAAQ,IAAI,SAAS,KAAK,CAAC;EAC1C,OAAO,KAAK;GACV,MAAM,OAAO,IAAI,cAAc,EAAE;GACjC,SAAS,qBAAqB,OAAO,IAAI,aAAa,EAAE,CAAC;GACzD,QAAQ,UAAU,IAAI,SAAS;GAC/B,SAAS,UAAU,IAAI,UAAU;GACjC,OAAO,IAAI,aAAa,KAAA,IAAY,OAAO,UAAU,IAAI,QAAQ;GACjE,SAAS,UAAU,IAAI,UAAU;EACnC,CAAC;EACD,QAAQ,IAAI,WAAW,MAAM;CAC/B;CAEA,OAAO,EAAE,UAAU,OAAO,cAAc,QAAQ,IAAI,SAAS,KAAK,CAAC,EAAE;AACvE;;;;;;;;AASA,SAAgB,qBAAqB,UAA4B;CAC/D,MAAM,OAAO,SAAS,QAAQ,GAAG;CACjC,IAAI,SAAS,IAAI,OAAO,CAAC;CAEzB,IAAI,QAAQ;CACZ,IAAI,MAAM;CACV,KAAK,IAAI,IAAI,MAAM,IAAI,SAAS,QAAQ,KACtC,IAAI,SAAS,OAAO,KAAK;MACpB,IAAI,SAAS,OAAO,KAAK;EAC5B;EACA,IAAI,UAAU,GAAG;GACf,MAAM;GACN;EACF;CACF;CAEF,IAAI,QAAQ,IAAI,OAAO,CAAC;CAExB,MAAM,OAAO,SAAS,MAAM,OAAO,GAAG,GAAG;CACzC,MAAM,QAAkB,CAAC;CACzB,IAAI,UAAU;CACd,QAAQ;CACR,KAAK,MAAM,QAAQ,MAAM;EACvB,IAAI,SAAS,KAAK;EAClB,IAAI,SAAS,KAAK;EAClB,IAAI,SAAS,OAAO,UAAU,GAAG;GAC/B,MAAM,KAAK,OAAO;GAClB,UAAU;GACV;EACF;EACA,WAAW;CACb;CACA,MAAM,KAAK,OAAO;CAElB,OAAO,MACJ,KAAK,SACJ,KACG,KAAK,CAAC,CAEN,QAAQ,sDAAsD,EAAE,CAAC,CACjE,KAAK,CAAC,CACN,QAAQ,YAAY,IAAI,CAC7B,CAAC,CACA,QAAQ,SAAS,KAAK,SAAS,CAAC;AACrC;AAEA,eAAe,kBACb,IACA,WACsB;CAKtB,IAAI;CACJ,IAAI;EAIF,YAAY,MAHS,GAAG,MACtB,mCAAmC,aAAa,SAAS,EAAE,EAC7D,EAAA,EACoB,QAAQ,CAAC;CAC/B,SAAS,OAAO;EACd,MAAM,IAAI,sBACR,uCAAuC,UAAU,MAAM,cAAc,KAAK,KAC1E,EAAE,OAAO,MAAM,CACjB;CACF;CAEA,MAAM,UAAuB,CAAC;CAC9B,KAAK,MAAM,OAAO,UAAU;EAC1B,MAAM,OAAO,OAAO,IAAI,QAAQ,EAAE;EAClC,IAAI,CAAC,MAAM;EAMX,MAAM,YADY,MAHO,GAAG,MAC1B,mCAAmC,aAAa,IAAI,EAAE,EACxD,EAAA,EAC8B,QAAQ,CAAC,EAAA,CAEpC,MAAM,CAAC,CACP,MAAM,MAAM,UAAU,OAAO,KAAK,SAAS,CAAC,IAAI,OAAO,MAAM,SAAS,CAAC,CAAC,CAAC,CACzE,KAAK,YAAa,QAAQ,QAAQ,OAAO,KAAK,OAAO,QAAQ,IAAI,CAAE,CAAC,CACpE,QAAQ,WAAW,OAAO,SAAS,CAAC;EAEvC,QAAQ,KAAK;GACX;GACA;GACA,QAAQ,UAAU,IAAI,MAAM;GAC5B,SAAS,OAAO,IAAI,UAAU,EAAE,MAAM;GACtC,OAAO;GACP,SAAS,UAAU,IAAI,OAAO;EAChC,CAAC;CACH;CAEA,OAAO;AACT;AAEA,eAAe,uBACb,IAC8B;CAK9B,MAAM,0BAAU,IAAI,IAAyB;CAC7C,IAAI;EACF,MAAM,SAAS,MAAM,GAAG,MACtB,qEACF;EACA,KAAK,MAAM,OAAQ,QAAQ,QAAQ,CAAC,GAAiC;GACnE,MAAM,YAAY,OAAO,IAAI,cAAc,EAAE;GAC7C,IAAI,CAAC,WAAW;GAChB,MAAM,SAAS,QAAQ,IAAI,SAAS,KAAK,CAAC;GAC1C,OAAO,KAAK;IACV,MAAM,OAAO,IAAI,cAAc,EAAE;IACjC,SAAS,qBAAqB,OAAO,IAAI,OAAO,EAAE,CAAC;IACnD,QAAQ,UAAU,IAAI,SAAS;IAC/B,SAAS;IACT,OAAO;IACP,SAAS;GACX,CAAC;GACD,QAAQ,IAAI,WAAW,MAAM;EAC/B;CACF,QAAQ;EACN,OAAO;CACT;CAEA,IAAI;EACF,MAAM,SAAS,MAAM,GAAG,MACtB,uFACF;EACA,KAAK,MAAM,OAAQ,QAAQ,QAAQ,CAAC,GAAiC;GACnE,MAAM,iBAAiB,OAAO,IAAI,mBAAmB,EAAE,CAAC,CAAC,YAAY;GACrE,IAAI,mBAAmB,YAAY,mBAAmB,eACpD;GAEF,MAAM,YAAY,OAAO,IAAI,cAAc,EAAE;GAC7C,MAAM,UAAU,wBAAwB,IAAI,uBAAuB;GACnE,IAAI,CAAC,aAAa,QAAQ,WAAW,GAAG;GAExC,MAAM,SAAS,QAAQ,IAAI,SAAS,KAAK,CAAC;GAC1C,OAAO,KAAK;IACV,MAAM,GAAG,UAAU,GAAG,QAAQ,KAAK,GAAG,EAAE,GAAG,mBAAmB,WAAW,QAAQ;IACjF;IACA,QAAQ;IACR,SAAS,mBAAmB;IAC5B,OAAO;IACP,SAAS;GACX,CAAC;GACD,QAAQ,IAAI,WAAW,MAAM;EAC/B;CACF,QAAQ;EAQN,OAAO;CACT;CAEA,OAAO,EAAE,UAAU,OAAO,cAAc,QAAQ,IAAI,SAAS,KAAK,CAAC,EAAE;AACvE;AAEA,SAAS,wBAAwB,OAA0B;CACzD,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,KAAK,UAAU,OAAO,KAAK,CAAC,CAAC,CAAC,OAAO,OAAO;CAE3D,IAAI,OAAO,UAAU,UACnB,OAAO,MACJ,QAAQ,iBAAiB,EAAE,CAAC,CAC5B,MAAM,GAAG,CAAC,CACV,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC,QAAQ,gBAAgB,EAAE,CAAC,CAAC,CACxD,OAAO,OAAO;CAEnB,OAAO,CAAC;AACV;AAEA,SAAS,UAAU,OAAyB;CAC1C,IAAI,OAAO,UAAU,WAAW,OAAO;CACvC,IAAI,OAAO,UAAU,UAAU,OAAO,UAAU;CAChD,IAAI,OAAO,UAAU,UACnB,OAAO,UAAU,OAAO,MAAM,YAAY,MAAM,UAAU,UAAU;CAEtE,OAAO;AACT;AAEA,SAAS,aAAa,OAAuB;CAC3C,OAAO,IAAI,MAAM,QAAQ,MAAM,IAAI,EAAE;AACvC;AAEA,SAAS,cAAc,OAAwB;CAC7C,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D"}
|
|
1
|
+
{"version":3,"file":"live-parity.js","names":[],"sources":["../../src/schema/live-parity.ts"],"sourcesContent":["/**\n * Live-schema parity check (#2368)\n *\n * Compares a **live** database against the shape the model layer assumes.\n * Three inputs define \"expected\":\n *\n * 1. **Declared table shapes** — the manifest schemas the caller passes in.\n * These give tables, columns, types, nullability, and declared indexes.\n * 2. **Hand-DDL system tables** — `_smrt_*` tables parsed out of\n * `src/system/schema.ts` (see `system-table-shapes.ts`). They never enter\n * the manifest, so nothing else has ever diffed them.\n * 3. **An expected-shape index policy that does not consult the manifest** —\n * every foreign-key / cross-package-ref / tenant column carries a leading\n * index, every declared upsert conflict target is backed by a matching\n * UNIQUE index, and every `unique: true` column is actually unique in the\n * live database.\n *\n * Point 3 is the reason this module exists rather than reusing the migration\n * differ: the differ compares the live database to the manifest, i.e. to the\n * artifact that dropped the index in the first place, which is why a database\n * missing 164 tenant-column indexes reported \"in sync\" (#2356).\n *\n * This module is read-only. It emits findings; it never repairs.\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport {\n collectIntegerWidthTargets,\n preflightIntegerWidthWidening,\n} from '../migrations/integer-width.js';\nimport { RETIRED_SYSTEM_TABLES } from '../system/schema.js';\n// The shared live-data column probes (#2874, consolidated for #2878): this\n// module's rename-pending detector used to duplicate `migrations/differ.ts`'s\n// copy of both probes exactly, which is why the #2874 regression had to be\n// fixed twice, in lockstep, in #2876.\nimport {\n columnsAllValuesUuidShapedBatch,\n columnsHaveNonEmptyValueBatch,\n} from './column-data-probes.js';\nimport { detectEngine, getDDLStrategy } from './ddl/index.js';\nimport type { DatabaseEngine } from './ddl/types.js';\nimport { getSystemTableShapes } from './system-table-shapes.js';\nimport type { ColumnDefinition, SchemaDefinition } from './types.js';\n\n/** How badly a finding breaks the deployment. */\nexport type LiveParitySeverity = 'error' | 'warning' | 'info';\n\n/** The class of drift a finding describes. */\nexport type LiveParityFindingKind =\n | 'missing_table'\n | 'extra_table'\n | 'missing_column'\n | 'extra_column'\n | 'column_type_drift'\n | 'column_nullability_drift'\n | 'missing_index'\n | 'extra_index'\n | 'unindexed_reference'\n | 'conflict_target_unindexed'\n | 'conflict_target_not_unique'\n | 'unique_constraint_missing'\n | 'invalid_index'\n /** Pre-#2373 PostgreSQL/DuckDB int4 column needing opt-in widening. */\n | 'legacy_integer_width'\n /**\n * A declared column exists live and holds no data while an undeclared\n * column of a compatible type does (#2752) — the shape a framework field\n * rename leaves behind when `db:migrate` adds the new column additively\n * and never moves the old data into it.\n */\n | 'rename_data_pending';\n\n/** One difference between the live database and the expected shape. */\nexport interface LiveParityFinding {\n kind: LiveParityFindingKind;\n severity: LiveParitySeverity;\n /** Table the finding is about. */\n table: string;\n /** Column or index the finding is about, when it is narrower than a table. */\n target?: string;\n /** Whether the table is an application table or a `_smrt_*` system table. */\n origin: ExpectedTableOrigin;\n message: string;\n recommendation: string;\n details?: Record<string, unknown>;\n}\n\n/** Aggregate result of one parity run. */\nexport interface LiveSchemaParityReport {\n engine: DatabaseEngine;\n /** Expected tables that exist in the live database and were compared. */\n tablesChecked: number;\n /** Expected tables that are absent from the live database. */\n tablesMissing: number;\n /** Whether `_smrt_*` system tables participated. */\n systemTablesIncluded: boolean;\n /**\n * `full` when index metadata (uniqueness, partial predicates, PostgreSQL\n * validity) could be read for this engine; `unavailable` when it could not,\n * in which case every index-class check is skipped rather than guessed.\n */\n indexIntrospection: 'full' | 'unavailable';\n findings: LiveParityFinding[];\n counts: Record<LiveParitySeverity, number>;\n /** True when no `error`-severity finding was produced. */\n ok: boolean;\n}\n\n/** A declared upsert conflict target for one table. */\nexport interface ConflictTargetInput {\n columns: string[];\n /** Where the target came from, e.g. a class name — used in messages. */\n source?: string;\n}\n\nexport interface LiveSchemaParityOptions {\n db: DatabaseInterface;\n /** Expected application table shapes, keyed by table name. */\n schemas?: Record<string, SchemaDefinition>;\n /** Declared upsert conflict targets, keyed by table name. */\n conflictTargets?: Record<string, ConflictTargetInput[]>;\n /** Include `_smrt_*` system tables (default true). */\n includeSystemTables?: boolean;\n /** Explicit engine hint for adapters whose URL is empty or ambiguous. */\n engineHint?: string;\n /** Report live tables no expected shape covers (default true). */\n reportExtraTables?: boolean;\n}\n\n/** Whether an expected table is an application table or a system table. */\nexport type ExpectedTableOrigin = 'application' | 'system';\n\ninterface ExpectedColumn {\n name: string;\n /** Engine-materialized type text. */\n type: string;\n primaryKey: boolean;\n notNull: boolean;\n unique: boolean;\n hasDefault: boolean;\n reference?: ColumnDefinition['referenceKind'];\n}\n\ninterface ExpectedIndex {\n name: string;\n columns: string[];\n unique: boolean;\n}\n\ninterface ExpectedTable {\n name: string;\n origin: ExpectedTableOrigin;\n columns: ExpectedColumn[];\n indexes: ExpectedIndex[];\n conflictTargets: ConflictTargetInput[];\n}\n\ninterface LiveColumn {\n name: string;\n type: string;\n notNull: boolean;\n primaryKey: boolean;\n hasDefault: boolean;\n}\n\ninterface LiveIndex {\n name: string;\n columns: string[];\n unique: boolean;\n /** PostgreSQL `indisvalid`; always true on engines without the concept. */\n valid: boolean;\n /** True for the index backing a primary key. */\n primary: boolean;\n /** True when the index is partial (has a WHERE predicate). */\n partial: boolean;\n}\n\n/**\n * Thrown when the live database cannot be introspected. Callers fail closed:\n * an unreadable database is never reported as \"in sync\".\n */\nexport class LiveSchemaParityError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = 'LiveSchemaParityError';\n }\n}\n\n/**\n * Compare a live database against the expected shape and return every\n * difference found.\n *\n * @throws {LiveSchemaParityError} when the database cannot be reached or the\n * adapter cannot describe a table. Connection problems must fail closed.\n */\nexport async function checkLiveSchemaParity(\n options: LiveSchemaParityOptions,\n): Promise<LiveSchemaParityReport> {\n const {\n db,\n schemas = {},\n conflictTargets = {},\n includeSystemTables = true,\n engineHint,\n reportExtraTables = true,\n } = options;\n\n const engine = resolveEngine(db, engineHint);\n const expected = buildExpectedTables({\n schemas,\n conflictTargets,\n includeSystemTables,\n engine,\n });\n\n const liveTableNames = await listLiveTables(db, engine);\n const indexCatalog = await readIndexCatalog(db, engine);\n const findings: LiveParityFinding[] = [];\n\n let tablesChecked = 0;\n let tablesMissing = 0;\n\n for (const table of expected) {\n if (!liveTableNames.has(table.name)) {\n tablesMissing++;\n findings.push({\n kind: 'missing_table',\n severity: 'error',\n table: table.name,\n origin: table.origin,\n message: `Table \\`${table.name}\\` is declared but does not exist in the live database.`,\n recommendation:\n table.origin === 'system'\n ? 'Initialize SMRT system tables against this database (they are created on framework bootstrap).'\n : 'Run `smrt db:migrate` to create the missing table.',\n });\n continue;\n }\n\n tablesChecked++;\n const liveColumns = await readLiveColumns(db, table.name);\n findings.push(...compareColumns(table, liveColumns, engine));\n findings.push(\n ...(await detectRenameDataPending(db, engine, table, liveColumns)),\n );\n\n if (indexCatalog) {\n const liveIndexes = await indexCatalog.forTable(table.name);\n findings.push(...compareIndexes(table, liveColumns, liveIndexes));\n }\n }\n\n // #2373 intentionally leaves int4/int8 equivalent to the ordinary differ\n // (and to structural parity) so fresh BIGINT DDL does not auto-rewrite a\n // production table. Surface the remaining 32-bit columns as advisory-only\n // findings here, with row counts for a maintenance-window estimate.\n const widthPreflight = await preflightIntegerWidthWidening(\n db,\n collectIntegerWidthTargets(schemas, { includeSystemTables }),\n { engineHint },\n );\n if (widthPreflight.supported) {\n for (const table of widthPreflight.tables) {\n for (const column of table.columns) {\n if (column.state !== 'pending') continue;\n findings.push({\n kind: 'legacy_integer_width',\n severity: 'warning',\n table: table.table,\n target: column.column,\n origin: table.table.startsWith('_smrt_') ? 'system' : 'application',\n message:\n `Column \\`${table.table}.${column.column}\\` is legacy \\`${column.declaredType}\\` ` +\n `instead of BIGINT (${table.rowCount ?? 0} row(s) in the table).`,\n recommendation:\n 'Schedule a maintenance window, run `smrt db:migrate-int8 --dry-run`, then run `smrt db:migrate-int8` after reviewing the table-rewrite plan.',\n details: {\n actual: column.declaredType,\n expected: 'BIGINT',\n rowCount: table.rowCount,\n },\n });\n }\n }\n }\n\n if (reportExtraTables) {\n const expectedNames = new Set(expected.map((table) => table.name));\n for (const liveName of liveTableNames) {\n if (expectedNames.has(liveName)) continue;\n if (isInternalTableName(liveName)) continue;\n // Without system tables in scope, every `_smrt_*` table would be\n // reported as unexplained; that is a scoping artifact, not drift.\n if (!includeSystemTables && liveName.startsWith('_smrt_')) continue;\n // A retired system table is not unexplained drift — the framework\n // deliberately stopped creating it and deliberately does not drop it\n // (issue #2376). Name the exact remedy instead of the generic advice.\n const retired = RETIRED_SYSTEM_TABLES.includes(liveName);\n findings.push({\n kind: 'extra_table',\n severity: 'info',\n table: liveName,\n origin: liveName.startsWith('_smrt_') ? 'system' : 'application',\n message: retired\n ? `Live table \\`${liveName}\\` is a retired SMRT system table; nothing reads or writes it.`\n : `Live table \\`${liveName}\\` is not covered by any declared schema.`,\n recommendation: retired\n ? `Drop it once you have confirmed it is unused: DROP TABLE IF EXISTS ${liveName};`\n : 'Confirm the table belongs to another application sharing this database, or remove it once its owning model is gone.',\n });\n }\n }\n\n const counts: Record<LiveParitySeverity, number> = {\n error: 0,\n warning: 0,\n info: 0,\n };\n for (const finding of findings) {\n counts[finding.severity]++;\n }\n\n return {\n engine,\n tablesChecked,\n tablesMissing,\n systemTablesIncluded: includeSystemTables,\n indexIntrospection: indexCatalog ? 'full' : 'unavailable',\n findings,\n counts,\n ok: counts.error === 0,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Expected shape\n// ---------------------------------------------------------------------------\n\nfunction buildExpectedTables(input: {\n schemas: Record<string, SchemaDefinition>;\n conflictTargets: Record<string, ConflictTargetInput[]>;\n includeSystemTables: boolean;\n engine: DatabaseEngine;\n}): ExpectedTable[] {\n const { schemas, conflictTargets, includeSystemTables, engine } = input;\n const strategy = getDDLStrategy(engine);\n const tables: ExpectedTable[] = [];\n\n for (const [key, schema] of Object.entries(schemas)) {\n const tableName = schema?.tableName || key;\n if (!tableName) continue;\n\n const columns: ExpectedColumn[] = Object.entries(schema.columns ?? {}).map(\n ([columnName, column]) => ({\n name: columnName,\n type: strategy.mapType(column.type),\n primaryKey: column.primaryKey === true,\n notNull: column.notNull === true || column.primaryKey === true,\n unique: column.unique === true,\n hasDefault: column.defaultValue !== undefined,\n reference: column.foreignKey ? 'foreignKey' : column.referenceKind,\n }),\n );\n\n tables.push({\n name: tableName,\n origin: 'application',\n columns,\n indexes: (schema.indexes ?? [])\n // Expression indexes (`@meta({ indexed: true })`) surface with empty\n // or null column lists in introspection, so a column-based comparison\n // can only produce false drift for them.\n .filter((index) => !index.jsonPath && (index.columns?.length ?? 0) > 0)\n .map((index) => ({\n name: index.name,\n columns: index.columns,\n unique: index.unique === true,\n })),\n conflictTargets: dedupeConflictTargets(conflictTargets[tableName] ?? []),\n });\n }\n\n if (includeSystemTables) {\n for (const shape of getSystemTableShapes(engine).values()) {\n tables.push({\n name: shape.tableName,\n origin: 'system',\n columns: shape.columns.map((column) => ({\n name: column.name,\n type: column.type,\n primaryKey: column.primaryKey,\n notNull: column.notNull || column.primaryKey,\n unique: column.unique,\n hasDefault: column.hasDefault,\n })),\n indexes: shape.indexes.map((index) => ({\n name: index.name,\n columns: index.columns,\n unique: index.unique,\n })),\n // Inline `UNIQUE (...)` constraints are what the framework upserts\n // against on system tables, so they are the conflict targets here.\n conflictTargets: dedupeConflictTargets(\n shape.uniqueConstraints.map((columns) => ({\n columns,\n source: `${shape.tableName} DDL`,\n })),\n ),\n });\n }\n }\n\n return tables;\n}\n\nfunction dedupeConflictTargets(\n targets: ConflictTargetInput[],\n): ConflictTargetInput[] {\n const seen = new Map<string, ConflictTargetInput>();\n for (const target of targets) {\n const columns = (target.columns ?? []).filter(Boolean);\n if (columns.length === 0) continue;\n const key = [...columns].sort().join(',');\n if (!seen.has(key)) {\n seen.set(key, { columns, source: target.source });\n }\n }\n return [...seen.values()];\n}\n\n// ---------------------------------------------------------------------------\n// Column comparison\n// ---------------------------------------------------------------------------\n\nfunction compareColumns(\n table: ExpectedTable,\n liveColumns: Map<string, LiveColumn>,\n engine: DatabaseEngine,\n): LiveParityFinding[] {\n const findings: LiveParityFinding[] = [];\n const expectedNames = new Set(table.columns.map((column) => column.name));\n\n for (const column of table.columns) {\n const live = liveColumns.get(column.name);\n if (!live) {\n findings.push({\n kind: 'missing_column',\n severity: 'error',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is declared but missing from the live database.`,\n recommendation:\n 'Run `smrt db:migrate` to add the column before application writes reach this table.',\n details: { expectedType: column.type },\n });\n continue;\n }\n\n if (!typesAreEquivalent(column, live, engine)) {\n findings.push({\n kind: 'column_type_drift',\n severity: 'error',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is \\`${live.type}\\` in the live database but declared \\`${column.type}\\`.`,\n recommendation:\n 'Reconcile the column type with `smrt db:migrate`, or repair the declaration if the live type is correct.',\n details: { expected: column.type, actual: live.type },\n });\n } else {\n const expectedBucket = normalizeSqlType(column.type);\n const actualBucket = normalizeSqlType(live.type);\n\n // #2772: `TEXT` (live) vs `JSON`/`JSONB` (declared) is tolerated above\n // (#1335) so it never becomes an `error`, but on a table SMRT itself\n // creates this is real, repairable drift — `db:migrate` can converge\n // it (see `differ.ts`'s shape-probed `type_upgrade`). Surface it as a\n // warning rather than staying invisible; the reverse direction (a\n // native `json`/`jsonb` column backed by a text-convention manifest\n // field) stays silent — that pairing is intentional, not drift.\n //\n // Engine-gated to `postgres` to match the differ's own repair gate\n // (`jsonUpgradeCandidate` in `differ.ts`, `this.engine === 'postgres'`\n // only): on DuckDB/SQLite there is no `type_upgrade` path for this\n // pairing, so flagging it here would be a permanent, unclearable\n // warning (review finding — the same class already fixed for #2770's\n // float-width check).\n if (\n engine === 'postgres' &&\n expectedBucket === 'JSON' &&\n actualBucket === 'TEXT'\n ) {\n findings.push({\n kind: 'column_type_drift',\n severity: 'warning',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is \\`${live.type}\\` in the live database but declared \\`${column.type}\\`.`,\n recommendation:\n 'Run `smrt db:migrate` to converge this column to native jsonb once its live values are confirmed valid JSON.',\n details: { expected: column.type, actual: live.type },\n });\n } else if (\n engine === 'postgres' &&\n expectedBucket === 'UUID' &&\n actualBucket === 'TEXT' &&\n isStructuralReference(column)\n ) {\n // #2772: the reverse of the uuid tolerance above already has a\n // framework repair path (`db:migrate-uuid`, #2608) — this differs\n // from the jsonb case in staying `info`: text/uuid interop on\n // structural columns is an intentional, long-supported compatibility\n // shape, not a bug, so this is a pointer to the optional convergence\n // path rather than a warning.\n //\n // Engine-gated to `postgres` to match `db:migrate-uuid`'s own repair\n // gate (`cli/src/commands/db-migrate-uuid.ts`, `runConvert = ... &&\n // isPostgres`): on DuckDB there is no native-uuid conversion path for\n // this pairing, so flagging it here would be a permanent, unclearable\n // finding (review finding — the same class already fixed for the\n // jsonb and float-width checks above).\n findings.push({\n kind: 'column_type_drift',\n severity: 'info',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is \\`${live.type}\\` in the live database but declared \\`${column.type}\\`; SMRT tolerates text/uuid for structural identifier/reference columns.`,\n recommendation:\n 'Run `smrt db:migrate-uuid` to converge this column to native uuid, or leave it as-is — this pairing is tolerated indefinitely.',\n details: { expected: column.type, actual: live.type },\n });\n } else if (\n (engine === 'postgres' || engine === 'duckdb') &&\n expectedBucket === 'REAL' &&\n actualBucket === 'REAL'\n ) {\n // #2770: REAL/DOUBLE PRECISION/DECIMAL/NUMERIC all normalize into\n // one 'REAL' bucket above, so single- vs double-precision float\n // drift never reaches the `!typesAreEquivalent` branch — the same\n // way int4-vs-int8 drift hides behind the shared 'INTEGER' bucket\n // (see `legacy_integer_width` below). Detect it here instead.\n //\n // Engine-gated to match the differ's own repair gate (`differ.ts`,\n // `this.engine === 'postgres' || this.engine === 'duckdb'`): SQLite\n // stores every real as an 8-byte double regardless of the declared\n // type name, so there is no narrowing and no `type_upgrade` path —\n // flagging it there would be a permanent, unclearable warning\n // (review finding).\n const expectedPrecision = floatPrecisionOf(column.type, engine);\n const actualPrecision = floatPrecisionOf(live.type, engine);\n if (\n expectedPrecision &&\n actualPrecision &&\n expectedPrecision !== actualPrecision\n ) {\n const widening =\n expectedPrecision === 'double' && actualPrecision === 'single';\n findings.push({\n kind: 'column_type_drift',\n // Consistent with the other width findings (`legacy_integer_width`\n // below): a maintenance concern to schedule, not a broken write path.\n severity: 'warning',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is \\`${live.type}\\` (${actualPrecision}-precision) in the live database but declared \\`${column.type}\\` (${expectedPrecision}-precision).`,\n recommendation: widening\n ? 'Run `smrt db:migrate` to widen this column to double precision (lossless).'\n : 'Narrowing to single precision can lose data; confirm the narrower declaration is intentional before repairing it manually.',\n details: { expected: column.type, actual: live.type },\n });\n }\n }\n }\n\n // A declared-NOT NULL column that is nullable live silently accepts rows\n // the model layer believes cannot exist; the reverse breaks every insert\n // that omits the column, which is the worse failure.\n if (!column.primaryKey && column.notNull && !live.notNull) {\n findings.push({\n kind: 'column_nullability_drift',\n severity: 'warning',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is declared NOT NULL but is nullable in the live database.`,\n recommendation:\n 'Backfill any NULL values and add the NOT NULL constraint, or relax the declaration.',\n });\n } else if (!column.notNull && live.notNull && !live.hasDefault) {\n findings.push({\n kind: 'column_nullability_drift',\n severity: 'error',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is NOT NULL in the live database with no default, but is not declared NOT NULL. Inserts that omit it will fail.`,\n recommendation:\n 'Drop the orphan NOT NULL constraint, give the column a default, or declare the column required.',\n });\n }\n }\n\n for (const live of liveColumns.values()) {\n if (expectedNames.has(live.name)) continue;\n\n const breaksInserts = live.notNull && !live.hasDefault;\n findings.push({\n kind: 'extra_column',\n severity: breaksInserts ? 'error' : 'info',\n table: table.name,\n target: live.name,\n origin: table.origin,\n message: breaksInserts\n ? `Live column \\`${table.name}.${live.name}\\` is undeclared, NOT NULL, and has no default — every framework insert into this table will fail.`\n : `Live column \\`${table.name}.${live.name}\\` is not declared by the current schema.`,\n recommendation: breaksInserts\n ? 'Drop the orphan column or give it a default; this is the residue of a renamed or removed required field.'\n : 'Confirm the column is intentional legacy data, or drop it once nothing reads it.',\n details: { type: live.type },\n });\n }\n\n return findings;\n}\n\n/**\n * Whether a live column type satisfies its declaration.\n *\n * Two tolerances match the model layer's actual conventions, and mirror the\n * migration differ so the two tools never disagree:\n *\n * - `TEXT` ↔ `UUID` for structural identifier/reference columns, because\n * SQLite has no uuid type and older PostgreSQL deployments legitimately\n * still carry text ids (R11).\n * - `TEXT` ↔ `JSON`/`JSONB` in both directions, because SMRT serializes JSON\n * values into text columns and a native json column already holds exactly\n * that (#1335).\n */\nfunction typesAreEquivalent(\n expected: ExpectedColumn,\n live: LiveColumn,\n engine: DatabaseEngine,\n): boolean {\n const expectedType = normalizeSqlType(expected.type);\n const actualType = normalizeSqlType(live.type);\n if (expectedType === actualType) return true;\n\n const uuidTextPair =\n (expectedType === 'UUID' && actualType === 'TEXT') ||\n (expectedType === 'TEXT' && actualType === 'UUID');\n if (uuidTextPair && isStructuralReference(expected)) {\n return true;\n }\n\n const jsonTextPair =\n (expectedType === 'JSON' && actualType === 'TEXT') ||\n (expectedType === 'TEXT' && actualType === 'JSON');\n if (jsonTextPair) return true;\n\n // SQLite has dynamic typing: BOOLEAN/TIMESTAMP columns are declared with\n // affinities that pragma reports verbatim, and INTEGER/NUMERIC storage is\n // interchangeable for the values SMRT writes.\n if (engine === 'sqlite') {\n const numericPair =\n (expectedType === 'BOOLEAN' && actualType === 'INTEGER') ||\n (expectedType === 'INTEGER' && actualType === 'BOOLEAN');\n if (numericPair) return true;\n }\n\n return false;\n}\n\n/**\n * Single- vs double-precision float classification for #2770's float-width\n * drift detection. `null` for anything that isn't unambiguously one or the\n * other (DECIMAL/NUMERIC have no fixed binary width and are out of scope).\n *\n * Bare `FLOAT` is engine-ambiguous and must be classified per `engine`:\n * PostgreSQL's `information_schema` reports `float4`/`real` as `real` and\n * `float8`/`double precision` as `double precision` — `FLOAT` alone never\n * appears there, so treating it as double-precision was previously safe.\n * DuckDB is different: `information_schema.columns.data_type` normalizes\n * *both* spellings of its single-precision type (`REAL`, `FLOAT4`) to the\n * bare string `FLOAT`, and reports its double-precision type as `DOUBLE`\n * (never `FLOAT`). Classifying DuckDB's `FLOAT` as double-precision — as a\n * shared, engine-unaware regex previously did — made every converged DuckDB\n * `REAL` column permanently misreport as narrowing drift with no\n * `db:migrate` able to clear it (review finding on #2770).\n */\nfunction floatPrecisionOf(\n type: string,\n engine: DatabaseEngine,\n): 'single' | 'double' | null {\n const upper = String(type ?? '')\n .toUpperCase()\n .trim()\n .replace(/\\(\\s*\\d+(\\s*,\\s*\\d+)?\\s*\\)/g, '');\n if (/^(REAL|FLOAT4)$/.test(upper)) return 'single';\n if (engine === 'duckdb' && upper === 'FLOAT') return 'single';\n if (/^(FLOAT8|DOUBLE|DOUBLE PRECISION|FLOAT)$/.test(upper)) return 'double';\n return null;\n}\n\nfunction isStructuralReference(column: ExpectedColumn): boolean {\n return (\n column.primaryKey ||\n column.reference === 'id' ||\n column.reference === 'foreignKey' ||\n column.reference === 'crossPackageRef' ||\n column.reference === 'tenantId'\n );\n}\n\n/** Fold engine-specific type spellings into comparable buckets. */\nexport function normalizeSqlType(type: string): string {\n const upper = String(type ?? '')\n .toUpperCase()\n .trim()\n .replace(/\\(\\s*[^)]*\\s*\\)/g, '')\n .replace(/\\[\\]$/, '')\n .trim();\n\n if (\n /^(INTEGER|INT|INT2|INT4|INT8|BIGINT|SMALLINT|TINYINT|SERIAL|BIGSERIAL)$/.test(\n upper,\n )\n )\n return 'INTEGER';\n if (\n /^(TEXT|CLOB|STRING|VARCHAR|CHAR|CHARACTER|CHARACTER VARYING|NAME)$/.test(\n upper,\n )\n )\n return 'TEXT';\n if (upper === 'UUID') return 'UUID';\n if (\n /^(REAL|FLOAT|FLOAT4|FLOAT8|DOUBLE|DOUBLE PRECISION|DECIMAL|NUMERIC|NUMBER)$/.test(\n upper,\n )\n )\n return 'REAL';\n if (/^(BOOLEAN|BOOL)$/.test(upper)) return 'BOOLEAN';\n if (/^(TIMESTAMPTZ|TIMESTAMP WITH TIME ZONE)$/.test(upper))\n return 'TIMESTAMPTZ';\n if (\n /^(DATETIME|TIMESTAMP|TIMESTAMP WITHOUT TIME ZONE|DATE|TIME)$/.test(upper)\n )\n return 'TIMESTAMP';\n if (/^(BLOB|BINARY|BYTEA)$/.test(upper)) return 'BLOB';\n if (/^(JSON|JSONB)$/.test(upper)) return 'JSON';\n\n return upper;\n}\n\n// ---------------------------------------------------------------------------\n// Rename-data-pending detection (#2752)\n// ---------------------------------------------------------------------------\n\n/** How a candidate (undeclared, live) column's type relates to a declared column's. */\ntype RenameCompatibility = 'same-type' | 'text-to-uuid';\n\n/**\n * Whether an undeclared live column could be the pre-rename home of a\n * declared column's data. \"Compatible\" per #2752: identical normalized\n * type, or the undeclared column is `TEXT` while the declared column is\n * `UUID` (gated separately on data shape by the caller).\n */\nfunction renameCompatibility(\n declaredType: string,\n candidateType: string,\n): RenameCompatibility | null {\n if (declaredType === candidateType) return 'same-type';\n if (declaredType === 'UUID' && candidateType === 'TEXT') {\n return 'text-to-uuid';\n }\n return null;\n}\n\nfunction buildRenameDataPendingFinding(\n table: ExpectedTable,\n declaredColumn: string,\n candidates: string[],\n): LiveParityFinding {\n const single = candidates.length === 1;\n const candidateList = candidates.map((name) => `\\`${name}\\``).join(', ');\n return {\n kind: 'rename_data_pending',\n severity: 'warning',\n table: table.name,\n target: declaredColumn,\n origin: table.origin,\n message: single\n ? `Column \\`${table.name}.${declaredColumn}\\` is declared but empty, while undeclared column \\`${table.name}.${candidates[0]}\\` holds data of a compatible type. Data appears to still live in \\`${candidates[0]}\\` after a field rename.`\n : `Column \\`${table.name}.${declaredColumn}\\` is declared but empty, while ${candidates.length} undeclared columns hold data of a compatible type (${candidateList}). Data may still live in one of them after a field rename, but which one is ambiguous.`,\n recommendation: single\n ? `Run \\`smrt db:diff\\` for the suggested backfill SQL, review it, then copy data from \\`${candidates[0]}\\` into \\`${declaredColumn}\\` and drop \\`${candidates[0]}\\`.`\n : 'Identify which undeclared column actually holds the pre-rename data before backfilling; SMRT will not guess among multiple candidates.',\n details: { candidates },\n };\n}\n\n/**\n * Detect a pending rename backfill (#2752): a manifest-declared column that\n * exists live but holds no data, paired with an undeclared live column of a\n * compatible type that does hold data. This is the shape a framework field\n * rename leaves behind when `db:migrate` adds the new column additively and\n * never moves data into it, nor drops the old column.\n *\n * Advisory only (`warning`): this module never repairs, and when several\n * undeclared columns qualify it lists them all rather than guessing.\n */\nasync function detectRenameDataPending(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n table: ExpectedTable,\n liveColumns: Map<string, LiveColumn>,\n): Promise<LiveParityFinding[]> {\n // The row-level probes below need plain SQL this module already assumes\n // for these two engines; DuckDB/JSON are out of scope (matches the\n // PostgreSQL/SQLite advisory SQL `db:diff` emits for the same pair).\n if (engine !== 'postgres' && engine !== 'sqlite') return [];\n\n const expectedNames = new Set(table.columns.map((column) => column.name));\n const extraColumns = [...liveColumns.values()].filter(\n (live) => !expectedNames.has(live.name),\n );\n if (extraColumns.length === 0) return [];\n\n const declaredCandidates = table.columns.filter((column) =>\n liveColumns.has(column.name),\n );\n if (declaredCandidates.length === 0) return [];\n\n // #2874: two phases, mirroring the original code's own two gates (review\n // findings F3 and its residual on the third pass). Phase one probes only\n // the declared candidates. A table where every declared candidate already\n // holds data — the ordinary, healthy case — returns here without ever\n // touching an extra (orphan) column, exactly like the original\n // `if (declaredHasData) continue;` gate. Only when some declared\n // candidate is confirmed empty does phase two batch-probe the extra\n // columns type-compatible with *those* empty candidates — an extra column\n // whose type matches nothing (or only already-populated candidates) would\n // either never produce a finding or never even be reachable, so probing\n // it would force a full-table scan (the `LIMIT 1` subquery never finds a\n // qualifying row) for nothing. Each phase is one round trip via\n // {@link columnsHaveNonEmptyValueBatch}, which falls back to isolated\n // per-column probes on a batch failure rather than discarding the whole\n // table (#2874 review finding F2). A column absent from a map below means\n // \"could not be probed\", not \"confirmed empty\".\n const declaredHasData = await columnsHaveNonEmptyValueBatch(\n db,\n table.name,\n declaredCandidates.map((column) => column.name),\n );\n const emptyDeclaredCandidates = declaredCandidates.filter(\n (column) => !(declaredHasData.get(column.name) ?? true),\n );\n if (emptyDeclaredCandidates.length === 0) return [];\n\n // Only probe an extra column that is type-compatible with at least one\n // *empty* declared candidate — restores the original per-column code's\n // compatibility gate, narrowed to the columns phase one actually found\n // empty. Costs no extra round trip: every type involved is already known\n // from `table.columns`/`liveColumns`, not the live data.\n const compatibleExtraColumns = extraColumns.filter((extra) => {\n const extraNormalized = normalizeSqlType(extra.type);\n return emptyDeclaredCandidates.some((column) =>\n renameCompatibility(normalizeSqlType(column.type), extraNormalized),\n );\n });\n if (compatibleExtraColumns.length === 0) return [];\n\n const extraHasData = await columnsHaveNonEmptyValueBatch(\n db,\n table.name,\n compatibleExtraColumns.map((extra) => extra.name),\n );\n const hasData = new Map([...declaredHasData, ...extraHasData]);\n\n type PendingCandidate = {\n declaredName: string;\n extraName: string;\n requiresShapeCheck: boolean;\n };\n const pending: PendingCandidate[] = [];\n const shapeCheckExtras = new Set<string>();\n\n for (const column of table.columns) {\n const live = liveColumns.get(column.name);\n if (!live) continue; // missing_column already covers this\n // Absent from the map means the probe could not run for this column\n // (#2874 review finding F2); default to \"has data\" so it is skipped,\n // matching the original per-column `catch { continue; }`.\n if (hasData.get(column.name) ?? true) continue;\n\n const declaredType = normalizeSqlType(column.type);\n\n for (const extra of compatibleExtraColumns) {\n const compatibility = renameCompatibility(\n declaredType,\n normalizeSqlType(extra.type),\n );\n if (!compatibility) continue;\n // Absent here defaults to \"no data\": this specific candidate is\n // excluded without affecting any other extra or declared column\n // (#2874 review finding F2).\n if (!(hasData.get(extra.name) ?? false)) continue;\n\n const requiresShapeCheck = compatibility === 'text-to-uuid';\n if (requiresShapeCheck) shapeCheckExtras.add(extra.name);\n pending.push({\n declaredName: column.name,\n extraName: extra.name,\n requiresShapeCheck,\n });\n }\n }\n\n // `columnsAllValuesUuidShapedBatch` falls back to isolated per-column\n // probes on a batch failure and never throws; a column absent from the\n // result defaults to \"not shaped\" below, excluding just that candidate.\n const shaped: Map<string, boolean> =\n shapeCheckExtras.size > 0\n ? await columnsAllValuesUuidShapedBatch(db, engine, table.name, [\n ...shapeCheckExtras,\n ])\n : new Map();\n\n const candidatesByColumn = new Map<string, string[]>();\n for (const candidate of pending) {\n if (\n candidate.requiresShapeCheck &&\n !(shaped.get(candidate.extraName) ?? false)\n ) {\n continue;\n }\n const list = candidatesByColumn.get(candidate.declaredName) ?? [];\n list.push(candidate.extraName);\n candidatesByColumn.set(candidate.declaredName, list);\n }\n\n const findings: LiveParityFinding[] = [];\n for (const [declaredName, candidates] of candidatesByColumn) {\n if (candidates.length === 0) continue;\n candidates.sort();\n findings.push(\n buildRenameDataPendingFinding(table, declaredName, candidates),\n );\n }\n\n return findings;\n}\n\n// ---------------------------------------------------------------------------\n// Index comparison — the manifest-independent policy lives here\n// ---------------------------------------------------------------------------\n\nfunction compareIndexes(\n table: ExpectedTable,\n liveColumns: Map<string, LiveColumn>,\n liveIndexes: LiveIndex[],\n): LiveParityFinding[] {\n const findings: LiveParityFinding[] = [];\n const byName = new Map(liveIndexes.map((index) => [index.name, index]));\n const signatures = new Set(\n liveIndexes.map((index) => indexSignature(index.columns, index.unique)),\n );\n\n for (const index of liveIndexes) {\n if (index.valid) continue;\n findings.push({\n kind: 'invalid_index',\n severity: 'error',\n table: table.name,\n target: index.name,\n origin: table.origin,\n message: `Index \\`${index.name}\\` on \\`${table.name}\\` is INVALID — PostgreSQL will not use it and a unique index in this state enforces nothing.`,\n recommendation:\n 'Drop and rebuild it (`REINDEX INDEX CONCURRENTLY`), then re-run the parity check.',\n });\n }\n\n // 1. Declared indexes the live database does not carry in that shape.\n // Matching is by column/uniqueness signature rather than by name so an\n // equivalent index under a different name is not reported as missing\n // (#741), and so a same-name index whose uniqueness or columns drifted\n // still is.\n for (const index of table.indexes) {\n if (signatures.has(indexSignature(index.columns, index.unique))) continue;\n // Conflict-target coverage is reported below with a message that explains\n // the actual consequence; do not report the same index twice.\n if (\n table.conflictTargets.some((target) =>\n sameColumnSet(index.columns, target.columns),\n )\n ) {\n continue;\n }\n\n const sameName = byName.get(index.name);\n findings.push({\n kind: 'missing_index',\n severity: 'warning',\n table: table.name,\n target: index.name,\n origin: table.origin,\n message: sameName\n ? `Index \\`${index.name}\\` on \\`${table.name}\\` exists but its shape drifted: declared (${index.columns.join(', ')})${index.unique ? ' UNIQUE' : ''}, live (${sameName.columns.join(', ')})${sameName.unique ? ' UNIQUE' : ''}.`\n : `Declared index \\`${index.name}\\` on \\`${table.name}\\` (${index.columns.join(', ')}) is missing from the live database.`,\n recommendation:\n table.origin === 'system'\n ? 'Re-run SMRT system-table initialization to create the missing system index.'\n : 'Run `smrt db:migrate` to create the missing index.',\n });\n }\n\n // 2. Conflict targets. An upsert whose conflict target has no matching\n // UNIQUE index either errors outright (PostgreSQL) or silently inserts\n // duplicates, so this is the one index class that is an error.\n for (const target of table.conflictTargets) {\n if (target.columns.some((column) => !liveColumns.has(column))) {\n // A missing column is already reported; do not double-report it here.\n continue;\n }\n\n const candidates = liveIndexes.filter((index) =>\n sameColumnSet(index.columns, target.columns),\n );\n // A *partial* unique index cannot arbitrate a plain `ON CONFLICT (cols)`:\n // PostgreSQL only infers an index whose predicate the statement repeats,\n // and on other engines it simply does not constrain the excluded rows.\n if (candidates.some((index) => index.unique && !index.partial)) {\n continue;\n }\n\n const partialUnique = candidates.find(\n (index) => index.unique && index.partial,\n );\n const nonUnique = candidates.find((index) => !index.unique);\n const columnList = target.columns.join(', ');\n const suffix = target.source ? ` (declared by ${target.source})` : '';\n\n if (nonUnique) {\n findings.push({\n kind: 'conflict_target_not_unique',\n severity: 'error',\n table: table.name,\n target: nonUnique.name,\n origin: table.origin,\n message: `Upsert conflict target \\`${table.name}\\` (${columnList})${suffix} is backed by \\`${nonUnique.name}\\`, which is not UNIQUE.`,\n recommendation:\n 'Recreate the index as UNIQUE (`smrt db:migrate`); until then upserts insert duplicates instead of updating.',\n });\n continue;\n }\n\n findings.push({\n kind: 'conflict_target_unindexed',\n severity: 'error',\n table: table.name,\n target: partialUnique ? partialUnique.name : columnList,\n origin: table.origin,\n message: partialUnique\n ? `Upsert conflict target \\`${table.name}\\` (${columnList})${suffix} is backed only by the partial index \\`${partialUnique.name}\\`, which cannot arbitrate the upsert.`\n : `Upsert conflict target \\`${table.name}\\` (${columnList})${suffix} has no matching UNIQUE index in the live database.`,\n recommendation:\n 'Create a full UNIQUE index over exactly these columns (`smrt db:migrate`); PostgreSQL rejects the upsert outright and other engines duplicate rows.',\n });\n }\n\n // 3. Columns declared unique that are not actually unique live.\n for (const column of table.columns) {\n if (!column.unique) continue;\n if (!liveColumns.has(column.name)) continue;\n // A partial unique index constrains only the rows its predicate selects,\n // so it does not make the column unique as declared.\n const unique = liveIndexes.some(\n (index) =>\n index.unique &&\n !index.partial &&\n sameColumnSet(index.columns, [column.name]),\n );\n if (unique) continue;\n\n findings.push({\n kind: 'unique_constraint_missing',\n severity: 'error',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Column \\`${table.name}.${column.name}\\` is declared unique but no UNIQUE index enforces it in the live database.`,\n recommendation:\n 'De-duplicate existing values and create the UNIQUE index; the declaration is currently decorative.',\n });\n }\n\n // 4. Manifest-independent coverage policy: every foreign-key, cross-package\n // reference and tenant column needs an index it *leads*, or every lookup\n // and every tenant-scoped list is a full scan (#2356).\n for (const column of table.columns) {\n if (!column.reference) continue;\n if (column.reference === 'id') continue;\n if (!liveColumns.has(column.name)) continue;\n\n const covered = liveIndexes.some(\n (index) => index.columns[0] === column.name,\n );\n if (covered) continue;\n\n findings.push({\n kind: 'unindexed_reference',\n severity: 'warning',\n table: table.name,\n target: column.name,\n origin: table.origin,\n message: `Reference column \\`${table.name}.${column.name}\\` (${column.reference}) leads no index in the live database.`,\n recommendation:\n 'Add an index led by this column; relationship loads, joins and tenant-scoped reads currently scan the whole table.',\n details: { referenceKind: column.reference },\n });\n }\n\n // 5. Live indexes nothing declares. Informational: an operator may have\n // hand-added them, and nothing will recreate them after a rebuild.\n //\n // An undeclared index whose lead column is a reference column\n // (tenantId, foreignKey, crossPackageRef) used to be silently exempted\n // here as deliberate policy. `migrations/differ.ts` disagreed and would\n // drop the same index under `--drop-indexes`, so an operator following\n // that command could remove something this check claimed to protect.\n // Decision (#2751): treat it as drift and report it — the recommendation\n // below already reads correctly (\"declare it ... or drop it if it is\n // obsolete\"), and an `info` finding costs nothing.\n const declaredNames = new Set(table.indexes.map((index) => index.name));\n const declaredSignatures = new Set(\n table.indexes.map((index) => indexSignature(index.columns, index.unique)),\n );\n for (const index of liveIndexes) {\n if (index.primary) continue;\n // Constraint-owned indexes (`sqlite_autoindex_*`, PostgreSQL `*_key` /\n // `*_pkey`) exist because of a table constraint, not because anyone\n // declared an index; reporting them as undeclared is pure noise.\n if (isConstraintOwnedIndexName(index.name)) continue;\n if (declaredNames.has(index.name)) continue;\n if (declaredSignatures.has(indexSignature(index.columns, index.unique)))\n continue;\n if (\n table.conflictTargets.some((target) =>\n sameColumnSet(index.columns, target.columns),\n )\n ) {\n continue;\n }\n\n findings.push({\n kind: 'extra_index',\n severity: 'info',\n table: table.name,\n target: index.name,\n origin: table.origin,\n message: `Live index \\`${index.name}\\` on \\`${table.name}\\` (${index.columns.join(', ') || 'expression'}) is not declared by the current schema.`,\n recommendation:\n 'Declare it so a rebuilt database keeps it, or drop it if it is obsolete.',\n details: { unique: index.unique, partial: index.partial },\n });\n }\n\n return findings;\n}\n\nfunction isConstraintOwnedIndexName(name: string): boolean {\n return (\n name.startsWith('sqlite_autoindex_') ||\n name.endsWith('_pkey') ||\n name.endsWith('_key')\n );\n}\n\nfunction indexSignature(columns: string[], unique: boolean): string {\n return `${columns.join(',')}:${unique}`;\n}\n\n/**\n * Conflict targets and unique constraints are order-insensitive: PostgreSQL\n * matches `ON CONFLICT (a, b)` against a unique index on `(b, a)`.\n */\nfunction sameColumnSet(left: string[], right: string[]): boolean {\n if (left.length !== right.length) return false;\n const sortedLeft = [...left].sort();\n const sortedRight = [...right].sort();\n return sortedLeft.every((column, index) => column === sortedRight[index]);\n}\n\n// ---------------------------------------------------------------------------\n// Live introspection\n// ---------------------------------------------------------------------------\n\nfunction resolveEngine(\n db: DatabaseInterface,\n engineHint?: string,\n): DatabaseEngine {\n if (typeof (db as { exportTable?: unknown }).exportTable === 'function') {\n return 'json';\n }\n const dbWithConfig = db as DatabaseInterface & { config?: { url?: string } };\n return detectEngine(db.url || dbWithConfig.config?.url || '', engineHint);\n}\n\nfunction isInternalTableName(name: string): boolean {\n return (\n name.startsWith('sqlite_') ||\n name.startsWith('pg_') ||\n name.startsWith('duckdb_') ||\n name === 'information_schema'\n );\n}\n\nasync function listLiveTables(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n): Promise<Set<string>> {\n const sql =\n engine === 'postgres'\n ? `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'`\n : `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;\n\n try {\n const result = await db.query(sql);\n const rows = (result?.rows ?? []) as {\n name?: string;\n table_name?: string;\n }[];\n return new Set(\n rows.map((row) => row.name || row.table_name || '').filter(Boolean),\n );\n } catch (error) {\n throw new LiveSchemaParityError(\n `Could not list tables in the live database: ${describeError(error)}`,\n { cause: error },\n );\n }\n}\n\nasync function readLiveColumns(\n db: DatabaseInterface,\n tableName: string,\n): Promise<Map<string, LiveColumn>> {\n if (typeof db.getTableSchema !== 'function') {\n throw new LiveSchemaParityError(\n 'The configured database adapter cannot describe tables (`getTableSchema` is unavailable), so live-schema parity cannot be verified.',\n );\n }\n\n let schema: Awaited<\n ReturnType<NonNullable<DatabaseInterface['getTableSchema']>>\n >;\n try {\n schema = await db.getTableSchema(tableName);\n } catch (error) {\n throw new LiveSchemaParityError(\n `Could not read the live schema of \\`${tableName}\\`: ${describeError(error)}`,\n { cause: error },\n );\n }\n\n const columns = new Map<string, LiveColumn>();\n for (const [name, column] of Object.entries(schema?.columns ?? {})) {\n columns.set(name, {\n name,\n type: String(column?.type ?? ''),\n notNull: column?.notNull === true,\n primaryKey: column?.primaryKey === true,\n hasDefault:\n column?.defaultValue !== undefined && column?.defaultValue !== null,\n });\n }\n return columns;\n}\n\ninterface IndexCatalog {\n forTable(tableName: string): Promise<LiveIndex[]>;\n}\n\n/**\n * Build an index reader for the engine, or `null` when index metadata cannot\n * be read. A null catalog disables every index-class check rather than\n * reporting indexes as missing on an engine we cannot introspect.\n */\nasync function readIndexCatalog(\n db: DatabaseInterface,\n engine: DatabaseEngine,\n): Promise<IndexCatalog | null> {\n if (engine === 'postgres') {\n return readPostgresIndexCatalog(db);\n }\n if (engine === 'sqlite') {\n return { forTable: (tableName) => readSqliteIndexes(db, tableName) };\n }\n return readDuckDbIndexCatalog(db);\n}\n\nasync function readPostgresIndexCatalog(\n db: DatabaseInterface,\n): Promise<IndexCatalog> {\n // One catalog pass for the whole schema: `pg_index` carries uniqueness,\n // primary-key ownership and — unlike `pg_indexes` — `indisvalid`, which is\n // the only way to see an index left INVALID by a failed CONCURRENTLY build.\n const sql = `\n SELECT t.relname AS table_name,\n c.relname AS index_name,\n i.indisunique AS is_unique,\n i.indisprimary AS is_primary,\n i.indisvalid AS is_valid,\n (i.indpred IS NOT NULL) AS is_partial,\n pg_get_indexdef(i.indexrelid) AS index_def\n FROM pg_index i\n JOIN pg_class c ON c.oid = i.indexrelid\n JOIN pg_class t ON t.oid = i.indrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE n.nspname = 'public'`;\n\n let rows: Record<string, unknown>[];\n try {\n const result = await db.query(sql);\n rows = (result?.rows ?? []) as Record<string, unknown>[];\n } catch (error) {\n throw new LiveSchemaParityError(\n `Could not read the PostgreSQL index catalog: ${describeError(error)}`,\n { cause: error },\n );\n }\n\n const byTable = new Map<string, LiveIndex[]>();\n for (const row of rows) {\n const tableName = String(row.table_name ?? '');\n if (!tableName) continue;\n const bucket = byTable.get(tableName) ?? [];\n bucket.push({\n name: String(row.index_name ?? ''),\n columns: parseIndexDefColumns(String(row.index_def ?? '')),\n unique: toBoolean(row.is_unique),\n primary: toBoolean(row.is_primary),\n valid: row.is_valid === undefined ? true : toBoolean(row.is_valid),\n partial: toBoolean(row.is_partial),\n });\n byTable.set(tableName, bucket);\n }\n\n return { forTable: async (tableName) => byTable.get(tableName) ?? [] };\n}\n\n/**\n * Extract the indexed column list from a `CREATE INDEX` definition.\n *\n * Expression components (`lower(name)`, `(meta ->> 'x')`) are kept verbatim so\n * they never collide with a plain column of the same name; ordering is\n * preserved because leading-column coverage is what the policy checks.\n */\nexport function parseIndexDefColumns(indexDef: string): string[] {\n const open = indexDef.indexOf('(');\n if (open === -1) return [];\n\n let depth = 0;\n let end = -1;\n for (let i = open; i < indexDef.length; i++) {\n if (indexDef[i] === '(') depth++;\n else if (indexDef[i] === ')') {\n depth--;\n if (depth === 0) {\n end = i;\n break;\n }\n }\n }\n if (end === -1) return [];\n\n const body = indexDef.slice(open + 1, end);\n const parts: string[] = [];\n let current = '';\n depth = 0;\n for (const char of body) {\n if (char === '(') depth++;\n if (char === ')') depth--;\n if (char === ',' && depth === 0) {\n parts.push(current);\n current = '';\n continue;\n }\n current += char;\n }\n parts.push(current);\n\n return parts\n .map((part) =>\n part\n .trim()\n // Drop operator classes and per-column modifiers PostgreSQL renders.\n .replace(/\\s+(ASC|DESC|NULLS\\s+(FIRST|LAST)|[a-z_]+_ops)\\b/gi, '')\n .trim()\n .replace(/^\"(.*)\"$/, '$1'),\n )\n .filter((part) => part.length > 0);\n}\n\nasync function readSqliteIndexes(\n db: DatabaseInterface,\n tableName: string,\n): Promise<LiveIndex[]> {\n // `pragma_index_list` — unlike `getTableSchema()` — also reports the\n // implicit indexes SQLite creates for inline UNIQUE/PRIMARY KEY table\n // constraints (`sqlite_autoindex_*`), which is exactly where system-table\n // uniqueness lives.\n let listRows: Record<string, unknown>[];\n try {\n const result = await db.query(\n `SELECT * FROM pragma_index_list(${quoteLiteral(tableName)})`,\n );\n listRows = (result?.rows ?? []) as Record<string, unknown>[];\n } catch (error) {\n throw new LiveSchemaParityError(\n `Could not read SQLite indexes for \\`${tableName}\\`: ${describeError(error)}`,\n { cause: error },\n );\n }\n\n const indexes: LiveIndex[] = [];\n for (const row of listRows) {\n const name = String(row.name ?? '');\n if (!name) continue;\n\n const infoResult = await db.query(\n `SELECT * FROM pragma_index_info(${quoteLiteral(name)})`,\n );\n const infoRows = (infoResult?.rows ?? []) as Record<string, unknown>[];\n const columns = infoRows\n .slice()\n .sort((left, right) => Number(left.seqno ?? 0) - Number(right.seqno ?? 0))\n .map((infoRow) => (infoRow.name == null ? '' : String(infoRow.name)))\n .filter((column) => column.length > 0);\n\n indexes.push({\n name,\n columns,\n unique: toBoolean(row.unique),\n primary: String(row.origin ?? '') === 'pk',\n valid: true,\n partial: toBoolean(row.partial),\n });\n }\n\n return indexes;\n}\n\nasync function readDuckDbIndexCatalog(\n db: DatabaseInterface,\n): Promise<IndexCatalog | null> {\n // DuckDB (and the DuckDB-backed JSON adapter) expose declared indexes\n // through `duckdb_indexes()`; uniqueness constraints live in\n // `duckdb_constraints()`. Both are best-effort: if either catalog is\n // unavailable the caller skips index checks entirely.\n const byTable = new Map<string, LiveIndex[]>();\n try {\n const result = await db.query(\n `SELECT table_name, index_name, is_unique, sql FROM duckdb_indexes()`,\n );\n for (const row of (result?.rows ?? []) as Record<string, unknown>[]) {\n const tableName = String(row.table_name ?? '');\n if (!tableName) continue;\n const bucket = byTable.get(tableName) ?? [];\n bucket.push({\n name: String(row.index_name ?? ''),\n columns: parseIndexDefColumns(String(row.sql ?? '')),\n unique: toBoolean(row.is_unique),\n primary: false,\n valid: true,\n partial: false,\n });\n byTable.set(tableName, bucket);\n }\n } catch {\n return null;\n }\n\n try {\n const result = await db.query(\n `SELECT table_name, constraint_type, constraint_column_names FROM duckdb_constraints()`,\n );\n for (const row of (result?.rows ?? []) as Record<string, unknown>[]) {\n const constraintType = String(row.constraint_type ?? '').toUpperCase();\n if (constraintType !== 'UNIQUE' && constraintType !== 'PRIMARY KEY') {\n continue;\n }\n const tableName = String(row.table_name ?? '');\n const columns = normalizeColumnNameList(row.constraint_column_names);\n if (!tableName || columns.length === 0) continue;\n\n const bucket = byTable.get(tableName) ?? [];\n bucket.push({\n name: `${tableName}_${columns.join('_')}_${constraintType === 'UNIQUE' ? 'key' : 'pkey'}`,\n columns,\n unique: true,\n primary: constraintType === 'PRIMARY KEY',\n valid: true,\n partial: false,\n });\n byTable.set(tableName, bucket);\n }\n } catch {\n // Constraints are not optional metadata on this engine: DuckDB requires an\n // inline UNIQUE constraint for upsert, so uniqueness lives almost entirely\n // in `duckdb_constraints()` rather than in `duckdb_indexes()`. Without it,\n // `conflict_target_unindexed` and `unique_constraint_missing` would fire as\n // *errors* against a correct database. Degrade the whole catalog instead —\n // the caller then skips every index check and the report says\n // `indexIntrospection: 'unavailable'`.\n return null;\n }\n\n return { forTable: async (tableName) => byTable.get(tableName) ?? [] };\n}\n\nfunction normalizeColumnNameList(value: unknown): string[] {\n if (Array.isArray(value)) {\n return value.map((entry) => String(entry)).filter(Boolean);\n }\n if (typeof value === 'string') {\n return value\n .replace(/^[[{]|[\\]}]$/g, '')\n .split(',')\n .map((entry) => entry.trim().replace(/^[\"']|[\"']$/g, ''))\n .filter(Boolean);\n }\n return [];\n}\n\nfunction toBoolean(value: unknown): boolean {\n if (typeof value === 'boolean') return value;\n if (typeof value === 'number') return value !== 0;\n if (typeof value === 'string') {\n return value === '1' || value.toLowerCase() === 'true' || value === 't';\n }\n return false;\n}\n\nfunction quoteLiteral(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`;\n}\n\nfunction describeError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -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.
|
|
6
|
+
"packageVersion": "0.51.1",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
10
|
+
"manifest": "ecea80ce0657328cc3f85a8017b0e023879ba56296bc3f3a77d87b1b1397bf71",
|
|
11
|
+
"packageJson": "40e77e98e084964716f5e18bd82a43730db0260211448132c75e6b8e788498c8",
|
|
12
|
+
"agents": "7e27a7c52729c302d80e9c36bf068fe1811a7ae51eccf66c6a5f9498aaec3318",
|
|
13
13
|
"moduleDoc:agents/object-runtime.md": "34a4e740c109467cd5486c4af0e3bf4b63915697721ab584df0e5506ea9290ca",
|
|
14
14
|
"moduleDoc:agents/revision-guard.md": "aa6b1ddb5b6b49fa27ebbe575ec7fcd6d5ceb35ee25acc702f877a226eb9a99a",
|
|
15
15
|
"moduleDoc:agents/collection-reads.md": "e5bb79b59c17e79a85a71580a019e6b0b641e498ac4d850073fd5ad634b57cb7",
|
|
@@ -98,8 +98,13 @@
|
|
|
98
98
|
"@happyvertical/sql",
|
|
99
99
|
"@happyvertical/utils"
|
|
100
100
|
],
|
|
101
|
-
"tags": [
|
|
102
|
-
|
|
101
|
+
"tags": [
|
|
102
|
+
"cross-package"
|
|
103
|
+
],
|
|
104
|
+
"risks": [
|
|
105
|
+
"polymorphic-associations:1",
|
|
106
|
+
"sensitive-fields-excluded"
|
|
107
|
+
],
|
|
103
108
|
"objects": [
|
|
104
109
|
{
|
|
105
110
|
"name": "SmrtClass",
|
|
@@ -1042,7 +1047,7 @@
|
|
|
1042
1047
|
"polymorphicAssociations": 1,
|
|
1043
1048
|
"uuidColumns": 3
|
|
1044
1049
|
},
|
|
1045
|
-
"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-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/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\n## Cross-module invariants\n\n- `ObjectRegistry` is a `globalThis` singleton so registration survives HMR.\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- 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- `ensureSystemTables(db, typeHint?)` provisions framework tables idempotently;\n call it on a base PostgreSQL connection before caller-owned transactions.\n Bootstrap uses an advisory lock. 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- 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- 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\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",
|
|
1050
|
+
"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-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\n## Cross-module invariants\n\n- `ObjectRegistry` is a `globalThis` singleton so registration survives HMR.\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- 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- `ensureSystemTables(db, typeHint?)` provisions framework tables idempotently;\n call it on a base PostgreSQL connection before caller-owned transactions.\n Bootstrap uses an advisory lock. 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- 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- 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\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",
|
|
1046
1051
|
"moduleDocs": [
|
|
1047
1052
|
{
|
|
1048
1053
|
"path": "agents/object-runtime.md",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.1",
|
|
4
4
|
"description": "Core AI agent framework with standardized collections, object-relational mapping, and code generators",
|
|
5
5
|
"author": "HappyVertical",
|
|
6
6
|
"type": "module",
|
|
@@ -154,9 +154,9 @@
|
|
|
154
154
|
"@happyvertical/files": "^0.89.9",
|
|
155
155
|
"@happyvertical/json": "^0.89.9",
|
|
156
156
|
"@happyvertical/logger": "^0.89.9",
|
|
157
|
-
"@happyvertical/smrt-config": "0.
|
|
158
|
-
"@happyvertical/smrt-scanner": "0.
|
|
159
|
-
"@happyvertical/smrt-types": "0.
|
|
157
|
+
"@happyvertical/smrt-config": "0.51.1",
|
|
158
|
+
"@happyvertical/smrt-scanner": "0.51.1",
|
|
159
|
+
"@happyvertical/smrt-types": "0.51.1",
|
|
160
160
|
"@happyvertical/sql": "^0.89.9",
|
|
161
161
|
"@happyvertical/utils": "^0.89.9",
|
|
162
162
|
"@oxc-project/runtime": "^0.138.0",
|