@happyvertical/smrt-core 0.43.10 → 0.44.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 (47) hide show
  1. package/agents/generators.md +70 -0
  2. package/agents/schema-paths.md +99 -0
  3. package/dist/generators/custom-action.d.ts.map +1 -1
  4. package/dist/generators/custom-action.js +1 -1
  5. package/dist/generators/custom-action.js.map +1 -1
  6. package/dist/index.js +2 -2
  7. package/dist/knowledge.d.ts +17 -1
  8. package/dist/knowledge.d.ts.map +1 -1
  9. package/dist/knowledge.js +33 -3
  10. package/dist/knowledge.js.map +1 -1
  11. package/dist/manifest/static-manifest.js +1 -1
  12. package/dist/manifest/static-manifest.js.map +1 -1
  13. package/dist/manifest/store.js +1 -1
  14. package/dist/manifest.json +1 -1
  15. package/dist/migrations/differ.d.ts +49 -0
  16. package/dist/migrations/differ.d.ts.map +1 -1
  17. package/dist/migrations/differ.js +220 -4
  18. package/dist/migrations/differ.js.map +1 -1
  19. package/dist/migrations/orchestrate.d.ts +23 -0
  20. package/dist/migrations/orchestrate.d.ts.map +1 -1
  21. package/dist/migrations/orchestrate.js +12 -3
  22. package/dist/migrations/orchestrate.js.map +1 -1
  23. package/dist/registry/types.d.ts +8 -3
  24. package/dist/registry/types.d.ts.map +1 -1
  25. package/dist/schema/foreign-key-ddl.d.ts +7 -0
  26. package/dist/schema/foreign-key-ddl.d.ts.map +1 -1
  27. package/dist/schema/foreign-key-ddl.js +7 -1
  28. package/dist/schema/foreign-key-ddl.js.map +1 -1
  29. package/dist/schema/schema-manager.d.ts +15 -0
  30. package/dist/schema/schema-manager.d.ts.map +1 -1
  31. package/dist/schema/schema-manager.js +52 -9
  32. package/dist/schema/schema-manager.js.map +1 -1
  33. package/dist/schema/types.d.ts +10 -0
  34. package/dist/schema/types.d.ts.map +1 -1
  35. package/dist/schema/uuid-convergence.d.ts +142 -0
  36. package/dist/schema/uuid-convergence.d.ts.map +1 -0
  37. package/dist/schema/uuid-convergence.js +0 -0
  38. package/dist/schema/uuid-convergence.js.map +1 -0
  39. package/dist/smrt-knowledge.json +7 -7
  40. package/dist/utils/scanner-module.d.ts +44 -0
  41. package/dist/utils/scanner-module.d.ts.map +1 -1
  42. package/dist/vite-plugin/index.d.ts +14 -1
  43. package/dist/vite-plugin/index.d.ts.map +1 -1
  44. package/dist/vite-plugin/index.js +54 -2
  45. package/dist/vite-plugin/index.js.map +1 -1
  46. package/dist/vite-plugin.js +3 -2
  47. package/package.json +4 -4
@@ -2,6 +2,7 @@ import { normalizeForeignKeyAction, requireForeignKeyAction } from "../schema/fo
2
2
  import { isJsonPathIndex, isStiSubtypeUniqueIndex, renderIndexTarget } from "../schema/index-utils.js";
3
3
  import { foreignKeyConstraintName, foreignKeyRelationshipKey, renderForeignKeyConstraint, renderForeignKeyOrphanDetector, renderForeignKeyOrphanRepair, schemaForeignKeys, schemaForeignKeysForEngine } from "../schema/foreign-key-ddl.js";
4
4
  import { detectEngine, getDDLStrategy } from "../schema/ddl/index.js";
5
+ import { describeForeignKeyTypeConflict, planUuidConvergence, renderUuidConvergenceRepairHint, renderUuidShapeProbe } from "../schema/uuid-convergence.js";
5
6
  import "../schema/utils.js";
6
7
  import { planSqliteTableRebuilds, sqliteRebuildPlaceholderSql } from "./sqlite-rebuild.js";
7
8
  import { createLogger } from "@happyvertical/logger";
@@ -291,6 +292,15 @@ var SchemaComparer = class {
291
292
  options;
292
293
  engine;
293
294
  ddlStrategy;
295
+ /**
296
+ * Live schemas read during one `compare()` run. The uuid convergence plan
297
+ * (#2608) needs the same introspection the per-table comparison does, and
298
+ * re-reading `getTableSchema` per relationship would multiply catalog
299
+ * round-trips on a wide schema.
300
+ */
301
+ liveSchemas = /* @__PURE__ */ new Map();
302
+ /** Convergence plan for this run; `null` on non-PostgreSQL engines. */
303
+ uuidConvergence = null;
294
304
  constructor(db, options = {}) {
295
305
  this.db = db;
296
306
  this.options = {
@@ -313,7 +323,13 @@ var SchemaComparer = class {
313
323
  changes: [],
314
324
  has_changes: false
315
325
  };
326
+ this.liveSchemas.clear();
316
327
  const existingTables = await this.getExistingTables();
328
+ this.uuidConvergence = await this.buildUuidConvergencePlan(manifestSchemas, existingTables);
329
+ for (const change of this.uuidConvergenceChanges(manifestSchemas)) {
330
+ diff.changes.push(change);
331
+ if (!isInfoOnlyChange(change)) diff.has_changes = true;
332
+ }
317
333
  for (const [tableName, schema] of Object.entries(manifestSchemas)) if (!existingTables.has(tableName)) {
318
334
  diff.added_tables.push(schema);
319
335
  diff.has_changes = true;
@@ -342,7 +358,7 @@ var SchemaComparer = class {
342
358
  */
343
359
  async compareTable(tableName, manifest, manifestSchemas = {}) {
344
360
  const changes = [];
345
- const dbSchema = await this.db.getTableSchema?.(tableName);
361
+ const dbSchema = await this.getLiveSchema(tableName);
346
362
  if (!dbSchema) return changes;
347
363
  const columnChanges = await this.compareColumns(tableName, manifest, dbSchema);
348
364
  changes.push(...this.engine === "sqlite" ? await planSqliteTableRebuilds({
@@ -357,6 +373,173 @@ var SchemaComparer = class {
357
373
  changes.push(...await this.compareForeignKeys(tableName, manifest, dbSchema, manifestSchemas));
358
374
  return changes;
359
375
  }
376
+ /**
377
+ * Read (and memoize for this run) one table's live schema. Returns
378
+ * `undefined` when the table does not exist or the adapter cannot
379
+ * introspect it.
380
+ */
381
+ async getLiveSchema(tableName) {
382
+ if (this.liveSchemas.has(tableName)) return this.liveSchemas.get(tableName) ?? void 0;
383
+ const schema = await this.db.getTableSchema?.(tableName);
384
+ this.liveSchemas.set(tableName, schema);
385
+ return schema ?? void 0;
386
+ }
387
+ /**
388
+ * Plan the pre-R11 `text` -> `uuid` convergence (#2608).
389
+ *
390
+ * PostgreSQL only: SQLite stores UUIDs as text by design and DuckDB cannot
391
+ * rewrite a column type in place, so both engines return `null` and emit
392
+ * nothing for this drift.
393
+ */
394
+ async buildUuidConvergencePlan(manifestSchemas, existingTables) {
395
+ if (this.engine !== "postgres") return null;
396
+ for (const tableName of Object.keys(manifestSchemas)) {
397
+ if (!existingTables.has(tableName)) continue;
398
+ await this.getLiveSchema(tableName);
399
+ }
400
+ const relationships = Object.entries(manifestSchemas).flatMap(([tableName, schema]) => schemaForeignKeysForEngine(schema, "postgres").map((foreignKey) => ({
401
+ childTable: tableName,
402
+ childColumn: foreignKey.column,
403
+ parentTable: foreignKey.referencesTable,
404
+ parentColumn: foreignKey.referencesColumn
405
+ })));
406
+ const conversionCandidates = /* @__PURE__ */ new Set();
407
+ for (const relationship of relationships) {
408
+ const childManifest = manifestSchemas[relationship.childTable]?.columns[relationship.childColumn];
409
+ const parentManifest = manifestSchemas[relationship.parentTable]?.columns[relationship.parentColumn];
410
+ if (childManifest?.type !== "UUID" || parentManifest?.type !== "UUID") continue;
411
+ for (const [table, column] of [[relationship.childTable, relationship.childColumn], [relationship.parentTable, relationship.parentColumn]]) {
412
+ if (!existingTables.has(table)) continue;
413
+ const live = this.liveSchemas.get(table)?.columns[column];
414
+ if (!live) continue;
415
+ if (this.normalizeType(live.type) !== "TEXT") continue;
416
+ conversionCandidates.add(`${table} ${column}`);
417
+ }
418
+ }
419
+ if (conversionCandidates.size > 0) for (const tableName of existingTables) {
420
+ if (manifestSchemas[tableName]) continue;
421
+ if (tableName.startsWith("sqlite_")) continue;
422
+ await this.getLiveSchema(tableName);
423
+ }
424
+ const liveForeignKeys = [];
425
+ for (const [tableName, schema] of this.liveSchemas) for (const live of schema?.foreignKeys ?? []) liveForeignKeys.push({
426
+ table: tableName,
427
+ column: live.column,
428
+ referencesTable: live.referencesTable,
429
+ referencesColumn: live.referencesColumn
430
+ });
431
+ return planUuidConvergence({
432
+ manifestSchemas,
433
+ relationships,
434
+ liveForeignKeys,
435
+ getLiveColumn: (table, column) => {
436
+ if (!existingTables.has(table)) return void 0;
437
+ const live = this.liveSchemas.get(table)?.columns[column];
438
+ if (!live) return void 0;
439
+ return {
440
+ normalizedType: this.normalizeType(live.type),
441
+ rawType: live.type,
442
+ hasDefault: live.defaultValue !== null && live.defaultValue !== void 0,
443
+ exists: true
444
+ };
445
+ },
446
+ probeUuidShape: (table, column) => this.probeUuidShape(table, column)
447
+ });
448
+ }
449
+ /**
450
+ * Count values a legacy `text` identifier column holds that cannot be
451
+ * reinterpreted as `uuid`. Any failure resolves to `unavailable`, which the
452
+ * planner treats as a block — SMRT never coerces identifier values.
453
+ */
454
+ async probeUuidShape(tableName, columnName) {
455
+ try {
456
+ const result = await this.db.query(renderUuidShapeProbe(tableName, columnName));
457
+ const rows = Array.isArray(result) ? result : result.rows || [];
458
+ const count = Number(rows[0]?.invalid_count ?? 0);
459
+ if (!Number.isFinite(count)) return {
460
+ status: "unavailable",
461
+ reason: "probe returned a non-numeric count"
462
+ };
463
+ if (count === 0) return { status: "clean" };
464
+ const sample = rows[0]?.sample_value;
465
+ return {
466
+ status: "dirty",
467
+ count,
468
+ ...typeof sample === "string" ? { sample } : {}
469
+ };
470
+ } catch (error) {
471
+ return {
472
+ status: "unavailable",
473
+ reason: error instanceof Error ? error.message : String(error)
474
+ };
475
+ }
476
+ }
477
+ /**
478
+ * Turn the convergence plan into diff entries: executable `type_upgrade`
479
+ * conversions marked `phase: 'pre_foreign_key'`, plus report-only warnings
480
+ * for components the planner refused.
481
+ */
482
+ uuidConvergenceChanges(manifestSchemas) {
483
+ const plan = this.uuidConvergence;
484
+ if (!plan) return [];
485
+ const manifestColumn = (table, column) => manifestSchemas[table]?.columns[column] ?? { type: "UUID" };
486
+ const changes = [];
487
+ for (const conversion of plan.conversions) changes.push({
488
+ type: "type_upgrade",
489
+ table: conversion.table,
490
+ name: conversion.column,
491
+ column: manifestColumn(conversion.table, conversion.column),
492
+ phase: "pre_foreign_key",
493
+ mismatch: {
494
+ expected: "UUID",
495
+ actual: conversion.liveType
496
+ },
497
+ sql: conversion.statements[0],
498
+ sqlStatements: conversion.statements
499
+ });
500
+ for (const block of plan.blocks) {
501
+ const [first] = block.targets.length > 0 ? block.targets : block.nodes;
502
+ changes.push({
503
+ type: "type_upgrade",
504
+ table: first?.table ?? "",
505
+ name: first?.column,
506
+ column: manifestColumn(first?.table ?? "", first?.column ?? ""),
507
+ phase: "pre_foreign_key",
508
+ mismatch: {
509
+ expected: "UUID",
510
+ actual: "mixed uuid/text"
511
+ },
512
+ advisory: {
513
+ severity: "warning",
514
+ message: `blocked: incompatible column types. The manifest declares UUID for ${block.nodes.map((node) => `${node.table}.${node.column}`).join(", ")}, but ${block.reason}.`,
515
+ suggestedSql: block.suggestedSql
516
+ }
517
+ });
518
+ }
519
+ return changes;
520
+ }
521
+ /**
522
+ * Live-type verdict for one foreign key once this run's conversions land.
523
+ * `undefined` means the pair is compatible (or unknowable) and the ordinary
524
+ * add path applies.
525
+ */
526
+ async describeForeignKeyTypeBlock(tableName, dbSchema, foreignKey) {
527
+ if (this.engine !== "postgres") return void 0;
528
+ const parentSchema = foreignKey.referencesTable === tableName ? dbSchema : await this.getLiveSchema(foreignKey.referencesTable);
529
+ const childType = dbSchema.columns[foreignKey.column]?.type;
530
+ const parentType = parentSchema?.columns[foreignKey.referencesColumn]?.type;
531
+ if (!childType || !parentType) return void 0;
532
+ const plan = this.uuidConvergence;
533
+ if ((plan?.effectiveType(tableName, foreignKey.column) ?? this.normalizeType(childType)) === (plan?.effectiveType(foreignKey.referencesTable, foreignKey.referencesColumn) ?? this.normalizeType(parentType))) return void 0;
534
+ return describeForeignKeyTypeConflict({
535
+ childTable: tableName,
536
+ childColumn: foreignKey.column,
537
+ childType,
538
+ parentTable: foreignKey.referencesTable,
539
+ parentColumn: foreignKey.referencesColumn,
540
+ parentType
541
+ });
542
+ }
360
543
  async compareForeignKeys(tableName, manifest, dbSchema, manifestSchemas) {
361
544
  const changes = [];
362
545
  const liveForeignKeys = dbSchema.foreignKeys || [];
@@ -433,6 +616,29 @@ var SchemaComparer = class {
433
616
  });
434
617
  continue;
435
618
  }
619
+ const typeBlock = await this.describeForeignKeyTypeBlock(tableName, dbSchema, foreignKey);
620
+ if (typeBlock) {
621
+ const textNodes = manifest.columns[foreignKey.column]?.type === "UUID" && manifestSchemas[foreignKey.referencesTable]?.columns[foreignKey.referencesColumn]?.type === "UUID" ? [this.normalizeType(dbSchema.columns[foreignKey.column]?.type ?? "") === "TEXT" ? {
622
+ table: tableName,
623
+ column: foreignKey.column
624
+ } : void 0, this.normalizeType((foreignKey.referencesTable === tableName ? dbSchema : this.liveSchemas.get(foreignKey.referencesTable))?.columns[foreignKey.referencesColumn]?.type ?? "") === "TEXT" ? {
625
+ table: foreignKey.referencesTable,
626
+ column: foreignKey.referencesColumn
627
+ } : void 0].filter((node) => node !== void 0) : [];
628
+ const suggestedSql = renderUuidConvergenceRepairHint(textNodes);
629
+ changes.push({
630
+ type: "add_foreign_key",
631
+ table: tableName,
632
+ name: foreignKeyConstraintName(tableName, foreignKey),
633
+ foreignKey,
634
+ advisory: {
635
+ severity: "warning",
636
+ message: `blocked: incompatible column types. ${typeBlock}. ` + (textNodes.length > 0 ? "Converge the columns to UUID, then rerun the migration." : "Align both columns on one physical type deliberately, then rerun the migration."),
637
+ ...suggestedSql.length > 0 ? { suggestedSql } : {}
638
+ }
639
+ });
640
+ continue;
641
+ }
436
642
  if (Boolean(dbSchema.columns[foreignKey.column]) && await this.foreignKeyHasOrphans(tableName, foreignKey, orphanOptions)) {
437
643
  changes.push({
438
644
  type: "add_foreign_key",
@@ -480,7 +686,7 @@ var SchemaComparer = class {
480
686
  nullable,
481
687
  uuidComparison
482
688
  };
483
- const parentSchema = foreignKey.referencesTable === tableName ? dbSchema : await this.db.getTableSchema?.(foreignKey.referencesTable);
689
+ const parentSchema = foreignKey.referencesTable === tableName ? dbSchema : await this.getLiveSchema(foreignKey.referencesTable);
484
690
  const childType = dbSchema.columns[foreignKey.column]?.type;
485
691
  const parentType = parentSchema?.columns[foreignKey.referencesColumn]?.type;
486
692
  if (!childType || !parentType) return {
@@ -1389,11 +1595,21 @@ function hasActionableChanges(diff) {
1389
1595
  * they already do for `type_upgrade`.
1390
1596
  */
1391
1597
  function getSQLFromDiff(diff) {
1598
+ return getSQLFromChanges(diff.changes);
1599
+ }
1600
+ /**
1601
+ * Executable SQL for an explicit subset of changes, in the order given.
1602
+ * Callers that have to interleave diff changes with statements produced
1603
+ * elsewhere — the orchestrator, which emits `CREATE TABLE` and deferred
1604
+ * foreign keys of its own — partition `diff.changes` by
1605
+ * {@link SchemaChange.phase} and render each part through this helper.
1606
+ */
1607
+ function getSQLFromChanges(changes) {
1392
1608
  const statements = [];
1393
- for (const change of diff.changes) if (change.type !== "type_mismatch" && !isAdvisoryOnlyChange(change)) statements.push(...change.sqlStatements ?? (change.sql ? [change.sql] : []));
1609
+ for (const change of changes) if (change.type !== "type_mismatch" && !isAdvisoryOnlyChange(change)) statements.push(...change.sqlStatements ?? (change.sql ? [change.sql] : []));
1394
1610
  return statements;
1395
1611
  }
1396
1612
  //#endregion
1397
- export { SchemaComparer, canonicalizeDefault, extractIndexPredicate, generateSchemaDiff, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, normalizeIndexPredicate, uniqueColumnIndexName };
1613
+ export { SchemaComparer, canonicalizeDefault, extractIndexPredicate, generateSchemaDiff, getSQLFromChanges, getSQLFromDiff, hasActionableChanges, isAdvisoryOnlyChange, isInfoOnlyChange, isManualOrAdvisoryChange, normalizeIndexPredicate, uniqueColumnIndexName };
1398
1614
 
1399
1615
  //# sourceMappingURL=differ.js.map