@happyvertical/smrt-cli 0.51.39 → 0.52.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 (48) hide show
  1. package/AGENTS.md +6 -2
  2. package/README.md +21 -0
  3. package/agents/type-drift.md +16 -0
  4. package/dist/{ApiKey-BZmbOTnL-DEwZtcGA.js → ApiKey-BZmbOTnL-CMUo3WLf.js} +2 -2
  5. package/dist/ApiKeyCollection-Bfb_nw81-C3Xz7MUq.js +2 -0
  6. package/dist/AuditLogCollection-B1YonCAP-DmmboC_X.js +2 -0
  7. package/dist/{NostrIdentity-n70E0vck-DiBJPv76.js → NostrIdentity-n70E0vck-BD7uutxz.js} +3 -3
  8. package/dist/NostrIdentityCollection-Ca74dkaL--KYlM_ou.js +2 -0
  9. package/dist/{OidcIdentity-K8V4u0CR-B_ek3cPN.js → OidcIdentity-Cb1bxreM-DY0aRLu-.js} +3 -3
  10. package/dist/OidcIdentityCollection-TV8VdK69-DPeFJt3R.js +2 -0
  11. package/dist/ProfileAssetCollection-B9hacUJa-6R8lwjti.js +2 -0
  12. package/dist/{ProfileCollection-DTT4KNpQ-D1RNYfjm.js → ProfileCollection-DuJT7Mum-D_ScbSMu.js} +69 -31
  13. package/dist/ProfileLinkCollection-4uNURz32-lMKBjaik.js +2 -0
  14. package/dist/ProfileMetadataCollection-DQhbfEfa-B1QlmBkX.js +2 -0
  15. package/dist/{ProfileMetafieldCollection-CMKhVi2I-mh3UgeZA.js → ProfileMetafieldCollection-CoBuM_5G-BDwo4x3z.js} +1 -1
  16. package/dist/{ProfileRelationshipCollection-DUVgaIqf-CXHrAni4.js → ProfileRelationshipCollection-DUVgaIqf-BRrBeg1Q.js} +1 -1
  17. package/dist/{ProfileRelationshipTermCollection-DtA9OM23-Cj4bLTpU.js → ProfileRelationshipTermCollection-DtA9OM23-D4WMfN9a.js} +1 -1
  18. package/dist/{ProfileRelationshipType-BRiRka6t-ICv2cawn.js → ProfileRelationshipType-BRiRka6t-B32A4UFT.js} +1 -1
  19. package/dist/{ProfileRelationshipTypeCollection-BdXwd7Kf-CaYLxJmh.js → ProfileRelationshipTypeCollection-BdXwd7Kf-BqQa8Awc.js} +1 -1
  20. package/dist/commands/db-diff.d.ts.map +1 -1
  21. package/dist/commands/db-history.d.ts.map +1 -1
  22. package/dist/commands/db-migrate-actions.d.ts +7 -0
  23. package/dist/commands/db-migrate-actions.d.ts.map +1 -1
  24. package/dist/commands/db-parity.d.ts +7 -0
  25. package/dist/commands/db-parity.d.ts.map +1 -1
  26. package/dist/commands/db-status.d.ts.map +1 -1
  27. package/dist/commands/tenant-natural-keys.d.ts +58 -0
  28. package/dist/commands/tenant-natural-keys.d.ts.map +1 -0
  29. package/dist/commands/utilities.d.ts.map +1 -1
  30. package/dist/{commands-C2PjCUio.js → commands-BvcMfEoO.js} +165 -18
  31. package/dist/{dist-B3npUhW6.js → dist-BTxdRxKP.js} +151 -6
  32. package/dist/{dist-C8sNOzr2.js → dist-Dm4q4uB4.js} +4 -4
  33. package/dist/dist-qJw7ALwV.js +1701 -0
  34. package/dist/dist-vJJL0WMc.js +3937 -0
  35. package/dist/index.js +9 -9
  36. package/dist/{json-validator-oEN7eUI3.js → json-validator-BiAdsE1O.js} +4 -4
  37. package/dist/{oidcProvisioningPrimitives-0-3n3D6f-yUBRl2DC.js → oidcProvisioningPrimitives-D9OYu5-F-D9DQdiwS.js} +7 -7
  38. package/dist/resolveIdentity-B-PK6RwU-Dvqeq9-I.js +2 -0
  39. package/package.json +14 -14
  40. package/dist/ApiKeyCollection-Bfb_nw81-D4pLjDGS.js +0 -2
  41. package/dist/AuditLogCollection-B1YonCAP-w1iOHW4A.js +0 -2
  42. package/dist/NostrIdentityCollection-Ca74dkaL-Ce-O_zQz.js +0 -2
  43. package/dist/OidcIdentityCollection-gnrLoF08-DJI-69Ow.js +0 -2
  44. package/dist/ProfileAssetCollection-CmbeSgDi-BpJ7XXsP.js +0 -2
  45. package/dist/ProfileMetadataCollection-DxX_BJKV-orYFlYii.js +0 -2
  46. package/dist/dist-DsMohSo_.js +0 -1487
  47. package/dist/dist-iMaSv6C_.js +0 -3712
  48. package/dist/resolveIdentity-BrJDCjYi-kdAssLr0.js +0 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-natural-keys.d.ts","sourceRoot":"","sources":["../../src/commands/tenant-natural-keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,yEAAyE;AACzE,KAAK,aAAa,GAAG;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,4CAA4C;AAC5C,MAAM,MAAM,aAAa,GAAG;IAAE,OAAO,CAAC,EAAE,aAAa,EAAE,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,CAAC;AAE7E;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE;QACP,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,MAAM,EAAE,CAAC;QAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,eAAe,GAAG,sBAAsB,GAAG,uBAAuB,CAAC;KAC1E,CAAC;CACH;AAMD;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,GAC9C,uBAAuB,EAAE,CAwG3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../../src/commands/utilities.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../../src/commands/utilities.ts"],"names":[],"mappings":"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"}
@@ -13,7 +13,7 @@ import { createLogger } from "@happyvertical/logger";
13
13
  import glob from "fast-glob";
14
14
  import { createHash } from "node:crypto";
15
15
  import { getPackageConfig } from "@happyvertical/smrt-config";
16
- import { toSnakeCase } from "@happyvertical/smrt-core/utils";
16
+ import { toSnakeCase as toSnakeCase$1 } from "@happyvertical/smrt-core/utils";
17
17
  import { checkKnowledgeGraphFreshness, discoverScopedPackageDirectories, readAgentModuleDocs, readPackageAgentDoc, resolveAgentModuleDocPaths } from "@happyvertical/smrt-core/knowledge";
18
18
  import { MCPGenerator } from "@happyvertical/smrt-core/generators";
19
19
  import { generateDeclarationsFromCLI } from "@happyvertical/smrt-core/prebuild";
@@ -767,7 +767,8 @@ function partitionSchemaChanges(changes, getClassForTable) {
767
767
  ...change.foreignKey ? { foreignKey: change.foreignKey } : {},
768
768
  ...change.orphanBlocked !== void 0 ? { orphanBlocked: change.orphanBlocked } : {},
769
769
  ...change.orphanNullable !== void 0 ? { orphanNullable: change.orphanNullable } : {},
770
- ...change.engineUnsupported !== void 0 ? { engineUnsupported: change.engineUnsupported } : {}
770
+ ...change.engineUnsupported !== void 0 ? { engineUnsupported: change.engineUnsupported } : {},
771
+ ...change.note ? { note: change.note } : {}
771
772
  };
772
773
  if (isAdvisoryOnlyChangeLike(change)) manualInterventions.push(action);
773
774
  else migrations.push(action);
@@ -833,7 +834,8 @@ function partitionSchemaChanges(changes, getClassForTable) {
833
834
  actual: mm.actual
834
835
  },
835
836
  sql: change.sql,
836
- ...change.sqlStatements ? { sqlStatements: change.sqlStatements } : {}
837
+ ...change.sqlStatements ? { sqlStatements: change.sqlStatements } : {},
838
+ ...change.note ? { note: change.note } : {}
837
839
  };
838
840
  const executionKind = classifyTypeUpgradeSql(change.sql);
839
841
  if (executionKind === "executable") migrations.push(action);
@@ -1230,6 +1232,11 @@ var dbDiffCommand = {
1230
1232
  description: "Include orphan-index drops in the diff (indexes in DB but not in the manifest, excluding *_pkey/*_key implicit-from-constraint indexes). Off by default for safety.",
1231
1233
  default: false
1232
1234
  },
1235
+ "drop-legacy-natural-key": {
1236
+ type: "boolean",
1237
+ description: "Include the drop of the legacy global (slug, context) unique index a tenant-led natural key superseded (see db:migrate --drop-legacy-natural-key).",
1238
+ default: false
1239
+ },
1233
1240
  "drop-columns": {
1234
1241
  type: "boolean",
1235
1242
  description: "Include orphan-column drops in the diff (columns in DB but not in the manifest). Off by default; orphans are always reported.",
@@ -1243,6 +1250,11 @@ var dbDiffCommand = {
1243
1250
  "postgres-timestamp-legacy-timezone": {
1244
1251
  type: "string",
1245
1252
  description: "Confirm that legacy PostgreSQL timestamp-without-time-zone values are UTC wall times before previewing their conversion to timestamptz. Exact value required: UTC; omitted by default."
1253
+ },
1254
+ "empty-text-as-null": {
1255
+ type: "boolean",
1256
+ description: "Preview storing empty or whitespace-only text as NULL when converging a nullable legacy text column to a typed column (timestamp, JSON, integer). Any other value that does not convert still blocks. Off by default; the diff names the empty-text count instead.",
1257
+ default: false
1246
1258
  }
1247
1259
  },
1248
1260
  handler: async (_args, options) => {
@@ -1298,8 +1310,10 @@ var dbDiffCommand = {
1298
1310
  includeDroppedTables: false,
1299
1311
  includeDroppedColumns: Boolean(options["drop-columns"]),
1300
1312
  includeDroppedIndexes: Boolean(options["drop-indexes"]),
1313
+ dropLegacyNaturalKey: Boolean(options["drop-legacy-natural-key"]),
1301
1314
  relaxColumns: Boolean(options["relax-columns"]),
1302
- postgresTimestampMigration
1315
+ postgresTimestampMigration,
1316
+ emptyTextAsNull: Boolean(options["empty-text-as-null"])
1303
1317
  }).compare(schemaDefinitions);
1304
1318
  if (options.json) {
1305
1319
  console.log(JSON.stringify({
@@ -1394,9 +1408,21 @@ var dbDiffCommand = {
1394
1408
  }
1395
1409
  if (typeUpgrades.length > 0) {
1396
1410
  console.log(` 🔁 Type upgrades (${typeUpgrades.length}):`);
1397
- for (const change of typeUpgrades) console.log(` ⤴ ${change.table}.${change.name}: ${change.mismatch?.actual} → ${change.mismatch?.expected}`);
1411
+ for (const change of typeUpgrades) {
1412
+ console.log(` ⤴ ${change.table}.${change.name}: ${change.mismatch?.actual} → ${change.mismatch?.expected}`);
1413
+ if (change.note) console.log(` (${change.note})`);
1414
+ }
1398
1415
  console.log(" (auto-applied by smrt db:migrate)\n");
1399
1416
  }
1417
+ const foreignKeyChanges = diff.changes.filter((c) => c.type === "add_foreign_key" && (c.sql || c.sqlStatements?.length));
1418
+ if (foreignKeyChanges.length > 0) {
1419
+ console.log(` 🔗 Foreign keys (${foreignKeyChanges.length}):`);
1420
+ for (const change of foreignKeyChanges) {
1421
+ console.log(` + ${change.name} on ${change.table}${change.note ? ` (${change.note})` : ""}`);
1422
+ if (change.advisory?.severity === "warning") console.log(` ⚠️ ${change.advisory.message}`);
1423
+ }
1424
+ console.log();
1425
+ }
1400
1426
  if (typeMismatches.length > 0) {
1401
1427
  console.log(` ⚠️ Type mismatches (${typeMismatches.length}):`);
1402
1428
  for (const change of typeMismatches) console.log(` ! ${change.table}.${change.name}: ${change.mismatch?.expected} vs ${change.mismatch?.actual}`);
@@ -1581,6 +1607,11 @@ var dbHistoryCommand = {
1581
1607
  description: "Show full details including checksums",
1582
1608
  default: false,
1583
1609
  short: "v"
1610
+ },
1611
+ "empty-text-as-null": {
1612
+ type: "boolean",
1613
+ description: "Assess the same convergence `db:migrate --empty-text-as-null` would apply: a nullable legacy text column whose only obstacle is empty or whitespace-only text is pending, not blocked. Off by default.",
1614
+ default: false
1584
1615
  }
1585
1616
  },
1586
1617
  handler: async (_args, options) => {
@@ -1619,7 +1650,7 @@ var dbHistoryCommand = {
1619
1650
  if (failedHistory.length > 0 && typeof db.getTableSchema === "function") try {
1620
1651
  await autoDiscoverAndLoad();
1621
1652
  const manifestSchemas = ObjectRegistry.getAllSchemasAsDefinitions();
1622
- const diff = await new SchemaComparer(db).compare(manifestSchemas);
1653
+ const diff = await new SchemaComparer(db, { emptyTextAsNull: Boolean(options["empty-text-as-null"]) }).compare(manifestSchemas);
1623
1654
  liveSchemaCompared = true;
1624
1655
  failedAssessments = assessFailedMigrations(failedHistory, diff);
1625
1656
  } catch {
@@ -2868,7 +2899,7 @@ async function evaluateSchemaContract(options) {
2868
2899
  present = (await ObjectRegistry.getAllFields(entry.name)).has(fieldName);
2869
2900
  if (present) {
2870
2901
  table = inferTableName(entry);
2871
- column = toSnakeCase(fieldName);
2902
+ column = toSnakeCase$1(fieldName);
2872
2903
  if (table) {
2873
2904
  inferredTables.add(table);
2874
2905
  inferredColumns.add(`${table}.${column}`);
@@ -2928,6 +2959,99 @@ function assertSchemaContract(report) {
2928
2959
  if (!report.ok) throw new SchemaContractError(report);
2929
2960
  }
2930
2961
  //#endregion
2962
+ //#region src/commands/tenant-natural-keys.ts
2963
+ /**
2964
+ * Tenant natural-key detector for `db:status`.
2965
+ *
2966
+ * A tenant-owned table (declared tenant scope, or an undeclared `tenantId`
2967
+ * field — `ObjectRegistry.getOwnershipTenantColumn()`) upserts on a
2968
+ * tenant-led key such as `(tenant_id, slug, context)`. A live database created
2969
+ * before that default can still carry the GLOBAL `(slug, context)` unique:
2970
+ * then a second tenant's same-slug save collides with a row it cannot see.
2971
+ * Before the save-time ownership guard, `ON CONFLICT … DO UPDATE` rewrote that
2972
+ * row's `id` and `tenant_id` (the Anytown Ludis takeover); now the save is
2973
+ * refused, and on engines that require an exact ON CONFLICT target the create
2974
+ * fails until `smrt db:migrate` swaps the index.
2975
+ *
2976
+ * Rollout is expand, then contract: `smrt db:migrate` builds the tenant-led
2977
+ * unique beside the global one (an `error` until it exists, so db:status exits
2978
+ * 1), and the global one stays — reported as a `warning` — until
2979
+ * `smrt db:migrate --drop-legacy-natural-key` once no old code is running.
2980
+ * Explicit `@smrt({ conflictColumns })` that omit the tenant column are the
2981
+ * author's declared contract and are not reported here (the tenancy
2982
+ * package's `auditTenantScopedRegistrations()` flags runtime-registered ones).
2983
+ */
2984
+ function sameColumnSet(a, b) {
2985
+ return a.length === b.length && a.every((column) => b.includes(column));
2986
+ }
2987
+ /**
2988
+ * Compare every tenant-owned table's upsert target with its live unique
2989
+ * indexes. Tables absent from `liveSchemas` (not created yet, or not
2990
+ * introspected) are skipped — the schema diff reports those.
2991
+ */
2992
+ function checkTenantNaturalKeyUniques(liveSchemas) {
2993
+ const findings = [];
2994
+ const seenTables = /* @__PURE__ */ new Set();
2995
+ for (const className of ObjectRegistry.getQualifiedClassNames()) {
2996
+ if (isCollectionRegistration(className)) continue;
2997
+ const tableName = ObjectRegistry.getTableName(className);
2998
+ if (!tableName || seenTables.has(tableName)) continue;
2999
+ const tenantColumn = ObjectRegistry.getOwnershipTenantColumn(className);
3000
+ if (!tenantColumn) continue;
3001
+ const conflictColumns = ObjectRegistry.getConflictColumns(className);
3002
+ if (!conflictColumns.includes(tenantColumn)) continue;
3003
+ const live = liveSchemas.get(tableName);
3004
+ if (!live) continue;
3005
+ seenTables.add(tableName);
3006
+ const uniques = (live.indexes ?? []).filter((index) => index.unique === true && !index.where);
3007
+ const naturalKey = conflictColumns.filter((column) => column !== tenantColumn);
3008
+ const global = uniques.find((index) => sameColumnSet(index.columns ?? [], naturalKey));
3009
+ const target = `(${conflictColumns.join(", ")})`;
3010
+ if (uniques.some((index) => sameColumnSet(index.columns ?? [], conflictColumns))) {
3011
+ if (global) findings.push({
3012
+ name: `${tableName}.${global.name ?? "(unnamed unique)"}`,
3013
+ status: "warning",
3014
+ message: `Tenant-owned table "${tableName}" (${className}) still carries the legacy unique index on (${naturalKey.join(", ")}) beside the tenant-led ${target}. Until it is dropped, a second tenant cannot store a key another tenant already uses (the save fails with a unique violation; nothing is overwritten).`,
3015
+ recommendation: "Once every running instance is on the release that upserts on the tenant-led key, run `smrt db:migrate --drop-legacy-natural-key` to drop it.",
3016
+ details: {
3017
+ className,
3018
+ tableName,
3019
+ conflictColumns,
3020
+ liveIndex: global.name,
3021
+ kind: "legacy_global_unique"
3022
+ }
3023
+ });
3024
+ continue;
3025
+ }
3026
+ if (global) findings.push({
3027
+ name: `${tableName}.${global.name ?? "(unnamed unique)"}`,
3028
+ status: "error",
3029
+ message: `Tenant-owned table "${tableName}" (${className}) has a unique index on (${naturalKey.join(", ")}) shared by every tenant, but the model upserts on ${target}. A second tenant cannot store a key another tenant already uses; saves that collide are refused (TENANT_ISOLATION_VIOLATION), and creates can fail outright until the index matches the upsert target.`,
3030
+ recommendation: "Run `smrt db:migrate` BEFORE deploying the release that upserts on the tenant-led key: it builds the tenant-inclusive unique under its own name (a superset, so it cannot fail on existing rows) and keeps this one, so old code (ON CONFLICT on the global key) and new code (ON CONFLICT on the tenant-led key) both keep working. After the rollout, `smrt db:migrate --drop-legacy-natural-key` drops the legacy index.",
3031
+ details: {
3032
+ className,
3033
+ tableName,
3034
+ conflictColumns,
3035
+ liveIndex: global.name,
3036
+ kind: "global_unique"
3037
+ }
3038
+ });
3039
+ else findings.push({
3040
+ name: `${tableName}.${target}`,
3041
+ status: "warning",
3042
+ message: `Tenant-owned table "${tableName}" (${className}) has no unique index on ${target}, the key the model upserts on.`,
3043
+ recommendation: "Run `smrt db:migrate` to create the tenant-inclusive unique index.",
3044
+ details: {
3045
+ className,
3046
+ tableName,
3047
+ conflictColumns,
3048
+ kind: "missing_tenant_unique"
3049
+ }
3050
+ });
3051
+ }
3052
+ return findings;
3053
+ }
3054
+ //#endregion
2931
3055
  //#region src/commands/db-status.ts
2932
3056
  /**
2933
3057
  * db:status Command
@@ -3148,6 +3272,11 @@ var dbStatusCommand = {
3148
3272
  description: "Also compare the live schema to the expected shape (incl. _smrt_* system tables and index policy)",
3149
3273
  default: false,
3150
3274
  short: "p"
3275
+ },
3276
+ "empty-text-as-null": {
3277
+ type: "boolean",
3278
+ description: "Assess the same convergence `db:migrate --empty-text-as-null` would apply: a nullable legacy text column whose only obstacle is empty or whitespace-only text is pending, not blocked. Off by default.",
3279
+ default: false
3151
3280
  }
3152
3281
  },
3153
3282
  handler: async (_args, options) => {
@@ -3246,16 +3375,16 @@ var dbStatusCommand = {
3246
3375
  const manifestSchemas = ObjectRegistry.getAllSchemasAsDefinitions();
3247
3376
  let comparer;
3248
3377
  if (typeof db.getTableSchema === "function") {
3249
- comparer = new SchemaComparer(db);
3378
+ comparer = new SchemaComparer(db, { emptyTextAsNull: Boolean(options["empty-text-as-null"]) });
3250
3379
  diff = await comparer.compare(manifestSchemas);
3251
3380
  status.drift = summarizeSchemaDiff(diff);
3252
3381
  status.notes = summarizeSchemaNotes(diff);
3253
- status.preconditions = await checkTenantIdUuidPreconditions({
3382
+ status.preconditions = [...await checkTenantIdUuidPreconditions({
3254
3383
  db,
3255
3384
  dbType,
3256
3385
  dbUrl,
3257
3386
  manifestSchemas
3258
- });
3387
+ }), ...checkTenantNaturalKeyUniques(comparer.getLiveSchemaSnapshot())];
3259
3388
  status.failedMigrations = summarizeFailedMigrations(failed, getUnresolvedGeneratedMigrationNames(diff.changes));
3260
3389
  if (options.parity) try {
3261
3390
  status.parity = await checkLiveSchemaParity({
@@ -4139,14 +4268,14 @@ var docsCommands = {
4139
4268
  * This replaces per-agent export commands (praeco export, caelus export, etc.)
4140
4269
  * with a single, schema-driven export that respects field-level export annotations.
4141
4270
  */
4142
- function toSnakeCase$1(str) {
4271
+ function toSnakeCase$2(str) {
4143
4272
  return str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
4144
4273
  }
4145
4274
  function toColumnName(fieldName) {
4146
4275
  const dotIndex = fieldName.indexOf(".");
4147
4276
  const baseFieldName = dotIndex >= 0 ? fieldName.substring(0, dotIndex) : fieldName;
4148
4277
  const jsonPath = dotIndex >= 0 ? fieldName.substring(dotIndex) : "";
4149
- return `${baseFieldName.startsWith("_") ? `_${toSnakeCase$1(baseFieldName.slice(1))}` : toSnakeCase$1(baseFieldName)}${jsonPath}`;
4278
+ return `${baseFieldName.startsWith("_") ? `_${toSnakeCase$2(baseFieldName.slice(1))}` : toSnakeCase$2(baseFieldName)}${jsonPath}`;
4150
4279
  }
4151
4280
  function assertSafeIdentifier(identifier, label) {
4152
4281
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/.test(identifier)) throw new Error(`Invalid ${label}: ${identifier}`);
@@ -6948,7 +7077,7 @@ var dbMaterializeTenantHierarchyCommand = {
6948
7077
  if (!config.database?.url || config.database.url === ":memory:") throw new Error("Database configuration required for db:materialize-tenant-hierarchy. Configure database.url in smrt.config.ts (or DATABASE_URL).");
6949
7078
  const dbType = config.database.type || "sqlite";
6950
7079
  const { getDatabase } = await import("@happyvertical/sql");
6951
- const { materializeTenantHierarchy, TenantHierarchyMaterializationError } = await import("./dist-iMaSv6C_.js");
7080
+ const { materializeTenantHierarchy, TenantHierarchyMaterializationError } = await import("./dist-vJJL0WMc.js");
6952
7081
  db = await getDatabase({
6953
7082
  type: dbType,
6954
7083
  url: config.database.url
@@ -7157,7 +7286,7 @@ var dbMigrateLedgerAccountsCommand = {
7157
7286
  if (dbType !== "postgres") throw new Error("db:migrate-ledger-accounts currently supports PostgreSQL only; no changes were made. See the smrt-ledgers README for SQLite.");
7158
7287
  await autoDiscoverAndLoad();
7159
7288
  const { getDatabase } = await import("@happyvertical/sql");
7160
- const { migrateLedgerAccountsTable, planLedgerAccountsTableMove } = await import("./dist-C8sNOzr2.js");
7289
+ const { migrateLedgerAccountsTable, planLedgerAccountsTableMove } = await import("./dist-Dm4q4uB4.js");
7161
7290
  db = await getDatabase({
7162
7291
  type: dbType,
7163
7292
  url: config.database.url
@@ -9876,7 +10005,7 @@ export default testManifest;
9876
10005
  }
9877
10006
  }
9878
10007
  try {
9879
- const { resolveDataPath, discoverJsonFiles, JsonDatabaseValidator, displayValidationResults } = await import("./json-validator-oEN7eUI3.js");
10008
+ const { resolveDataPath, discoverJsonFiles, JsonDatabaseValidator, displayValidationResults } = await import("./json-validator-BiAdsE1O.js");
9880
10009
  const dataPath = await resolveDataPath(options.data);
9881
10010
  if (!dataPath) {
9882
10011
  console.error("\n❌ Could not find data directory");
@@ -9998,6 +10127,11 @@ export default testManifest;
9998
10127
  type: "string",
9999
10128
  description: "Confirm that legacy PostgreSQL timestamp-without-time-zone values are UTC wall times before converting them to timestamptz. Exact value required: UTC; omitted by default."
10000
10129
  },
10130
+ "empty-text-as-null": {
10131
+ type: "boolean",
10132
+ description: "Store empty or whitespace-only text as NULL when converging a nullable legacy text column to a typed column (timestamp, JSON, integer). Any other value that does not convert still blocks, and NOT NULL columns are never changed. Off by default.",
10133
+ default: false
10134
+ },
10001
10135
  force: {
10002
10136
  type: "boolean",
10003
10137
  description: "Force re-apply even if already applied (skip checksum validation)",
@@ -10023,6 +10157,11 @@ export default testManifest;
10023
10157
  description: "Drop orphan indexes (in DB but not in manifest, excluding *_pkey/*_key implicit-from-constraint indexes). Off by default.",
10024
10158
  default: false
10025
10159
  },
10160
+ "drop-legacy-natural-key": {
10161
+ type: "boolean",
10162
+ description: "Drop the legacy global (slug, context) unique index a tenant-led natural key superseded on a table that is tenant-owned by its tenantId field. Off by default: db:migrate builds the tenant-led index beside it so code still on the previous release keeps upserting. Pass this once every instance runs the release that upserts on the tenant-led key.",
10163
+ default: false
10164
+ },
10026
10165
  "drop-columns": {
10027
10166
  type: "boolean",
10028
10167
  description: "DESTRUCTIVE: drop orphan columns (in DB but not in manifest). Off by default; orphans are always reported.",
@@ -10171,8 +10310,10 @@ export default testManifest;
10171
10310
  const diff = await new SchemaComparer(db, {
10172
10311
  includeDroppedIndexes: Boolean(options["drop-indexes"]),
10173
10312
  includeDroppedColumns: Boolean(options["drop-columns"]),
10313
+ dropLegacyNaturalKey: Boolean(options["drop-legacy-natural-key"]),
10174
10314
  relaxColumns: Boolean(options["relax-columns"]),
10175
- postgresTimestampMigration
10315
+ postgresTimestampMigration,
10316
+ emptyTextAsNull: Boolean(options["empty-text-as-null"])
10176
10317
  }).compare(manifestSchemas);
10177
10318
  const engine = tracker.getEngine();
10178
10319
  const tablePlan = planForeignKeyCreation(diff.added_tables, engine);
@@ -10255,6 +10396,12 @@ export default testManifest;
10255
10396
  pendingOrphanDispositions.length = 0;
10256
10397
  pendingOrphanDispositions.push(...stillPending);
10257
10398
  }
10399
+ const destructiveForeignKeys = migrations.filter((migration) => migration.type === "add_foreign_key" && migration.advisory?.severity === "warning" && Boolean(migration.sql || migration.sqlStatements?.length));
10400
+ if (destructiveForeignKeys.length > 0) {
10401
+ console.log(`\n⚠️ ${destructiveForeignKeys.length} foreign-key change(s) make deletes DESTRUCTIVE:`);
10402
+ for (const migration of destructiveForeignKeys) console.log(` ${migration.advisory?.message}`);
10403
+ console.log();
10404
+ }
10258
10405
  const preForeignKeyMigrations = migrations.filter((migration) => migration.phase === "pre_foreign_key");
10259
10406
  const remainingMigrations = migrations.filter((migration) => migration.phase !== "pre_foreign_key");
10260
10407
  assertForceMigrationTargetsExist(forceSelection.forceMigrations, [
@@ -10407,7 +10554,7 @@ export default testManifest;
10407
10554
  actionDesc = `Dropped column ${migration.tableName}.${migration.columnName}`;
10408
10555
  } else if (migration.type === "type_upgrade" && migration.column) {
10409
10556
  migrationSql = migration.sql || "";
10410
- actionDesc = `Upgraded column ${migration.tableName}.${migration.column.name} from ${migration.mismatch?.actual} to ${migration.mismatch?.expected}`;
10557
+ actionDesc = `Upgraded column ${migration.tableName}.${migration.column.name} from ${migration.mismatch?.actual} to ${migration.mismatch?.expected}${migration.note ? ` (${migration.note})` : ""}`;
10411
10558
  } else if (migration.type === "add_index" && migration.index) {
10412
10559
  migrationSql = migration.sql || "";
10413
10560
  actionDesc = `Created index ${migration.index.name} on ${migration.tableName}`;
@@ -10416,7 +10563,7 @@ export default testManifest;
10416
10563
  actionDesc = `Dropped index ${migration.indexName} on ${migration.tableName}`;
10417
10564
  } else if (migration.type === "add_foreign_key") {
10418
10565
  migrationSql = migration.sql || "";
10419
- actionDesc = `Added foreign-key constraint on ${migration.tableName}`;
10566
+ actionDesc = migration.note ? `Replaced foreign-key constraint on ${migration.tableName} (${migration.note})` : `Added foreign-key constraint on ${migration.tableName}`;
10420
10567
  } else if (migration.type === "drop_foreign_key") {
10421
10568
  migrationSql = migration.sql || "";
10422
10569
  actionDesc = `Dropped foreign-key constraint on ${migration.tableName}`;