@happyvertical/smrt-core 0.47.2 → 0.49.0

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 (110) hide show
  1. package/agents/generators.md +12 -0
  2. package/agents/object-runtime.md +7 -0
  3. package/agents/schema-paths.md +62 -0
  4. package/agents/system-diagnostics.md +6 -1
  5. package/dist/cascade.d.ts.map +1 -1
  6. package/dist/cascade.js +9 -6
  7. package/dist/cascade.js.map +1 -1
  8. package/dist/collection.d.ts +0 -1
  9. package/dist/collection.d.ts.map +1 -1
  10. package/dist/collection.js +35 -45
  11. package/dist/collection.js.map +1 -1
  12. package/dist/decorators/compatibility.d.ts +1 -1
  13. package/dist/decorators/compatibility.d.ts.map +1 -1
  14. package/dist/decorators/compatibility.js +2 -2
  15. package/dist/decorators/compatibility.js.map +1 -1
  16. package/dist/decorators/index.d.ts.map +1 -1
  17. package/dist/decorators/index.js +33 -24
  18. package/dist/decorators/index.js.map +1 -1
  19. package/dist/interceptors.d.ts +3 -1
  20. package/dist/interceptors.d.ts.map +1 -1
  21. package/dist/interceptors.js +3 -2
  22. package/dist/interceptors.js.map +1 -1
  23. package/dist/manifest/static-manifest.js +1 -1
  24. package/dist/manifest/static-manifest.js.map +1 -1
  25. package/dist/manifest/store.js +1 -1
  26. package/dist/manifest.json +1 -1
  27. package/dist/migrations/differ.d.ts +10 -4
  28. package/dist/migrations/differ.d.ts.map +1 -1
  29. package/dist/migrations/differ.js +173 -5
  30. package/dist/migrations/differ.js.map +1 -1
  31. package/dist/object.d.ts +1 -5
  32. package/dist/object.d.ts.map +1 -1
  33. package/dist/object.js +28 -37
  34. package/dist/object.js.map +1 -1
  35. package/dist/registry/class-registration.d.ts +2 -1
  36. package/dist/registry/class-registration.d.ts.map +1 -1
  37. package/dist/registry/class-registration.js +47 -16
  38. package/dist/registry/class-registration.js.map +1 -1
  39. package/dist/registry/name-resolver.d.ts.map +1 -1
  40. package/dist/registry/name-resolver.js +3 -3
  41. package/dist/registry/name-resolver.js.map +1 -1
  42. package/dist/registry/relationship-graph.d.ts +2 -0
  43. package/dist/registry/relationship-graph.d.ts.map +1 -1
  44. package/dist/registry/relationship-graph.js +60 -5
  45. package/dist/registry/relationship-graph.js.map +1 -1
  46. package/dist/registry/schema-builder.d.ts.map +1 -1
  47. package/dist/registry/schema-builder.js +4 -1
  48. package/dist/registry/schema-builder.js.map +1 -1
  49. package/dist/registry/shared-state.d.ts +18 -0
  50. package/dist/registry/shared-state.d.ts.map +1 -1
  51. package/dist/registry/shared-state.js +25 -1
  52. package/dist/registry/shared-state.js.map +1 -1
  53. package/dist/registry/types.d.ts +8 -0
  54. package/dist/registry/types.d.ts.map +1 -1
  55. package/dist/registry.d.ts +32 -5
  56. package/dist/registry.d.ts.map +1 -1
  57. package/dist/registry.js +120 -12
  58. package/dist/registry.js.map +1 -1
  59. package/dist/relationship-loader.d.ts +6 -0
  60. package/dist/relationship-loader.d.ts.map +1 -0
  61. package/dist/relationship-loader.js +35 -0
  62. package/dist/relationship-loader.js.map +1 -0
  63. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  64. package/dist/scanner/manifest-generator.js +6 -3
  65. package/dist/scanner/manifest-generator.js.map +1 -1
  66. package/dist/schema/foreign-key-ddl.d.ts +8 -0
  67. package/dist/schema/foreign-key-ddl.d.ts.map +1 -1
  68. package/dist/schema/foreign-key-ddl.js +12 -1
  69. package/dist/schema/foreign-key-ddl.js.map +1 -1
  70. package/dist/schema/foreign-key-orphan-report.d.ts.map +1 -1
  71. package/dist/schema/foreign-key-orphan-report.js +18 -1
  72. package/dist/schema/foreign-key-orphan-report.js.map +1 -1
  73. package/dist/schema/generator.d.ts +2 -0
  74. package/dist/schema/generator.d.ts.map +1 -1
  75. package/dist/schema/generator.js +16 -9
  76. package/dist/schema/generator.js.map +1 -1
  77. package/dist/schema/index.d.ts +1 -1
  78. package/dist/schema/index.d.ts.map +1 -1
  79. package/dist/schema/index.js +2 -2
  80. package/dist/schema/live-parity.d.ts.map +1 -1
  81. package/dist/schema/live-parity.js +74 -0
  82. package/dist/schema/live-parity.js.map +1 -1
  83. package/dist/schema/text-cast-probe.d.ts +66 -0
  84. package/dist/schema/text-cast-probe.d.ts.map +1 -0
  85. package/dist/schema/text-cast-probe.js +142 -0
  86. package/dist/schema/text-cast-probe.js.map +1 -0
  87. package/dist/schema/types.d.ts +28 -0
  88. package/dist/schema/types.d.ts.map +1 -1
  89. package/dist/schema/utils.d.ts.map +1 -1
  90. package/dist/schema/utils.js +2 -1
  91. package/dist/schema/utils.js.map +1 -1
  92. package/dist/schema.js +2 -2
  93. package/dist/smrt-knowledge.json +11 -11
  94. package/dist/system/diagnostics.d.ts +22 -0
  95. package/dist/system/diagnostics.d.ts.map +1 -1
  96. package/dist/system/diagnostics.js +88 -31
  97. package/dist/system/diagnostics.js.map +1 -1
  98. package/dist/test-utils.d.ts.map +1 -1
  99. package/dist/utils.d.ts.map +1 -1
  100. package/dist/utils.js +2 -1
  101. package/dist/utils.js.map +1 -1
  102. package/dist/vite-plugin/dev-plane-route.d.ts +5 -0
  103. package/dist/vite-plugin/dev-plane-route.d.ts.map +1 -0
  104. package/dist/vite-plugin/dev-plane-route.js +91 -0
  105. package/dist/vite-plugin/dev-plane-route.js.map +1 -0
  106. package/dist/vite-plugin/sveltekit-generator.d.ts +8 -0
  107. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  108. package/dist/vite-plugin/sveltekit-generator.js +2 -0
  109. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  110. package/package.json +4 -4
@@ -1,7 +1,8 @@
1
1
  import { normalizeForeignKeyAction, requireForeignKeyAction } from "../schema/foreign-key-policy.js";
2
2
  import { isJsonPathIndex, isStiSubtypeUniqueIndex, renderIndexTarget } from "../schema/index-utils.js";
3
- import { CANONICAL_UUID_PATTERN, CANONICAL_UUID_SQLITE_GLOB_PATTERN, 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, renderForeignKeyAddStatements, renderForeignKeyOrphanDetector, renderForeignKeyOrphanRepair, schemaForeignKeys, schemaForeignKeysForEngine } from "../schema/foreign-key-ddl.js";
4
4
  import { detectEngine, getDDLStrategy } from "../schema/ddl/index.js";
5
+ import { maskSampleValue, probeCastSafety, renderJsonbColumnConversion, renderTimestamptzColumnConversion } from "../schema/text-cast-probe.js";
5
6
  import { describeForeignKeyTypeConflict, planUuidConvergence, renderUuidConvergenceRepairHint, renderUuidShapeProbe } from "../schema/uuid-convergence.js";
6
7
  import "../schema/utils.js";
7
8
  import { planSqliteTableRebuilds, sqliteRebuildPlaceholderSql } from "./sqlite-rebuild.js";
@@ -64,6 +65,18 @@ function uniqueColumnIndexName(tableName, columnName) {
64
65
  * True when a change is report-only: it carries an advisory and no
65
66
  * executable statement. Such changes never reach the migration stream.
66
67
  */
68
+ /**
69
+ * Single- vs double-precision float classification for #2770's float-width
70
+ * drift detection. `null` for anything that isn't unambiguously one or the
71
+ * other (DECIMAL/NUMERIC have no fixed binary width and are out of scope).
72
+ */
73
+ function floatPrecisionOf(type, engine) {
74
+ const upper = type.toUpperCase().trim().replace(/\(\s*\d+(\s*,\s*\d+)?\s*\)/g, "");
75
+ if (/^(REAL|FLOAT4)$/.test(upper)) return "single";
76
+ if (engine === "duckdb" && upper === "FLOAT") return "single";
77
+ if (/^(FLOAT8|DOUBLE|DOUBLE PRECISION|FLOAT)$/.test(upper)) return "double";
78
+ return null;
79
+ }
67
80
  function isAdvisoryOnlyChange(change) {
68
81
  if (!change.advisory) return false;
69
82
  return (change.sqlStatements ?? (change.sql ? [change.sql] : [])).length === 0;
@@ -475,6 +488,18 @@ var SchemaComparer = class {
475
488
  }
476
489
  }
477
490
  /**
491
+ * Probe a candidate `text` -> `timestamptz`/`jsonb` column (#2771, #2772)
492
+ * with a real, exception-safe cast attempt (see `text-cast-probe.ts`).
493
+ * Unlike {@link probeUuidShape}, a failed/unrealistic probe
494
+ * (`unavailable`) is the caller's cue to fall back to the pre-existing
495
+ * behavior for that pair rather than surface a new finding — this keeps
496
+ * the new convergence strictly additive for every case that isn't
497
+ * affirmatively confirmed safe or affirmatively confirmed unsafe.
498
+ */
499
+ async probeTextCastShape(tableName, columnName, kind) {
500
+ return probeCastSafety(this.db, tableName, columnName, kind);
501
+ }
502
+ /**
478
503
  * Turn the convergence plan into diff entries: executable `type_upgrade`
479
504
  * conversions marked `phase: 'pre_foreign_key'`, plus report-only warnings
480
505
  * for components the planner refused.
@@ -608,6 +633,7 @@ var SchemaComparer = class {
608
633
  table: tableName,
609
634
  name: foreignKeyConstraintName(tableName, foreignKey),
610
635
  foreignKey,
636
+ engineUnsupported: true,
611
637
  advisory: {
612
638
  severity: "warning",
613
639
  message: `${engineReason} Run the orphan detector, repair rows, and rebuild the table with the generated constraint.`,
@@ -645,6 +671,8 @@ var SchemaComparer = class {
645
671
  table: tableName,
646
672
  name: foreignKeyConstraintName(tableName, foreignKey),
647
673
  foreignKey,
674
+ orphanBlocked: true,
675
+ orphanNullable: orphanOptions.nullable,
648
676
  advisory: {
649
677
  severity: "warning",
650
678
  message: `Cannot add foreign key ${tableName}.${foreignKey.column}: existing rows do not match ${foreignKey.referencesTable}.${foreignKey.referencesColumn}. Repair them, then rerun.`,
@@ -659,7 +687,7 @@ var SchemaComparer = class {
659
687
  table: tableName,
660
688
  name: constraintName,
661
689
  foreignKey,
662
- sqlStatements: [`ALTER TABLE ${this.quoteIdentifier(tableName)} ADD ${renderForeignKeyConstraint(tableName, foreignKey)} NOT VALID`, `ALTER TABLE ${this.quoteIdentifier(tableName)} VALIDATE CONSTRAINT ${this.quoteIdentifier(constraintName)}`]
690
+ sqlStatements: renderForeignKeyAddStatements(tableName, foreignKey)
663
691
  });
664
692
  }
665
693
  return changes;
@@ -680,7 +708,9 @@ var SchemaComparer = class {
680
708
  async getForeignKeyOrphanOptions(tableName, manifest, dbSchema, foreignKey, manifestSchemas) {
681
709
  const sourceColumn = manifest.columns[foreignKey.column];
682
710
  const targetColumn = manifestSchemas[foreignKey.referencesTable]?.columns[foreignKey.referencesColumn];
683
- const nullable = sourceColumn?.notNull !== true;
711
+ const manifestNullable = sourceColumn?.notNull !== true;
712
+ const liveNotNull = dbSchema.columns[foreignKey.column]?.notNull === true;
713
+ const nullable = manifestNullable && !liveNotNull;
684
714
  const uuidComparison = sourceColumn?.type === "UUID" && (targetColumn === void 0 || targetColumn.type === "UUID");
685
715
  if (!uuidComparison) return {
686
716
  nullable,
@@ -743,10 +773,148 @@ var SchemaComparer = class {
743
773
  const expectedEngineType = this.ddlStrategy.mapType(validatedType);
744
774
  const normalizedExpected = this.normalizeType(expectedEngineType);
745
775
  const normalizedActual = this.normalizeType(dbCol.type);
776
+ if ((this.engine === "postgres" || this.engine === "duckdb") && normalizedExpected === "REAL" && normalizedActual === "REAL") {
777
+ const expectedPrecision = floatPrecisionOf(expectedEngineType, this.engine);
778
+ const actualPrecision = floatPrecisionOf(dbCol.type, this.engine);
779
+ if (expectedPrecision && actualPrecision && expectedPrecision !== actualPrecision) {
780
+ typeDrifted = true;
781
+ const table = this.quoteIdentifier(tableName);
782
+ const column = this.quoteIdentifier(colName);
783
+ if (expectedPrecision === "double" && actualPrecision === "single") {
784
+ const targetType = this.engine === "postgres" ? "DOUBLE PRECISION" : "DOUBLE";
785
+ changes.push({
786
+ type: "type_upgrade",
787
+ table: tableName,
788
+ name: colName,
789
+ column: colDef,
790
+ mismatch: {
791
+ expected: expectedEngineType,
792
+ actual: dbCol.type
793
+ },
794
+ sql: `ALTER TABLE ${table} ALTER COLUMN ${column} TYPE ${targetType} USING ${column}::${targetType}`
795
+ });
796
+ } else changes.push({
797
+ type: "type_upgrade",
798
+ table: tableName,
799
+ name: colName,
800
+ column: colDef,
801
+ mismatch: {
802
+ expected: expectedEngineType,
803
+ actual: dbCol.type
804
+ },
805
+ advisory: {
806
+ severity: "warning",
807
+ message: `blocked: ${tableName}.${colName} is declared single-precision (${expectedEngineType}) but the live column is double-precision (${dbCol.type}). Narrowing loses precision, so this stays manual: confirm the narrower declaration is intentional, or widen the manifest field instead of the column.`,
808
+ suggestedSql: [`ALTER TABLE ${table} ALTER COLUMN ${column} TYPE ${expectedEngineType} USING ${column}::${expectedEngineType}`]
809
+ }
810
+ });
811
+ }
812
+ }
746
813
  const isUuidTextEquivalent = this.isUuidTextEquivalentColumn(colName, colDef, normalizedExpected, normalizedActual);
747
- if (normalizedExpected !== normalizedActual && !isUuidTextEquivalent && !(normalizedExpected === "JSON" && normalizedActual === "TEXT" || normalizedExpected === "TEXT" && normalizedActual === "JSON")) {
814
+ const jsonUpgradeCandidate = this.engine === "postgres" && normalizedExpected === "JSON" && normalizedActual === "TEXT";
815
+ let jsonProbe;
816
+ if (jsonUpgradeCandidate) jsonProbe = await this.probeTextCastShape(tableName, colName, "jsonb");
817
+ const isJsonTextEquivalent = normalizedExpected === "TEXT" && normalizedActual === "JSON" || normalizedExpected === "JSON" && normalizedActual === "TEXT" && (!jsonUpgradeCandidate || jsonProbe?.status === "unavailable");
818
+ const timestamptzUpgradeCandidate = this.engine === "postgres" && this.normalizeType(colDef.type) === "TIMESTAMP" && normalizedActual === "TEXT" && normalizedExpected === "TIMESTAMPTZ" && !this.isCompatibleTypeUpgrade(colDef.type, dbCol.type);
819
+ let timestamptzProbe;
820
+ if (timestamptzUpgradeCandidate) timestamptzProbe = await this.probeTextCastShape(tableName, colName, "timestamptz");
821
+ if (normalizedExpected !== normalizedActual && !isUuidTextEquivalent && !isJsonTextEquivalent) {
748
822
  typeDrifted = true;
749
- if (this.isCompatibleTypeUpgrade(colDef.type, dbCol.type)) {
823
+ const hasLiveDefault = dbCol.defaultValue !== null && dbCol.defaultValue !== void 0;
824
+ const conversionOptions = {
825
+ hasLiveDefault,
826
+ manifestDefaultValue: colDef.defaultValue
827
+ };
828
+ const liveOnlyDefaultNeedsRelaxOptIn = hasLiveDefault && colDef.defaultValue === void 0 && !this.options.relaxColumns;
829
+ if (jsonUpgradeCandidate && jsonProbe?.status === "clean" && liveOnlyDefaultNeedsRelaxOptIn) changes.push({
830
+ type: "type_upgrade",
831
+ table: tableName,
832
+ name: colName,
833
+ column: colDef,
834
+ mismatch: {
835
+ expected: colDef.type,
836
+ actual: dbCol.type
837
+ },
838
+ advisory: {
839
+ severity: "warning",
840
+ message: `blocked: ${tableName}.${colName} has a live default (${String(dbCol.defaultValue)}) the manifest no longer declares; converging to jsonb requires dropping it first. ${this.relaxHint("drop it as part of this conversion")}`,
841
+ suggestedSql: renderJsonbColumnConversion(tableName, colName, conversionOptions)
842
+ }
843
+ });
844
+ else if (jsonUpgradeCandidate && jsonProbe?.status === "clean") {
845
+ const statements = renderJsonbColumnConversion(tableName, colName, conversionOptions);
846
+ changes.push({
847
+ type: "type_upgrade",
848
+ table: tableName,
849
+ name: colName,
850
+ column: colDef,
851
+ mismatch: {
852
+ expected: colDef.type,
853
+ actual: dbCol.type
854
+ },
855
+ sql: statements[statements.length - 1],
856
+ sqlStatements: statements
857
+ });
858
+ } else if (jsonUpgradeCandidate && jsonProbe?.status === "dirty") changes.push({
859
+ type: "type_upgrade",
860
+ table: tableName,
861
+ name: colName,
862
+ column: colDef,
863
+ mismatch: {
864
+ expected: colDef.type,
865
+ actual: dbCol.type
866
+ },
867
+ advisory: {
868
+ severity: "warning",
869
+ message: `blocked: ${tableName}.${colName} is declared JSON but ${jsonProbe.count} live value(s) are not valid JSON (sample: ${jsonProbe.sample ? maskSampleValue(jsonProbe.sample) : "unavailable"}). Repair or clear the offending value(s), then rerun \`smrt db:migrate\`.`,
870
+ suggestedSql: renderJsonbColumnConversion(tableName, colName, conversionOptions)
871
+ }
872
+ });
873
+ else if (timestamptzUpgradeCandidate && timestamptzProbe?.status === "clean" && liveOnlyDefaultNeedsRelaxOptIn) changes.push({
874
+ type: "type_upgrade",
875
+ table: tableName,
876
+ name: colName,
877
+ column: colDef,
878
+ mismatch: {
879
+ expected: colDef.type,
880
+ actual: dbCol.type
881
+ },
882
+ advisory: {
883
+ severity: "warning",
884
+ message: `blocked: ${tableName}.${colName} has a live default (${String(dbCol.defaultValue)}) the manifest no longer declares; converging to timestamptz requires dropping it first. ${this.relaxHint("drop it as part of this conversion")}`,
885
+ suggestedSql: renderTimestamptzColumnConversion(tableName, colName, conversionOptions)
886
+ }
887
+ });
888
+ else if (timestamptzUpgradeCandidate && timestamptzProbe?.status === "clean") {
889
+ const statements = renderTimestamptzColumnConversion(tableName, colName, conversionOptions);
890
+ changes.push({
891
+ type: "type_upgrade",
892
+ table: tableName,
893
+ name: colName,
894
+ column: colDef,
895
+ mismatch: {
896
+ expected: colDef.type,
897
+ actual: dbCol.type
898
+ },
899
+ sql: statements[statements.length - 1],
900
+ sqlStatements: statements
901
+ });
902
+ } else if (timestamptzUpgradeCandidate && timestamptzProbe?.status === "dirty") changes.push({
903
+ type: "type_upgrade",
904
+ table: tableName,
905
+ name: colName,
906
+ column: colDef,
907
+ mismatch: {
908
+ expected: colDef.type,
909
+ actual: dbCol.type
910
+ },
911
+ advisory: {
912
+ severity: "warning",
913
+ message: `blocked: ${tableName}.${colName} is declared TIMESTAMP but ${timestamptzProbe.count} live value(s) do not parse as an unambiguous timestamp (sample: ${timestamptzProbe.sample ? maskSampleValue(timestamptzProbe.sample) : "unavailable"}). Repair the offending value(s), or confirm legacy naive wall-clock provenance with \`smrt db:migrate --postgres-timestamp-legacy-timezone=UTC\`, then rerun.`,
914
+ suggestedSql: renderTimestamptzColumnConversion(tableName, colName, conversionOptions)
915
+ }
916
+ });
917
+ else if (this.isCompatibleTypeUpgrade(colDef.type, dbCol.type)) {
750
918
  const generatedSQL = this.generateTypeUpgradeSQL(tableName, colName, colDef, dbCol.type);
751
919
  changes.push({
752
920
  type: "type_upgrade",