@happyvertical/smrt-core 0.47.1 → 0.47.2

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 (66) hide show
  1. package/AGENTS.md +1 -0
  2. package/agents/registry-snapshot.md +28 -0
  3. package/agents/schema-paths.md +12 -0
  4. package/dist/browser.js +3 -2
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +4 -2
  8. package/dist/manifest/static-manifest.js +1 -1
  9. package/dist/manifest/static-manifest.js.map +1 -1
  10. package/dist/manifest/store.js +1 -1
  11. package/dist/manifest.json +1 -1
  12. package/dist/migrations/differ.d.ts +78 -0
  13. package/dist/migrations/differ.d.ts.map +1 -1
  14. package/dist/migrations/differ.js +191 -1
  15. package/dist/migrations/differ.js.map +1 -1
  16. package/dist/migrations/generator.d.ts.map +1 -1
  17. package/dist/migrations/generator.js +2 -1
  18. package/dist/migrations/generator.js.map +1 -1
  19. package/dist/prebuild/index.d.ts.map +1 -1
  20. package/dist/prebuild/index.js +4 -3
  21. package/dist/prebuild/index.js.map +1 -1
  22. package/dist/registry/class-registration.d.ts.map +1 -1
  23. package/dist/registry/class-registration.js +3 -1
  24. package/dist/registry/class-registration.js.map +1 -1
  25. package/dist/registry/schema-builder.js +1 -1
  26. package/dist/registry.js +1 -1
  27. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  28. package/dist/scanner/manifest-generator.js +22 -1
  29. package/dist/scanner/manifest-generator.js.map +1 -1
  30. package/dist/schema/foreign-key-ddl.d.ts +18 -0
  31. package/dist/schema/foreign-key-ddl.d.ts.map +1 -1
  32. package/dist/schema/foreign-key-ddl.js +15 -2
  33. package/dist/schema/foreign-key-ddl.js.map +1 -1
  34. package/dist/schema/foreign-key-orphan-report.d.ts +57 -0
  35. package/dist/schema/foreign-key-orphan-report.d.ts.map +1 -0
  36. package/dist/schema/foreign-key-orphan-report.js +142 -0
  37. package/dist/schema/foreign-key-orphan-report.js.map +1 -0
  38. package/dist/schema/index.d.ts +1 -0
  39. package/dist/schema/index.d.ts.map +1 -1
  40. package/dist/schema/index.js +2 -1
  41. package/dist/schema/live-parity.d.ts +8 -1
  42. package/dist/schema/live-parity.d.ts.map +1 -1
  43. package/dist/schema/live-parity.js +98 -2
  44. package/dist/schema/live-parity.js.map +1 -1
  45. package/dist/schema/types.d.ts +11 -1
  46. package/dist/schema/types.d.ts.map +1 -1
  47. package/dist/schema.js +2 -1
  48. package/dist/smrt-knowledge.json +14 -8
  49. package/dist/system/index.d.ts +1 -0
  50. package/dist/system/index.d.ts.map +1 -1
  51. package/dist/system/index.js +2 -1
  52. package/dist/system/registry-snapshot.d.ts +112 -0
  53. package/dist/system/registry-snapshot.d.ts.map +1 -0
  54. package/dist/system/registry-snapshot.js +171 -0
  55. package/dist/system/registry-snapshot.js.map +1 -0
  56. package/dist/testing/database.d.ts.map +1 -1
  57. package/dist/testing/database.js +10 -2
  58. package/dist/testing/database.js.map +1 -1
  59. package/dist/vite-plugin/index.d.ts.map +1 -1
  60. package/dist/vite-plugin/index.js +4 -3
  61. package/dist/vite-plugin/index.js.map +1 -1
  62. package/dist/vite-plugin/web-collections.d.ts +6 -0
  63. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  64. package/dist/vite-plugin/web-collections.js +9 -4
  65. package/dist/vite-plugin/web-collections.js.map +1 -1
  66. package/package.json +4 -4
@@ -220,6 +220,84 @@ export declare class SchemaComparer {
220
220
  * that orphan gets an executable `DROP NOT NULL` where the engine allows.
221
221
  */
222
222
  private compareColumns;
223
+ /**
224
+ * Detect a pending rename backfill (#2752): a manifest-declared column
225
+ * that exists live but holds no data, paired with an orphan (undeclared)
226
+ * live column of a compatible type that does hold data — the shape a
227
+ * framework field rename leaves behind when `db:migrate` adds the new
228
+ * column additively and never moves data into it. For each such pair,
229
+ * emit the copy-then-drop repair as an advisory: never executed,
230
+ * PostgreSQL and SQLite only (DuckDB/JSON are out of scope). The repair
231
+ * is genuinely idempotent SQL on PostgreSQL (a self-guarding
232
+ * `DO $$ ... $$` block); SQLite has no conditional-DDL construct, so its
233
+ * repair is operator-mediated instead — a guard query plus instructions,
234
+ * not a blind-rerun-safe statement. See {@link describeRenameDataPending}.
235
+ */
236
+ private detectRenameDataPending;
237
+ /**
238
+ * Live-data probe: does the column hold any non-null, non-empty value?
239
+ * Used by {@link detectRenameDataPending}. Errors propagate to the
240
+ * caller, which skips the pair rather than guessing (#2752).
241
+ */
242
+ private columnHasNonEmptyValue;
243
+ /**
244
+ * Live-data probe: are every one of a column's non-empty values
245
+ * UUID-shaped ({@link CANONICAL_UUID_PATTERN})? PostgreSQL pushes the
246
+ * check into the query with its regex operator; SQLite has no regex
247
+ * operator, but its case-sensitive `GLOB` can still express the fixed
248
+ * 36-character canonical shape ({@link CANONICAL_UUID_SQLITE_GLOB_PATTERN}
249
+ * against `LOWER(...)`, guarded by an exact `LENGTH(...) = 36` check), so
250
+ * both engines run one server-side aggregate `count(*)` rather than
251
+ * fetching every non-empty value into JS to test in a loop — important on
252
+ * a production table with many rows (#2767 review).
253
+ */
254
+ private allNonEmptyValuesUuidShaped;
255
+ /**
256
+ * Render the repair for one rename-data-pending pair (#2752): copy
257
+ * non-empty `oldColumn` into `newColumn` only where `newColumn` is still
258
+ * empty (casting to `uuid` when the new column is native uuid, otherwise a
259
+ * plain copy), then drop `oldColumn`.
260
+ *
261
+ * Idempotency review findings and the fix:
262
+ *
263
+ * - (P1) The `newColumn`-is-empty predicate previously compared any
264
+ * non-uuid destination to `''` (`col IS NULL OR col = ''`), which is
265
+ * only valid for text-affinity types — PostgreSQL rejects `col = ''`
266
+ * for `integer`/`boolean`/`timestamp`/etc. before a same-type rename
267
+ * pair of one of those types ever copies anything. Fixed by comparing
268
+ * `CAST(col AS TEXT) = ''` instead, mirroring the same portable
269
+ * emptiness check {@link columnHasNonEmptyValue} already uses for
270
+ * detection, so the predicate is valid for every column type.
271
+ * - (P2) An `UPDATE` unconditionally naming `oldColumn` fails once that
272
+ * column is gone, on either engine, regardless of the DROP's own
273
+ * idempotency — a guard query placed before the DROP (or even before
274
+ * the UPDATE) still leaves a human decision between "run" and "don't",
275
+ * which is not genuine SQL-level idempotency. PostgreSQL CAN express a
276
+ * truly self-contained, unconditionally-safe-to-rerun statement here:
277
+ * an anonymous `DO $$ ... $$` block (the same idiom this file already
278
+ * uses for `generatePostgresIntegerPreflightSQL`) that checks
279
+ * `information_schema.columns` and only runs the UPDATE + DROP when the
280
+ * old column still exists — one executable statement, no operator
281
+ * judgment required, safe to rerun any number of times. SQLite has no
282
+ * procedural block or conditional-DDL construct at all (confirmed
283
+ * against a real connection), so there the contract is explicitly
284
+ * downgraded from "idempotent SQL" to "operator-mediated": a guard
285
+ * query plus instructions, not a single statement that is itself safe
286
+ * to blindly rerun.
287
+ *
288
+ * This is report-only either way: `db:migrate` never executes it.
289
+ */
290
+ private describeRenameDataPending;
291
+ /**
292
+ * Render the advisory for an ambiguous rename-data-pending match (#2767
293
+ * review): more than one orphan column is a populated, type-compatible
294
+ * candidate for the same empty declared column, so the real rename source
295
+ * cannot be inferred. Unlike {@link describeRenameDataPending}, this
296
+ * carries no `suggestedSql` — running a per-candidate copy-and-drop would
297
+ * let output order decide which candidate's data wins and then drop every
298
+ * one of them, which is not a repair an advisory should suggest blindly.
299
+ */
300
+ private describeRenameDataPendingAmbiguous;
223
301
  /**
224
302
  * Compare nullability and default between a manifest column and the live
225
303
  * column, emitting `alter_column` changes (#2369).
@@ -1 +1 @@
1
- {"version":3,"file":"differ.d.ts","sourceRoot":"","sources":["../../src/migrations/differ.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"differ.d.ts","sourceRoot":"","sources":["../../src/migrations/differ.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { normalizeForeignKeyAction, requireForeignKeyAction } from "../schema/foreign-key-policy.js";
2
2
  import { isJsonPathIndex, isStiSubtypeUniqueIndex, renderIndexTarget } from "../schema/index-utils.js";
3
- import { foreignKeyConstraintName, foreignKeyRelationshipKey, renderForeignKeyConstraint, renderForeignKeyOrphanDetector, renderForeignKeyOrphanRepair, schemaForeignKeys, schemaForeignKeysForEngine } from "../schema/foreign-key-ddl.js";
3
+ import { CANONICAL_UUID_PATTERN, CANONICAL_UUID_SQLITE_GLOB_PATTERN, foreignKeyConstraintName, foreignKeyRelationshipKey, renderForeignKeyConstraint, renderForeignKeyOrphanDetector, renderForeignKeyOrphanRepair, schemaForeignKeys, schemaForeignKeysForEngine } from "../schema/foreign-key-ddl.js";
4
4
  import { detectEngine, getDDLStrategy } from "../schema/ddl/index.js";
5
5
  import { describeForeignKeyTypeConflict, planUuidConvergence, renderUuidConvergenceRepairHint, renderUuidShapeProbe } from "../schema/uuid-convergence.js";
6
6
  import "../schema/utils.js";
@@ -791,9 +791,199 @@ var SchemaComparer = class {
791
791
  }
792
792
  changes.push(this.describeOrphanColumn(tableName, colName, dbCol));
793
793
  }
794
+ changes.push(...await this.detectRenameDataPending(tableName, manifest, dbSchema));
794
795
  return changes;
795
796
  }
796
797
  /**
798
+ * Detect a pending rename backfill (#2752): a manifest-declared column
799
+ * that exists live but holds no data, paired with an orphan (undeclared)
800
+ * live column of a compatible type that does hold data — the shape a
801
+ * framework field rename leaves behind when `db:migrate` adds the new
802
+ * column additively and never moves data into it. For each such pair,
803
+ * emit the copy-then-drop repair as an advisory: never executed,
804
+ * PostgreSQL and SQLite only (DuckDB/JSON are out of scope). The repair
805
+ * is genuinely idempotent SQL on PostgreSQL (a self-guarding
806
+ * `DO $$ ... $$` block); SQLite has no conditional-DDL construct, so its
807
+ * repair is operator-mediated instead — a guard query plus instructions,
808
+ * not a blind-rerun-safe statement. See {@link describeRenameDataPending}.
809
+ */
810
+ async detectRenameDataPending(tableName, manifest, dbSchema) {
811
+ if (this.engine !== "postgres" && this.engine !== "sqlite") return [];
812
+ const dbColumnNames = new Set(Object.keys(dbSchema.columns));
813
+ const manifestColumnNames = new Set(Object.keys(manifest.columns));
814
+ const orphanColumnNames = [...dbColumnNames].filter((name) => !manifestColumnNames.has(name));
815
+ if (orphanColumnNames.length === 0) return [];
816
+ const changes = [];
817
+ for (const [colName, colDef] of Object.entries(manifest.columns)) {
818
+ if (!dbSchema.columns[colName]) continue;
819
+ let declaredHasData;
820
+ try {
821
+ declaredHasData = await this.columnHasNonEmptyValue(tableName, colName);
822
+ } catch {
823
+ continue;
824
+ }
825
+ if (declaredHasData) continue;
826
+ const validatedType = isValidSQLDataType(colDef.type) ? colDef.type : "TEXT";
827
+ const isLogicalUuid = validatedType === "UUID";
828
+ const declaredNormalized = this.normalizeType(this.ddlStrategy.mapType(validatedType));
829
+ const candidates = [];
830
+ for (const orphanName of orphanColumnNames) {
831
+ const orphanCol = dbSchema.columns[orphanName];
832
+ const orphanNormalized = this.normalizeType(orphanCol.type);
833
+ const requiresShapeCheck = isLogicalUuid && orphanNormalized === "TEXT";
834
+ if (!requiresShapeCheck && !(orphanNormalized === declaredNormalized)) continue;
835
+ let orphanHasData;
836
+ try {
837
+ orphanHasData = await this.columnHasNonEmptyValue(tableName, orphanName);
838
+ } catch {
839
+ continue;
840
+ }
841
+ if (!orphanHasData) continue;
842
+ if (requiresShapeCheck) {
843
+ let shaped;
844
+ try {
845
+ shaped = await this.allNonEmptyValuesUuidShaped(tableName, orphanName);
846
+ } catch {
847
+ continue;
848
+ }
849
+ if (!shaped) continue;
850
+ }
851
+ candidates.push({
852
+ orphanName,
853
+ isUuidCast: declaredNormalized === "UUID"
854
+ });
855
+ }
856
+ if (candidates.length === 1) changes.push(this.describeRenameDataPending(tableName, colName, candidates[0].orphanName, candidates[0].isUuidCast));
857
+ else if (candidates.length > 1) changes.push(this.describeRenameDataPendingAmbiguous(tableName, colName, candidates.map((c) => c.orphanName)));
858
+ }
859
+ return changes;
860
+ }
861
+ /**
862
+ * Live-data probe: does the column hold any non-null, non-empty value?
863
+ * Used by {@link detectRenameDataPending}. Errors propagate to the
864
+ * caller, which skips the pair rather than guessing (#2752).
865
+ */
866
+ async columnHasNonEmptyValue(tableName, colName) {
867
+ const quotedTable = this.quoteIdentifier(tableName);
868
+ const quotedCol = this.quoteIdentifier(colName);
869
+ return ((await this.db.query(`SELECT 1 AS present FROM ${quotedTable} WHERE ${quotedCol} IS NOT NULL AND CAST(${quotedCol} AS TEXT) <> '' LIMIT 1`)).rows?.length ?? 0) > 0;
870
+ }
871
+ /**
872
+ * Live-data probe: are every one of a column's non-empty values
873
+ * UUID-shaped ({@link CANONICAL_UUID_PATTERN})? PostgreSQL pushes the
874
+ * check into the query with its regex operator; SQLite has no regex
875
+ * operator, but its case-sensitive `GLOB` can still express the fixed
876
+ * 36-character canonical shape ({@link CANONICAL_UUID_SQLITE_GLOB_PATTERN}
877
+ * against `LOWER(...)`, guarded by an exact `LENGTH(...) = 36` check), so
878
+ * both engines run one server-side aggregate `count(*)` rather than
879
+ * fetching every non-empty value into JS to test in a loop — important on
880
+ * a production table with many rows (#2767 review).
881
+ */
882
+ async allNonEmptyValuesUuidShaped(tableName, colName) {
883
+ const quotedTable = this.quoteIdentifier(tableName);
884
+ const quotedCol = this.quoteIdentifier(colName);
885
+ const nonEmptyPredicate = `${quotedCol} IS NOT NULL AND CAST(${quotedCol} AS TEXT) <> ''`;
886
+ if (this.engine === "postgres") {
887
+ const row = (await this.db.query(`SELECT count(*) AS invalid_count FROM ${quotedTable} WHERE ${nonEmptyPredicate} AND CAST(${quotedCol} AS TEXT) !~* '${CANONICAL_UUID_PATTERN}'`)).rows?.[0];
888
+ return Number(row?.invalid_count ?? 0) === 0;
889
+ }
890
+ const row = (await this.db.query(`SELECT count(*) AS invalid_count FROM ${quotedTable} WHERE ${nonEmptyPredicate} AND NOT (LENGTH(CAST(${quotedCol} AS TEXT)) = 36 AND LOWER(CAST(${quotedCol} AS TEXT)) GLOB '${CANONICAL_UUID_SQLITE_GLOB_PATTERN}')`)).rows?.[0];
891
+ return Number(row?.invalid_count ?? 0) === 0;
892
+ }
893
+ /**
894
+ * Render the repair for one rename-data-pending pair (#2752): copy
895
+ * non-empty `oldColumn` into `newColumn` only where `newColumn` is still
896
+ * empty (casting to `uuid` when the new column is native uuid, otherwise a
897
+ * plain copy), then drop `oldColumn`.
898
+ *
899
+ * Idempotency review findings and the fix:
900
+ *
901
+ * - (P1) The `newColumn`-is-empty predicate previously compared any
902
+ * non-uuid destination to `''` (`col IS NULL OR col = ''`), which is
903
+ * only valid for text-affinity types — PostgreSQL rejects `col = ''`
904
+ * for `integer`/`boolean`/`timestamp`/etc. before a same-type rename
905
+ * pair of one of those types ever copies anything. Fixed by comparing
906
+ * `CAST(col AS TEXT) = ''` instead, mirroring the same portable
907
+ * emptiness check {@link columnHasNonEmptyValue} already uses for
908
+ * detection, so the predicate is valid for every column type.
909
+ * - (P2) An `UPDATE` unconditionally naming `oldColumn` fails once that
910
+ * column is gone, on either engine, regardless of the DROP's own
911
+ * idempotency — a guard query placed before the DROP (or even before
912
+ * the UPDATE) still leaves a human decision between "run" and "don't",
913
+ * which is not genuine SQL-level idempotency. PostgreSQL CAN express a
914
+ * truly self-contained, unconditionally-safe-to-rerun statement here:
915
+ * an anonymous `DO $$ ... $$` block (the same idiom this file already
916
+ * uses for `generatePostgresIntegerPreflightSQL`) that checks
917
+ * `information_schema.columns` and only runs the UPDATE + DROP when the
918
+ * old column still exists — one executable statement, no operator
919
+ * judgment required, safe to rerun any number of times. SQLite has no
920
+ * procedural block or conditional-DDL construct at all (confirmed
921
+ * against a real connection), so there the contract is explicitly
922
+ * downgraded from "idempotent SQL" to "operator-mediated": a guard
923
+ * query plus instructions, not a single statement that is itself safe
924
+ * to blindly rerun.
925
+ *
926
+ * This is report-only either way: `db:migrate` never executes it.
927
+ */
928
+ describeRenameDataPending(tableName, newColumn, oldColumn, newIsNativeUuid) {
929
+ const quotedTable = this.quoteIdentifier(tableName);
930
+ const quotedNew = this.quoteIdentifier(newColumn);
931
+ const quotedOld = this.quoteIdentifier(oldColumn);
932
+ const copySql = `UPDATE ${quotedTable} SET ${quotedNew} = ${newIsNativeUuid ? `${quotedOld}::uuid` : quotedOld} WHERE ${newIsNativeUuid ? `${quotedNew} IS NULL` : `(${quotedNew} IS NULL OR CAST(${quotedNew} AS TEXT) = '')`} AND ${quotedOld} IS NOT NULL AND CAST(${quotedOld} AS TEXT) <> ''`;
933
+ let suggestedSql;
934
+ let dropIdempotencyNote;
935
+ if (this.engine === "postgres") {
936
+ suggestedSql = [`DO $$ BEGIN IF ${`EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ${this.quoteLiteral(tableName)} AND column_name = ${this.quoteLiteral(oldColumn)})`} THEN ${copySql}; ${`ALTER TABLE ${quotedTable} DROP COLUMN ${quotedOld}`}; END IF; END $$`];
937
+ dropIdempotencyNote = `This single statement is idempotent: rerunning it after the rename is complete is a genuine no-op, because it only touches ${oldColumn} when the block finds it still exists.`;
938
+ } else {
939
+ suggestedSql = [
940
+ `SELECT count(*) AS old_column_still_present FROM pragma_table_info(${this.quoteLiteral(tableName)}) WHERE name = ${this.quoteLiteral(oldColumn)}`,
941
+ copySql,
942
+ this.generateDropColumnSQL(tableName, oldColumn)
943
+ ];
944
+ dropIdempotencyNote = `SQLite has no conditional-DDL construct, so this is operator-mediated, not self-contained idempotent SQL: run the guard query first, and only run the UPDATE and DROP below when it reports ${oldColumn} still present.`;
945
+ }
946
+ return {
947
+ type: "rename_data_pending",
948
+ table: tableName,
949
+ name: newColumn,
950
+ mismatch: {
951
+ expected: `data in ${newColumn}`,
952
+ actual: `data appears to still be in ${oldColumn}`
953
+ },
954
+ advisory: {
955
+ severity: "warning",
956
+ message: `${tableName}.${newColumn} is declared but empty, while undeclared column ${tableName}.${oldColumn} holds data of a compatible type. This looks like a framework field rename whose data was never moved (db:migrate adds columns additively and never drops the old one). ${dropIdempotencyNote}`,
957
+ suggestedSql
958
+ }
959
+ };
960
+ }
961
+ /**
962
+ * Render the advisory for an ambiguous rename-data-pending match (#2767
963
+ * review): more than one orphan column is a populated, type-compatible
964
+ * candidate for the same empty declared column, so the real rename source
965
+ * cannot be inferred. Unlike {@link describeRenameDataPending}, this
966
+ * carries no `suggestedSql` — running a per-candidate copy-and-drop would
967
+ * let output order decide which candidate's data wins and then drop every
968
+ * one of them, which is not a repair an advisory should suggest blindly.
969
+ */
970
+ describeRenameDataPendingAmbiguous(tableName, newColumn, candidateColumns) {
971
+ const candidateList = candidateColumns.map((name) => `${tableName}.${name}`).join(", ");
972
+ return {
973
+ type: "rename_data_pending",
974
+ table: tableName,
975
+ name: newColumn,
976
+ mismatch: {
977
+ expected: `data in ${newColumn}`,
978
+ actual: `data appears to still be in one of several columns: ${candidateList}`
979
+ },
980
+ advisory: {
981
+ severity: "warning",
982
+ message: `${tableName}.${newColumn} is declared but empty, while ${candidateColumns.length} undeclared columns of a compatible type hold data (${candidateList}). This looks like a framework field rename, but which column is the actual rename source is ambiguous — no suggested repair SQL is printed. An operator must identify the correct source column and write its own targeted copy-then-drop.`
983
+ }
984
+ };
985
+ }
986
+ /**
797
987
  * Compare nullability and default between a manifest column and the live
798
988
  * column, emitting `alter_column` changes (#2369).
799
989
  *