@happyvertical/smrt-core 0.51.6 → 0.51.8

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.
Files changed (32) hide show
  1. package/README.md +26 -0
  2. package/agents/build-knowledge.md +13 -0
  3. package/dist/consumer-plugin/index.d.ts.map +1 -1
  4. package/dist/consumer-plugin/index.js +141 -49
  5. package/dist/consumer-plugin/index.js.map +1 -1
  6. package/dist/manifest/discover-smrt-packages.d.ts +3 -2
  7. package/dist/manifest/discover-smrt-packages.d.ts.map +1 -1
  8. package/dist/manifest/discover-smrt-packages.js +34 -15
  9. package/dist/manifest/discover-smrt-packages.js.map +1 -1
  10. package/dist/manifest/package-manifest-exports.d.ts +34 -0
  11. package/dist/manifest/package-manifest-exports.d.ts.map +1 -0
  12. package/dist/manifest/package-manifest-exports.js +144 -0
  13. package/dist/manifest/package-manifest-exports.js.map +1 -0
  14. package/dist/manifest/static-manifest.js +1 -1
  15. package/dist/manifest/static-manifest.js.map +1 -1
  16. package/dist/manifest/store.js +1 -1
  17. package/dist/manifest.json +1 -1
  18. package/dist/migrations/differ.d.ts.map +1 -1
  19. package/dist/migrations/differ.js +19 -27
  20. package/dist/migrations/differ.js.map +1 -1
  21. package/dist/schema/column-data-probes.d.ts +71 -0
  22. package/dist/schema/column-data-probes.d.ts.map +1 -1
  23. package/dist/schema/column-data-probes.js +79 -1
  24. package/dist/schema/column-data-probes.js.map +1 -1
  25. package/dist/schema/live-parity.d.ts.map +1 -1
  26. package/dist/schema/live-parity.js +10 -12
  27. package/dist/schema/live-parity.js.map +1 -1
  28. package/dist/smrt-knowledge.json +5 -5
  29. package/dist/vite-plugin/index.d.ts.map +1 -1
  30. package/dist/vite-plugin/index.js +93 -3
  31. package/dist/vite-plugin/index.js.map +1 -1
  32. 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';\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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"}
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 resolveRenameDataPendingCandidates,\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 // #2911: a suggestion about data, not a schema mismatch — kept at\n // `info` for the same reason as the `differ.ts` advisory of the same\n // name, so a wrong guess here cannot gate anything that treats\n // `warning`/`error` findings as failing closed.\n severity: 'info',\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 // Group by target and resolve both ambiguity shapes (#2911): more than\n // one source for the same target stays ambiguous (no recommended repair);\n // the same source shared across more than one target is dropped from\n // every target's list rather than reported for each — see\n // {@link resolveRenameDataPendingCandidates}.\n const candidatesByColumn = resolveRenameDataPendingCandidates(\n pending.map((candidate) => ({\n targetName: candidate.declaredName,\n sourceName: candidate.extraName,\n requiresShapeCheck: candidate.requiresShapeCheck,\n extra: undefined,\n })),\n (sourceName) => shaped.get(sourceName) ?? false,\n );\n\n const findings: LiveParityFinding[] = [];\n for (const [declaredName, candidates] of candidatesByColumn) {\n const names = candidates.map((c) => c.sourceName).sort();\n findings.push(buildRenameDataPendingFinding(table, declaredName, names));\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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"}
@@ -3,12 +3,12 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-core",
6
- "packageVersion": "0.51.6",
6
+ "packageVersion": "0.51.8",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "04d47de7becbb63c0e648a0e424c96b446e83ce69624516bd116bf58b822358d",
11
- "packageJson": "7c2366cea8c6f20e72113c11a0efa83ca69ffe61414db2bd63de5136628788ce",
10
+ "manifest": "3c4aaf93aadc973a359f4b66ee96db5da69e2810b78aace5070cf2acfb9d942e",
11
+ "packageJson": "c586add2f48f0b7f433cd30bf6035b662597d2d1d99684b7b466fa38b51438c1",
12
12
  "agents": "7e27a7c52729c302d80e9c36bf068fe1811a7ae51eccf66c6a5f9498aaec3318",
13
13
  "moduleDoc:agents/object-runtime.md": "34a4e740c109467cd5486c4af0e3bf4b63915697721ab584df0e5506ea9290ca",
14
14
  "moduleDoc:agents/revision-guard.md": "aa6b1ddb5b6b49fa27ebbe575ec7fcd6d5ceb35ee25acc702f877a226eb9a99a",
@@ -19,7 +19,7 @@
19
19
  "moduleDoc:agents/change-feed.md": "3600fd344bc49f2ec50d375f432c7fd18ef54050a4fcabd3e3e89a2f02198fb6",
20
20
  "moduleDoc:agents/change-signals.md": "d9cb6a5541728ffea46607a6b1d4fa61d4621849f2b4ea86a0645fbb0af892e9",
21
21
  "moduleDoc:agents/generators.md": "87e0203be47d70b6493851cbac421e8a4a6232a0e47af3e1146c63cdc3d39235",
22
- "moduleDoc:agents/build-knowledge.md": "5e67560801f22ccb1413a512452bcc5d29fafed9b4411561b8c8d9095eba50f3",
22
+ "moduleDoc:agents/build-knowledge.md": "804fb47dfbdef4fa3ac7b66fafc825917a474d8c91b1675e647fe646df474c6b",
23
23
  "moduleDoc:agents/memory.md": "a86bdf370403d08ecad3be86e341eb322dfe6e6b7a4e8806a6f8be9bf3421da5",
24
24
  "moduleDoc:agents/system-diagnostics.md": "b7ccae18ee312bf788d5470f95fa78d3535f1789247d5372e40ca86538290772",
25
25
  "moduleDoc:agents/registry-snapshot.md": "6234aa540427396d1dda1825a4641b1072e247187d5ddf413ae46c691344ee30"
@@ -1097,7 +1097,7 @@
1097
1097
  {
1098
1098
  "path": "agents/build-knowledge.md",
1099
1099
  "module": "build-knowledge",
1100
- "content": "# Decorators, build integration, and knowledge\n\nKey options: `tableName`, `tableStrategy` ('cti'|'sti'), `conflictColumns`, `indexes` (declared multi-column indexes; see [schema-paths.md](schema-paths.md)), `api`/`mcp`/`cli` (generation config), `ai` (callable methods), `hooks` (beforeSave/afterSave/beforeDelete/afterDelete), `embeddings` (auto-generate), `tenantScoped`, `agent`, `ui` (`{ icon, label, description }` — nav/help hints round-tripped through the manifest as plain data; `description` is the object-level seed for form-level help, #2046).\n\nRegistration sets `SMRT_TABLE_NAME` static property (survives minification).\n\n## @field() UI hints (#2046)\n\n`@field({ ui: { basic, group, order, locked } })` — a static, presentation-only\nseed for the field-policy rail (epic #2045). Carried in the manifest under the\nfield's `_meta.ui` (never a top-level `FieldDefinition` key), readable at\nruntime via `getAllFields()` at `field._meta.ui`, and emitted (sanitized) with\n`description` into generated web-collection definitions and browser MCP tool\nschemas. No schema/persistence/security effect — `sensitive`/`readPermission`\nstay the security rail, and `sensitive`/`transient` fields never emit to the\nclient at all.\n\n## Lightweight discovery\n\n`src/knowledge-discovery.ts`, exported through `smrt-core/knowledge`, enumerates\ninstalled scope directories and reads canonical AGENTS/legacy CLAUDE docs without\nloading package code, artifacts, or scanning objects. CLI snapshots and MCP share\nthese primitives. Callers retain selection policy: snapshots resolve links and\nfall back to `packages/*` only when no installed SMRT package loads; MCP preserves\nnode_modules paths, deduplicates realpaths, excludes authored workspace links,\nand then enriches the selected packages. Workspace-root/glob discovery remains\nowned by each consumer.\n\n## Domain Knowledge Artifacts\n\n`smrtPlugin()` writes runtime manifests and agent/developer knowledge artifacts:\n\n- local dev/build: `.smrt/manifest.json` and `.smrt/smrt-knowledge.json`\n- package build: `dist/manifest.json` and `dist/smrt-knowledge.json`\n\nKeep `manifest.json` runtime-focused. `smrt-knowledge.json` is the deterministic\nagent contract for downstream review and architecture tools.\n\nThe schema-version-1 object projection is additive and high-signal: it retains\nnormalized tenant mode/field, explicit `cti`/`sti` strategy, conflict columns,\nmethod signatures, and field defaults/constraints/readonly/transient flags.\nSensitive fields are removed before both `fields` and `relationships` are\nderived, including legacy flags stored under `_meta`; matching field and\nsnake-case column names are also removed from projected conflict columns, and a\nsensitive custom tenant field is omitted while retaining scope and mode.\nGenerated artifacts assert this boundary with `sensitiveFieldsExcluded: true`;\nthe optional marker keeps schema version 1 additive while letting readers\nidentify older artifacts that require raw-manifest corroboration.\n\nConfig precedence for knowledge is defaults → top-level `knowledge` in\n`smrt.config.ts` → `packages[packageName].knowledge` → plugin option →\nobject-level `@smrt({ knowledge })`.\n\nObject-level `knowledge: false` excludes an object from authored context only;\nit must not change runtime manifest registration. Use\n`knowledge: { tags, summary, risks }` for review-sensitive domain objects.\n\nHTTP knowledge routes are disabled by default. If `knowledge.api.enabled` is\ntrue, generated SvelteKit routes must stay GET-only and guarded by dev mode or\nadmin auth.\n\n\n## Vite Plugin\n\n```typescript\n// vite.config.ts — required for @smrt() decorators (Vite 8+, oxc transform)\nexport default defineConfig({\n oxc: {\n decorator: {\n legacy: true,\n emitDecoratorMetadata: true,\n },\n },\n});\n```\n\nUnder Vite 8 the oxc transform does not honor the pre-Vite-8 `esbuild.tsconfigRaw`\nrecipe (or tsconfig `experimentalDecorators` reached through SvelteKit's\n`extends \"./.svelte-kit/tsconfig.json\"` chain), so that recipe throws\n`SyntaxError: Invalid or unexpected token` on the first SSR request. Configure\ndecorators through `oxc.decorator` instead. Consumers still pinned on vite<8 need\nthe legacy `esbuild.tsconfigRaw` form with `experimentalDecorators: true,\nemitDecoratorMetadata: true`.\n\nFor independent CI invocations, both `smrtPlugin()` and `smrtConsumer()` accept\nthe same `generationSnapshot: { path, sha256, provenance, sourceRoot }`. The\nschema-v1 snapshot produced by `serializeSmrtGenerationSnapshot()` contains the\nmerged project/dependency manifest, portable source paths, and source-file\ndigests; each plugin selects its own view. Reuse mode fails closed on\nbyte/provenance/path/content drift, skips scans and manifest writes, and still\ngenerates routes, types, registration, and virtual modules. Omit it for normal\nlocal development and watch mode.\n"
1100
+ "content": "# Decorators, build integration, and knowledge\n\nKey options: `tableName`, `tableStrategy` ('cti'|'sti'), `conflictColumns`, `indexes` (declared multi-column indexes; see [schema-paths.md](schema-paths.md)), `api`/`mcp`/`cli` (generation config), `ai` (callable methods), `hooks` (beforeSave/afterSave/beforeDelete/afterDelete), `embeddings` (auto-generate), `tenantScoped`, `agent`, `ui` (`{ icon, label, description }` — nav/help hints round-tripped through the manifest as plain data; `description` is the object-level seed for form-level help, #2046).\n\nRegistration sets `SMRT_TABLE_NAME` static property (survives minification).\n\n## @field() UI hints (#2046)\n\n`@field({ ui: { basic, group, order, locked } })` — a static, presentation-only\nseed for the field-policy rail (epic #2045). Carried in the manifest under the\nfield's `_meta.ui` (never a top-level `FieldDefinition` key), readable at\nruntime via `getAllFields()` at `field._meta.ui`, and emitted (sanitized) with\n`description` into generated web-collection definitions and browser MCP tool\nschemas. No schema/persistence/security effect — `sensitive`/`readPermission`\nstay the security rail, and `sensitive`/`transient` fields never emit to the\nclient at all.\n\n## Lightweight discovery\n\n`src/knowledge-discovery.ts`, exported through `smrt-core/knowledge`, enumerates\ninstalled scope directories and reads canonical AGENTS/legacy CLAUDE docs without\nloading package code, artifacts, or scanning objects. CLI snapshots and MCP share\nthese primitives. Callers retain selection policy: snapshots resolve links and\nfall back to `packages/*` only when no installed SMRT package loads; MCP preserves\nnode_modules paths, deduplicates realpaths, excludes authored workspace links,\nand then enriches the selected packages. Workspace-root/glob discovery remains\nowned by each consumer.\n\n## Domain Knowledge Artifacts\n\n`smrtPlugin()` writes runtime manifests and agent/developer knowledge artifacts:\n\n- local dev/build: `.smrt/manifest.json` and `.smrt/smrt-knowledge.json`\n- package build: `dist/manifest.json` and `dist/smrt-knowledge.json`\n\nKeep `manifest.json` runtime-focused. `smrt-knowledge.json` is the deterministic\nagent contract for downstream review and architecture tools.\n\nThe CLI's schema-command gate reads that same merged file: a pure-consumer\nproject (0 local objects) qualifies on the consumed packages its manifest\nrecords, so a clobbered `objects` map is what made `smrt db:migrate` report\n`missing_local_manifest` (#2925).\n\n`.smrt/manifest.json` has two writers and neither owns all of it: `smrtPlugin()`\ncontributes the project's scanned objects and `smrtConsumer()` the consumed\npackages' entries. Both writes are merge-preserving, keyed on each entry's\nrecorded `packageName` — a plain overwrite in either direction silently drops\nthe other's objects, and the last pass of a multi-environment build is not\nreliably the aggregating one (#1760, #2925). `dist/manifest.json` stays\nlocal-only: a published package must not carry its dependencies' objects.\n\nThe schema-version-1 object projection is additive and high-signal: it retains\nnormalized tenant mode/field, explicit `cti`/`sti` strategy, conflict columns,\nmethod signatures, and field defaults/constraints/readonly/transient flags.\nSensitive fields are removed before both `fields` and `relationships` are\nderived, including legacy flags stored under `_meta`; matching field and\nsnake-case column names are also removed from projected conflict columns, and a\nsensitive custom tenant field is omitted while retaining scope and mode.\nGenerated artifacts assert this boundary with `sensitiveFieldsExcluded: true`;\nthe optional marker keeps schema version 1 additive while letting readers\nidentify older artifacts that require raw-manifest corroboration.\n\nConfig precedence for knowledge is defaults → top-level `knowledge` in\n`smrt.config.ts` → `packages[packageName].knowledge` → plugin option →\nobject-level `@smrt({ knowledge })`.\n\nObject-level `knowledge: false` excludes an object from authored context only;\nit must not change runtime manifest registration. Use\n`knowledge: { tags, summary, risks }` for review-sensitive domain objects.\n\nHTTP knowledge routes are disabled by default. If `knowledge.api.enabled` is\ntrue, generated SvelteKit routes must stay GET-only and guarded by dev mode or\nadmin auth.\n\n\n## Vite Plugin\n\n```typescript\n// vite.config.ts — required for @smrt() decorators (Vite 8+, oxc transform)\nexport default defineConfig({\n oxc: {\n decorator: {\n legacy: true,\n emitDecoratorMetadata: true,\n },\n },\n});\n```\n\nUnder Vite 8 the oxc transform does not honor the pre-Vite-8 `esbuild.tsconfigRaw`\nrecipe (or tsconfig `experimentalDecorators` reached through SvelteKit's\n`extends \"./.svelte-kit/tsconfig.json\"` chain), so that recipe throws\n`SyntaxError: Invalid or unexpected token` on the first SSR request. Configure\ndecorators through `oxc.decorator` instead. Consumers still pinned on vite<8 need\nthe legacy `esbuild.tsconfigRaw` form with `experimentalDecorators: true,\nemitDecoratorMetadata: true`.\n\nFor independent CI invocations, both `smrtPlugin()` and `smrtConsumer()` accept\nthe same `generationSnapshot: { path, sha256, provenance, sourceRoot }`. The\nschema-v1 snapshot produced by `serializeSmrtGenerationSnapshot()` contains the\nmerged project/dependency manifest, portable source paths, and source-file\ndigests; each plugin selects its own view. Reuse mode fails closed on\nbyte/provenance/path/content drift, skips scans and manifest writes, and still\ngenerates routes, types, registration, and virtual modules. Omit it for normal\nlocal development and watch mode.\n"
1101
1101
  },
1102
1102
  {
1103
1103
  "path": "agents/memory.md",
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"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"}
@@ -185,6 +185,84 @@ function generateInlineRegisterModule(manifest) {
185
185
  ""
186
186
  ].join("\n");
187
187
  }
188
+ /**
189
+ * Decide whether a `.smrt/manifest.json` entry belongs to this project's own
190
+ * scan rather than to a consumed package aggregated into the same file by
191
+ * `smrtConsumer()`.
192
+ *
193
+ * Ownership is read from the entry's recorded `packageName`: the scanner
194
+ * stamps every locally scanned object with the project's own package name
195
+ * (issue #713), and the consumer plugin stamps every aggregated entry with
196
+ * its source package. An entry with no recorded owner can only have come from
197
+ * the local write, because the consumer always records one.
198
+ *
199
+ * `localPackageNames` carries every name this project has written under, not
200
+ * just its current one. Renaming the project's `package.json#name` would
201
+ * otherwise orphan the entries stamped with the old name: they stop matching
202
+ * the current name, get classified as another package's, and survive every
203
+ * later write — so a local object deleted or renamed in the same change stays
204
+ * in the manifest schema commands read. The previous write's own top-level
205
+ * `packageName` supplies that prior name (`saveAggregatedManifest()` keeps it
206
+ * as the local project's), and it is the only other name a local entry can
207
+ * carry.
208
+ */
209
+ function isLocallyOwnedManifestEntry(entry, localPackageNames) {
210
+ const owner = entry?.packageName;
211
+ if (typeof owner !== "string" || owner.length === 0) return true;
212
+ return localPackageNames.has(owner);
213
+ }
214
+ /** Read a manifest's `smrtDependencies`, tolerating an untrusted on-disk shape. */
215
+ function readManifestDependencies(manifest) {
216
+ if (!Array.isArray(manifest?.smrtDependencies)) return [];
217
+ return manifest.smrtDependencies.filter((dependency) => typeof dependency === "string" && dependency.length > 0);
218
+ }
219
+ /**
220
+ * Union a freshly scanned local manifest with the consumed-package state
221
+ * already present in `.smrt/manifest.json` (issue #2925).
222
+ *
223
+ * The local scan is authoritative for the objects it owns — a deleted local
224
+ * object must disappear from the file — so only entries owned by another
225
+ * package are carried forward, and a local entry wins any key collision. As
226
+ * with the consumer plugin's merge, `.smrt/` is a build cache: dropping a
227
+ * package from `smrtConsumer()` leaves its stale entries until the directory
228
+ * is removed and the project rebuilt.
229
+ *
230
+ * `smrtDependencies` is merged the same way and for the same reason. The
231
+ * consumer writer owns that list too — `aggregateTypeManifests()` seeds it
232
+ * from `smrtConsumer()`'s explicit `packages` — while the local scan derives
233
+ * its own from the dependency tree, and the two need not agree: a configured
234
+ * package the local discovery does not return would keep its objects here but
235
+ * lose its declaration. The CLI's schema gate reads exactly that list to
236
+ * admit a zero-local-object project, so dropping it reintroduces the
237
+ * `missing_local_manifest` failure this change fixes.
238
+ */
239
+ function mergeExternalManifestEntries(manifest, manifestPath) {
240
+ if (!existsSync(manifestPath)) return manifest;
241
+ let existing;
242
+ try {
243
+ existing = JSON.parse(readFileSync(manifestPath, "utf-8"));
244
+ } catch {
245
+ return manifest;
246
+ }
247
+ if (!existing?.objects || typeof existing.objects !== "object") return manifest;
248
+ const localPackageNames = /* @__PURE__ */ new Set();
249
+ if (manifest.packageName) localPackageNames.add(manifest.packageName);
250
+ if (typeof existing.packageName === "string" && existing.packageName) localPackageNames.add(existing.packageName);
251
+ const preserved = {};
252
+ for (const [name, entry] of Object.entries(existing.objects)) if (!isLocallyOwnedManifestEntry(entry, localPackageNames)) preserved[name] = entry;
253
+ const scannedDependencies = new Set(readManifestDependencies(manifest));
254
+ const mergedDependencies = /* @__PURE__ */ new Set([...scannedDependencies, ...readManifestDependencies(existing)]);
255
+ const carriesExtraDependency = mergedDependencies.size > scannedDependencies.size;
256
+ if (Object.keys(preserved).length === 0 && !carriesExtraDependency) return manifest;
257
+ return {
258
+ ...manifest,
259
+ objects: {
260
+ ...preserved,
261
+ ...manifest.objects
262
+ },
263
+ ...mergedDependencies.size > 0 ? { smrtDependencies: [...mergedDependencies].sort() } : {}
264
+ };
265
+ }
188
266
  function smrtPlugin(options = {}) {
189
267
  const { projectRoot: configuredProjectRoot, generationSnapshot, include = ["src/**/*.ts", "src/**/*.js"], exclude = [
190
268
  "**/*.test.ts",
@@ -275,6 +353,16 @@ function smrtPlugin(options = {}) {
275
353
  * Write manifest to .smrt/manifest.json for CLI discovery.
276
354
  * This ensures `smrt db:migrate`, `smrt db:status`, etc. can find
277
355
  * locally-defined SMRT objects in non-library builds (Issue #963).
356
+ *
357
+ * Merge-preserving, symmetrically with `smrtConsumer()`'s
358
+ * `saveAggregatedManifest()` (issue #1760): both plugins write this one
359
+ * file, and neither owns all of it. This hook runs once per Vite build
360
+ * pass/environment, while the consumer's aggregation runs in `buildStart`
361
+ * and does not run on every pass — so a SvelteKit `adapter-node` build ends
362
+ * with this write, and a plain overwrite here drops every consumed
363
+ * package's objects from the file the CLI then reads (issue #2925). For a
364
+ * pure-consumer app (0 local objects, the shape `template-sveltekit`
365
+ * documents) that left `objects: {}` on disk and broke `smrt db:migrate`.
278
366
  */
279
367
  async function writeLocalManifest(m, rootDir) {
280
368
  try {
@@ -283,10 +371,12 @@ function smrtPlugin(options = {}) {
283
371
  const smrtDir = resolve(rootDir, ".smrt");
284
372
  mkdirSync(smrtDir, { recursive: true });
285
373
  const manifestPath = resolve(smrtDir, "manifest.json");
286
- writeFileSync(manifestPath, JSON.stringify(m, null, 2), "utf-8");
287
- await writeDomainKnowledgeArtifact(m, rootDir, resolve(smrtDir, "smrt-knowledge.json"), manifestPath);
374
+ const merged = mergeExternalManifestEntries(m, manifestPath);
375
+ writeFileSync(manifestPath, JSON.stringify(merged, null, 2), "utf-8");
376
+ await writeDomainKnowledgeArtifact(merged, rootDir, resolve(smrtDir, "smrt-knowledge.json"), manifestPath);
288
377
  const objectCount = Object.keys(m.objects).length;
289
- console.log(`[smrt] Wrote local manifest with ${objectCount} objects to .smrt/manifest.json`);
378
+ const preservedCount = Object.keys(merged.objects).length - objectCount;
379
+ console.log(preservedCount > 0 ? `[smrt] Wrote local manifest with ${objectCount} objects to .smrt/manifest.json (preserved ${preservedCount} consumed package objects)` : `[smrt] Wrote local manifest with ${objectCount} objects to .smrt/manifest.json`);
290
380
  } catch (error) {
291
381
  console.error("[smrt] Error writing local manifest:", error);
292
382
  }