@runa-ai/runa-cli 0.10.0 → 0.10.1

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 (37) hide show
  1. package/dist/{chunk-ZWDWFMOX.js → chunk-HWR5NUUZ.js} +24 -3
  2. package/dist/{chunk-JQXOVCOP.js → chunk-NIS77243.js} +8 -5
  3. package/dist/{chunk-URWDB7YL.js → chunk-O3M7A73M.js} +58 -2
  4. package/dist/{chunk-QDOR3GTD.js → chunk-XRLIZKB2.js} +80 -12
  5. package/dist/{chunk-IEKYTCYA.js → chunk-YTQS2O4H.js} +59 -0
  6. package/dist/{chunk-OXQISY3J.js → chunk-ZPE52NEK.js} +1 -1
  7. package/dist/{ci-FLTJ2UXB.js → ci-3HZWUQFN.js} +4 -4
  8. package/dist/{cli-THEA6T7N.js → cli-RES5QRC2.js} +12 -12
  9. package/dist/commands/db/apply/helpers/pg-schema-diff-helpers.d.ts +6 -0
  10. package/dist/commands/db/commands/db-sync/production-precheck.d.ts +0 -8
  11. package/dist/commands/db/sync/schema-guardrail-graph-guidance.d.ts +18 -1
  12. package/dist/commands/db/sync/schema-guardrail-graph-nodes.d.ts +1 -1
  13. package/dist/commands/db/sync/schema-guardrail-graph-sql-helpers.d.ts +1 -1
  14. package/dist/commands/db/sync/schema-guardrail-types.d.ts +4 -2
  15. package/dist/commands/db/utils/changed-files-detector.d.ts +21 -0
  16. package/dist/commands/db/utils/schema-sync.d.ts +12 -0
  17. package/dist/commands/db/utils/sql-boundary-parser.d.ts +13 -0
  18. package/dist/commands/db/utils/sql-file-collector.d.ts +2 -0
  19. package/dist/constants/versions.d.ts +9 -0
  20. package/dist/{db-IDKQ44VX.js → db-PRGL7PBX.js} +587 -76
  21. package/dist/{dev-LGSMDFJN.js → dev-QR55VDNZ.js} +1 -1
  22. package/dist/{error-handler-YRQWRDEF.js → error-handler-XUQOP4TU.js} +1 -2
  23. package/dist/{hotfix-RJIAPLAM.js → hotfix-JYHDY2M6.js} +1 -2
  24. package/dist/index.js +4 -4
  25. package/dist/{init-2O6ODG5Z.js → init-4UAWYY75.js} +1 -1
  26. package/dist/{license-OB7GVJQ2.js → license-M6ODBV4X.js} +140 -154
  27. package/dist/pg-schema-diff-helpers-JZO4GAQG.js +7 -0
  28. package/dist/{upgrade-QZKEI3NJ.js → upgrade-LBO3Z3J7.js} +1 -1
  29. package/dist/utils/license/index.d.ts +15 -24
  30. package/dist/utils/license/types.d.ts +3 -4
  31. package/dist/utils/template-access.d.ts +20 -0
  32. package/dist/utils/template-fetcher.d.ts +10 -7
  33. package/dist/{vuln-check-JRPMUHLF.js → vuln-check-5JJ2YAJW.js} +1 -1
  34. package/dist/{vuln-checker-Q7LSHUHJ.js → vuln-checker-JF5234BL.js} +1 -1
  35. package/package.json +1 -1
  36. package/dist/chunk-ZZOXM6Q4.js +0 -8
  37. package/dist/pg-schema-diff-helpers-7377FS2D.js +0 -7
@@ -2,22 +2,21 @@
2
2
  import { createRequire } from 'module';
3
3
  import { detectDatabaseStack, getStackPaths } from './chunk-MILCC3B6.js';
4
4
  import { categorizeRisks, detectSchemaRisks } from './chunk-PAWNJA3N.js';
5
- import { isExecaError, resolveDbPreviewEnvironment, buildDbPlanCommandLabel, runDbApply, buildDbApplyCliError, DbPlanOutputSchema, parseDbPreviewProfile, DEFAULT_DB_PREVIEW_PROFILE, getDbPreviewModeLabel, buildDbPreviewCommandLabel, isCompareOnlyPreviewProfile, DbApplyOutputSchema, applyCommand, getDbPreviewIdempotentSchemaCount, classifyDbSyncCommandFailure, getDbSyncFallbackSuggestions, detectAppSchemas, normalizeDatabaseUrlForDdl, analyzeDuplicateFunctionOwnership, formatDuplicateFunctionOwnershipFinding, reviewDeclarativeDependencyWarnings, logDeclarativeDependencyWarnings, buildDeclarativeDependencyWarningFailureLines, parsePlanOutput, validateDependencyOrder, getBoundaryPolicy, resolveProductionApplyStrictMode, findDeclarativeRiskAllowlistMatch, assertBoundaryPolicyUsable, assertBoundaryPolicyQualityGate, formatSchemasForSql, findDirectoryPlacementAllowlistMatch, formatAllowlistMetadata, assessPlanSize, formatPlanSizeSummary, extractFunctionOwnershipDefinition } from './chunk-QDOR3GTD.js';
6
- import './chunk-ZZOXM6Q4.js';
7
- import { createError } from './chunk-JQXOVCOP.js';
5
+ import { isExecaError, resolveDbPreviewEnvironment, buildDbPlanCommandLabel, runDbApply, buildDbApplyCliError, DbPlanOutputSchema, parseDbPreviewProfile, DEFAULT_DB_PREVIEW_PROFILE, getDbPreviewModeLabel, buildDbPreviewCommandLabel, isCompareOnlyPreviewProfile, DbApplyOutputSchema, applyCommand, getDbPreviewIdempotentSchemaCount, classifyDbSyncCommandFailure, getDbSyncFallbackSuggestions, detectAppSchemas, normalizeDatabaseUrlForDdl, analyzeDuplicateFunctionOwnership, formatDuplicateFunctionOwnershipFinding, reviewDeclarativeDependencyWarnings, logDeclarativeDependencyWarnings, buildDeclarativeDependencyWarningFailureLines, parsePlanOutput, validateDependencyOrder, getBoundaryPolicy, resolveProductionApplyStrictMode, findDeclarativeRiskAllowlistMatch, assertBoundaryPolicyUsable, assertBoundaryPolicyQualityGate, formatSchemasForSql, findDirectoryPlacementAllowlistMatch, formatAllowlistMetadata, assessPlanSize, formatPlanSizeSummary, extractFunctionOwnershipDefinition } from './chunk-XRLIZKB2.js';
6
+ import { createError } from './chunk-NIS77243.js';
8
7
  import { resolveDatabaseUrl, resolveDatabaseTarget, tryResolveDatabaseUrl } from './chunk-WGRVAGSR.js';
9
8
  export { resolveDatabaseUrl, tryResolveDatabaseUrl } from './chunk-WGRVAGSR.js';
10
- import { analyzeDeclarativeDependencyContract, formatDeclarativeDependencyViolation, parseSqlFilename, collectSqlFiles, splitSqlStatements, extractFirstDollarBody, sanitizeExecutableCode, FUNCTION_DEFINITION_RE, blankQuotedStrings, sanitizeExecutableCodePreserveStrings, countNewlines, shouldReviewUnknownDeclarativeDdl, extractDdlObject, isNonSchemaOperation, isNonDdlMaintenanceStatement, shouldReviewUnknownIdempotentDdl } from './chunk-ZWDWFMOX.js';
9
+ import { analyzeDeclarativeDependencyContract, formatDeclarativeDependencyViolation, parseSqlFilename, collectSqlFiles, splitSqlStatements, ALLOW_DYNAMIC_SQL_ANNOTATION, extractFirstDollarBody, sanitizeExecutableCode, detectExtensionFilePath, FUNCTION_DEFINITION_RE, blankQuotedStrings, sanitizeExecutableCodePreserveStrings, countNewlines, shouldReviewUnknownDeclarativeDdl, extractDdlObject, isNonSchemaOperation, isNonDdlMaintenanceStatement, shouldReviewUnknownIdempotentDdl } from './chunk-HWR5NUUZ.js';
11
10
  import './chunk-UHDAYPHH.js';
12
11
  import './chunk-Y5ANTCKE.js';
13
12
  import { loadEnvFiles } from './chunk-IWVXI5O4.js';
14
- import './chunk-OXQISY3J.js';
13
+ import './chunk-ZPE52NEK.js';
15
14
  import { diagnoseSupabaseStart } from './chunk-AAIE4F2U.js';
16
15
  import { validateUserFilePath, filterSafePaths, resolveSafePath } from './chunk-B7C7CLW2.js';
17
16
  import { runMachine } from './chunk-QDF7QXBL.js';
18
17
  import './chunk-XVNDDHAF.js';
19
18
  import { writeEnvLocalBridge, removeEnvLocalBridge } from './chunk-KUH3G522.js';
20
- import { extractSchemaTablesAndEnums, fetchDbTablesAndEnums, extractTablesFromIdempotentSql, diffSchema, getSqlParserUtils, buildTablePatternMatcher } from './chunk-URWDB7YL.js';
19
+ import { extractSchemaTablesAndEnums, fetchDbTablesAndEnums, extractTablesFromIdempotentSql, extractDynamicTablePatternsFromIdempotentSql, diffSchema, getSqlParserUtils, buildTablePatternMatcher } from './chunk-O3M7A73M.js';
21
20
  import { psqlExec, psqlQuery, blankDollarQuotedBodies, stripSqlComments, parsePostgresUrl, buildPsqlArgs, buildPsqlEnv } from './chunk-A6A7JIRD.js';
22
21
  import { redactSecrets } from './chunk-II7VYQEM.js';
23
22
  import { init_local_supabase, init_constants, detectLocalSupabasePorts, buildLocalDatabaseUrl, DATABASE_DEFAULTS, SEED_DEFAULTS, SCRIPT_LOCATIONS } from './chunk-QSEF4T3Y.js';
@@ -441,9 +440,10 @@ async function runCleanupAction(env, options) {
441
440
  } catch {
442
441
  }
443
442
  const idempotentTables = extractTablesFromIdempotentSql(idempotentSqlDir);
444
- if (idempotentTables.length > 0) {
443
+ const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(idempotentSqlDir);
444
+ if (idempotentTables.length > 0 || dynamicPatterns.length > 0) {
445
445
  excludeFromOrphanDetection = [
446
- .../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables])
446
+ .../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables, ...dynamicPatterns])
447
447
  ];
448
448
  }
449
449
  const diff = diffSchema({
@@ -1511,14 +1511,30 @@ function buildGuardrailConflictEntries(report, entries) {
1511
1511
  }
1512
1512
  function buildGuardrailHeaderEntries(report, entries) {
1513
1513
  if (report.staleBlocks.length > 0) {
1514
+ const byFile = /* @__PURE__ */ new Map();
1515
+ for (const block of report.staleBlocks) {
1516
+ const kinds = byFile.get(block.file) ?? [];
1517
+ kinds.push(block.kind === "file-header" ? "file metadata" : `table: ${block.target}`);
1518
+ byFile.set(block.file, kinds);
1519
+ }
1514
1520
  entries.push({
1515
1521
  level: "warn",
1516
- message: `Generated headers are stale in ${report.staleBlocks.map((value) => `${value.file}:${value.kind}`).join(", ")}`
1522
+ message: `Generated headers are stale (${report.staleBlocks.length} block(s) in ${byFile.size} file(s)):`
1517
1523
  });
1524
+ for (const [file, kinds] of byFile) {
1525
+ entries.push({
1526
+ level: "info",
1527
+ message: ` ${file}: ${kinds.join(", ")}`
1528
+ });
1529
+ }
1518
1530
  if (!report.failure) {
1519
1531
  entries.push({
1520
1532
  level: "info",
1521
- message: "Run `runa db sync` to refresh generated headers before apply."
1533
+ message: "Auto-fix: Run `runa db sync` to regenerate all headers automatically."
1534
+ });
1535
+ entries.push({
1536
+ level: "info",
1537
+ message: "Headers track: FK references, RLS policies, triggers, function ownership, and schema dependencies."
1522
1538
  });
1523
1539
  }
1524
1540
  }
@@ -1885,10 +1901,54 @@ function reportMediumRisks(result, logger4, mediumRisks) {
1885
1901
  logger4.info(` \u2022 ${summary}`);
1886
1902
  }
1887
1903
  }
1888
- function reportRiskGuidance(logger4, highRiskCount, lowRiskCount) {
1889
- if (lowRiskCount > 0) {
1890
- logger4.info(` Found ${lowRiskCount} LOW risk suggestion(s) (informational)`);
1904
+ var SHOW_LOW_RISKS = process.env.RUNA_DB_SHOW_LOW_RISKS === "1";
1905
+ function reportLowRisks(logger4, lowRisks) {
1906
+ if (lowRisks.length === 0) return;
1907
+ if (SHOW_LOW_RISKS) {
1908
+ logger4.info(` Found ${lowRisks.length} LOW risk suggestion(s):`);
1909
+ const summaries = summarizeRisks(lowRisks);
1910
+ for (const summary of summaries) {
1911
+ logger4.info(` ${summary.replace("[MEDIUM]", "[LOW]")}`);
1912
+ }
1913
+ } else {
1914
+ logger4.info(
1915
+ ` Found ${lowRisks.length} LOW risk suggestion(s) (informational; set RUNA_DB_SHOW_LOW_RISKS=1 to see details)`
1916
+ );
1891
1917
  }
1918
+ }
1919
+ function writeRiskJson(allRisks, categorized) {
1920
+ if (!SHOW_LOW_RISKS) return;
1921
+ const outputDir = path12.join(process.cwd(), ".runa", "tmp");
1922
+ const outputPath = path12.join(outputDir, "schema-risks.json");
1923
+ try {
1924
+ mkdirSync(outputDir, { recursive: true });
1925
+ const json = JSON.stringify(
1926
+ {
1927
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1928
+ summary: {
1929
+ high: categorized.high.length,
1930
+ medium: categorized.medium.length,
1931
+ low: categorized.low.length,
1932
+ total: allRisks.length
1933
+ },
1934
+ risks: allRisks.map((risk) => ({
1935
+ level: risk.level,
1936
+ file: risk.file,
1937
+ line: risk.line,
1938
+ description: risk.description,
1939
+ mitigation: risk.mitigation,
1940
+ reasonCode: risk.reasonCode
1941
+ }))
1942
+ },
1943
+ null,
1944
+ 2
1945
+ );
1946
+ writeFileSync(outputPath, json, "utf-8");
1947
+ } catch {
1948
+ }
1949
+ }
1950
+ function reportRiskGuidance(logger4, highRiskCount, lowRisks) {
1951
+ reportLowRisks(logger4, lowRisks);
1892
1952
  if (highRiskCount > 0) {
1893
1953
  logger4.info("");
1894
1954
  logger4.info(" AGENTS.md requires Supabase Auth Schema Independence:");
@@ -1914,7 +1974,8 @@ async function runSqlSchemaRiskCheck(result, logger4, step) {
1914
1974
  const categorized = categorizeRisks(applySyncPreflightRiskPolicy(allRisks));
1915
1975
  reportHighRisks(result, logger4, categorized.high);
1916
1976
  reportMediumRisks(result, logger4, categorized.medium);
1917
- reportRiskGuidance(logger4, categorized.high.length, categorized.low.length);
1977
+ reportRiskGuidance(logger4, categorized.high.length, categorized.low);
1978
+ writeRiskJson(allRisks, categorized);
1918
1979
  } catch (error) {
1919
1980
  const message = error instanceof Error ? error.message : "Unknown error";
1920
1981
  logger4.warn(`SQL schema risk check skipped: ${message}`);
@@ -1971,9 +2032,10 @@ async function runOrphanCheck(env, dbPackagePath, result, logger4, step) {
1971
2032
  } catch {
1972
2033
  }
1973
2034
  const idempotentTables = extractTablesFromIdempotentSql(idempotentSqlDir);
1974
- if (idempotentTables.length > 0) {
2035
+ const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(idempotentSqlDir);
2036
+ if (idempotentTables.length > 0 || dynamicPatterns.length > 0) {
1975
2037
  excludeFromOrphanDetection = [
1976
- .../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables])
2038
+ .../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables, ...dynamicPatterns])
1977
2039
  ];
1978
2040
  }
1979
2041
  const diff = diffSchema({
@@ -2724,26 +2786,45 @@ function shouldAbortSchemaPrecheckForBudget(state, filePath) {
2724
2786
 
2725
2787
  // src/commands/db/utils/sql-file-collector.ts
2726
2788
  init_esm_shims();
2789
+ var IGNORED_DIRECTORY_NAMES = /* @__PURE__ */ new Set([
2790
+ "compat",
2791
+ "archive",
2792
+ "legacy",
2793
+ "deprecated",
2794
+ "backup",
2795
+ "node_modules",
2796
+ ".git",
2797
+ "dist",
2798
+ "build"
2799
+ ]);
2800
+ function shouldSkipDirectory(name) {
2801
+ return IGNORED_DIRECTORY_NAMES.has(name.toLowerCase());
2802
+ }
2803
+ function scanDirectory(dir, queue, sqlFiles) {
2804
+ try {
2805
+ const entries = readdirSync(dir, { withFileTypes: true });
2806
+ for (const entry of entries) {
2807
+ const fullPath = path12.join(dir, entry.name);
2808
+ if (entry.isDirectory()) {
2809
+ if (!shouldSkipDirectory(entry.name)) queue.push(fullPath);
2810
+ } else if (entry.isFile() && entry.name.endsWith(".sql")) {
2811
+ sqlFiles.push(fullPath);
2812
+ }
2813
+ }
2814
+ } catch {
2815
+ }
2816
+ }
2727
2817
  function* collectSqlFilesRecursively(baseDir) {
2728
2818
  const queue = [baseDir];
2819
+ const sqlFiles = [];
2729
2820
  let index = 0;
2730
2821
  while (index < queue.length) {
2731
2822
  const currentDir = queue[index++];
2732
2823
  if (!currentDir) continue;
2733
- try {
2734
- const entries = readdirSync(currentDir, { withFileTypes: true });
2735
- for (const entry of entries) {
2736
- const fullPath = path12.join(currentDir, entry.name);
2737
- if (entry.isDirectory()) {
2738
- queue.push(fullPath);
2739
- continue;
2740
- }
2741
- if (entry.isFile() && entry.name.endsWith(".sql")) {
2742
- yield fullPath;
2743
- }
2744
- }
2745
- } catch {
2746
- }
2824
+ scanDirectory(currentDir, queue, sqlFiles);
2825
+ }
2826
+ for (const file of sqlFiles) {
2827
+ yield file;
2747
2828
  }
2748
2829
  }
2749
2830
 
@@ -3557,12 +3638,18 @@ function classifyIdempotentMisplacementRisk(file, content, boundaryPolicy) {
3557
3638
  skipUnknown: (statement, unknownObject) => isPartitionOfCreateTable(statement) && unknownObject === "TABLE"
3558
3639
  });
3559
3640
  }
3641
+ var DYNAMIC_SQL_DOWNGRADEABLE_PATTERNS = [
3642
+ "Function DDL should be in declarative",
3643
+ "Trigger DDL should be in declarative"
3644
+ ];
3645
+ var DROP_IF_EXISTS_CLEANUP = /^\s*DROP\s+(?:FUNCTION|TRIGGER|VIEW|INDEX|POLICY|TYPE|SEQUENCE)\s+IF\s+EXISTS\b/i;
3560
3646
  function classifyFileMisplacementRisks(params) {
3561
3647
  const risks = [];
3562
3648
  const relative2 = path12.relative(process.cwd(), params.file);
3563
3649
  const normalized = normalizeSqlForPlacementCheck(params.content);
3564
3650
  const statements = splitSqlStatements(normalized);
3565
3651
  const seenMessages = /* @__PURE__ */ new Set();
3652
+ const hasAllowDynamicSqlAnnotation = params.fileType === "idempotent" && ALLOW_DYNAMIC_SQL_ANNOTATION.test(params.content);
3566
3653
  for (const { statement, line } of statements) {
3567
3654
  const candidates = collectRuleBasedCandidates({
3568
3655
  statement,
@@ -3584,6 +3671,14 @@ function classifyFileMisplacementRisks(params) {
3584
3671
  }
3585
3672
  const best = selectHighestPriorityRisk(candidates);
3586
3673
  if (!best) continue;
3674
+ if (params.fileType === "idempotent") {
3675
+ if (DROP_IF_EXISTS_CLEANUP.test(statement)) {
3676
+ best.level = "low";
3677
+ }
3678
+ if (hasAllowDynamicSqlAnnotation && DYNAMIC_SQL_DOWNGRADEABLE_PATTERNS.some((p) => best.message.includes(p))) {
3679
+ best.level = "low";
3680
+ }
3681
+ }
3587
3682
  const key = buildDirectoryRiskKey(best);
3588
3683
  if (seenMessages.has(key)) continue;
3589
3684
  seenMessages.add(key);
@@ -4388,9 +4483,10 @@ var reconcile = fromPromise(
4388
4483
  } catch {
4389
4484
  }
4390
4485
  const idempotentTables = extractTablesFromIdempotentSql(idempotentSqlDir);
4391
- if (idempotentTables.length > 0) {
4486
+ const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(idempotentSqlDir);
4487
+ if (idempotentTables.length > 0 || dynamicPatterns.length > 0) {
4392
4488
  excludeFromOrphanDetection = [
4393
- .../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables])
4489
+ .../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables, ...dynamicPatterns])
4394
4490
  ];
4395
4491
  }
4396
4492
  const diff = diffSchema({
@@ -5185,6 +5281,29 @@ function createDefaultSchemaGuardrailConfig() {
5185
5281
  idempotentSqlDir: "supabase/schemas/idempotent"
5186
5282
  };
5187
5283
  }
5284
+ function loadAllowedDuplicatesFromSchemaOwnership(targetDir) {
5285
+ const candidates = [
5286
+ join(targetDir, "supabase", "schemas", "schema-ownership.json"),
5287
+ join(targetDir, "schema-ownership.json")
5288
+ ];
5289
+ for (const filePath of candidates) {
5290
+ if (!existsSync(filePath)) continue;
5291
+ try {
5292
+ const raw = JSON.parse(readFileSync(filePath, "utf-8"));
5293
+ const allowedDuplicates = raw?.rules?.allowed_duplicates;
5294
+ if (!Array.isArray(allowedDuplicates)) continue;
5295
+ return allowedDuplicates.filter(
5296
+ (entry) => typeof entry === "object" && entry !== null && "qualifiedName" in entry && typeof entry.qualifiedName === "string"
5297
+ ).map((entry) => ({
5298
+ qualifiedName: normalizeFunctionQualifiedName(entry.qualifiedName),
5299
+ signature: normalizeAllowlistSignature(entry.signature ?? ""),
5300
+ reason: entry.reason ?? "Loaded from schema-ownership.json"
5301
+ }));
5302
+ } catch {
5303
+ }
5304
+ }
5305
+ return [];
5306
+ }
5188
5307
  function loadSchemaGuardrailConfig(targetDir) {
5189
5308
  const defaults = createDefaultSchemaGuardrailConfig();
5190
5309
  try {
@@ -5192,13 +5311,17 @@ function loadSchemaGuardrailConfig(targetDir) {
5192
5311
  const databaseConfig = config.database ?? {};
5193
5312
  return {
5194
5313
  declarativeSqlDir: databaseConfig.schemaGuardrails?.declarativeSqlDir ?? defaults.declarativeSqlDir,
5195
- allowedDuplicateFunctions: databaseConfig.schemaGuardrails?.allowedDuplicateFunctions?.map((entry) => ({
5196
- ...entry,
5197
- qualifiedName: normalizeFunctionQualifiedName(entry.qualifiedName),
5198
- signature: normalizeAllowlistSignature(entry.signature),
5199
- declarativeFile: entry.declarativeFile ? normalizePathForMatch(entry.declarativeFile) : void 0,
5200
- idempotentFile: entry.idempotentFile ? normalizePathForMatch(entry.idempotentFile) : void 0
5201
- })) ?? [],
5314
+ allowedDuplicateFunctions: [
5315
+ ...databaseConfig.schemaGuardrails?.allowedDuplicateFunctions?.map((entry) => ({
5316
+ ...entry,
5317
+ qualifiedName: normalizeFunctionQualifiedName(entry.qualifiedName),
5318
+ signature: normalizeAllowlistSignature(entry.signature),
5319
+ declarativeFile: entry.declarativeFile ? normalizePathForMatch(entry.declarativeFile) : void 0,
5320
+ idempotentFile: entry.idempotentFile ? normalizePathForMatch(entry.idempotentFile) : void 0
5321
+ })) ?? [],
5322
+ // Fallback: merge schema-ownership.json allowed_duplicates if present
5323
+ ...loadAllowedDuplicatesFromSchemaOwnership(targetDir)
5324
+ ],
5202
5325
  generatedHeaderRewriteTargets: normalizeFileList(
5203
5326
  (databaseConfig.schemaGuardrails?.generatedHeaderRewriteTargets ?? []).map(
5204
5327
  (value) => normalizePathForMatch(value)
@@ -5266,8 +5389,13 @@ function loadExtraTableFilters(targetDir) {
5266
5389
  const idempotentManagedTables = new Set(
5267
5390
  extractTablesFromIdempotentSql(idempotentDirectory, targetDir)
5268
5391
  );
5392
+ const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(
5393
+ idempotentDirectory,
5394
+ targetDir
5395
+ );
5396
+ const allExclusions = [...config.excludeFromOrphanDetection, ...dynamicPatterns];
5269
5397
  return {
5270
- exclusionMatcher: buildTablePatternMatcher(config.excludeFromOrphanDetection),
5398
+ exclusionMatcher: buildTablePatternMatcher(allExclusions),
5271
5399
  idempotentManagedTables
5272
5400
  };
5273
5401
  }
@@ -5480,24 +5608,36 @@ function collectExecuteOccurrencesFromBody(body, startLine) {
5480
5608
  }
5481
5609
  return occurrences.sort((left, right) => left.line - right.line);
5482
5610
  }
5611
+ var PARTITION_INFRA_PATTERNS = [
5612
+ /\bPARTITION\b/i,
5613
+ /\bATTACH\b/i,
5614
+ /\bDETACH\b/i,
5615
+ /\bCREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\b/i,
5616
+ /\bdefault\s*partition\b/i,
5617
+ /\brange_partition\b/i
5618
+ ];
5619
+ function isPartitionInfrastructure(body) {
5620
+ return PARTITION_INFRA_PATTERNS.some((pattern) => pattern.test(body));
5621
+ }
5483
5622
  function createDynamicSqlBlockersForStatement(params) {
5484
5623
  const body = extractFirstDollarBody(params.statement, params.line);
5485
5624
  if (!body) {
5486
5625
  return [];
5487
5626
  }
5488
5627
  const executeOccurrences = collectExecuteOccurrencesFromBody(body.body, body.startLine);
5628
+ const isInfra = isPartitionInfrastructure(body.body);
5489
5629
  return executeOccurrences.map((occurrence) => ({
5490
- kind: "dynamic-sql",
5630
+ kind: isInfra ? "dynamic-sql-infra" : "dynamic-sql",
5491
5631
  sourceFile: params.sourceFile,
5492
5632
  line: occurrence.line,
5493
5633
  target: "EXECUTE",
5494
- details: occurrence.hasStaticLiteral ? "EXECUTE in SQL bodies is blocked locally. Replace it with direct guarded calls or static SQL." : "Unresolved dynamic EXECUTE is blocked locally. Replace it with explicit static SQL or explicit allowlisted statements."
5634
+ details: isInfra ? "Dynamic SQL detected in partition/DDL infrastructure helper. Add `-- runa:allow-dynamic-sql reason: partition-helper` to suppress." : occurrence.hasStaticLiteral ? "EXECUTE in SQL bodies is blocked locally. Replace it with direct guarded calls or static SQL." : "Unresolved dynamic EXECUTE is blocked locally. Replace it with explicit static SQL or explicit allowlisted statements."
5495
5635
  }));
5496
5636
  }
5497
5637
  function buildDynamicSqlBlockers(params) {
5498
5638
  const blockers = [];
5499
- const allFiles = [...params.sources.declarativeFiles, ...params.sources.idempotentFiles];
5500
- for (const file of allFiles) {
5639
+ for (const file of params.sources.declarativeFiles) {
5640
+ if (ALLOW_DYNAMIC_SQL_ANNOTATION.test(file.content)) continue;
5501
5641
  for (const parsed of splitSqlStatements(file.content)) {
5502
5642
  blockers.push(
5503
5643
  ...createDynamicSqlBlockersForStatement({
@@ -5508,6 +5648,21 @@ function buildDynamicSqlBlockers(params) {
5508
5648
  );
5509
5649
  }
5510
5650
  }
5651
+ for (const file of params.sources.idempotentFiles) {
5652
+ if (ALLOW_DYNAMIC_SQL_ANNOTATION.test(file.content)) continue;
5653
+ for (const parsed of splitSqlStatements(file.content)) {
5654
+ const stmtBlockers = createDynamicSqlBlockersForStatement({
5655
+ sourceFile: file.relativePath,
5656
+ statement: parsed.statement,
5657
+ line: parsed.line
5658
+ });
5659
+ for (const blocker of stmtBlockers) {
5660
+ blocker.kind = "dynamic-sql-infra";
5661
+ blocker.details = `${blocker.details} (idempotent file \u2014 add \`-- runa:allow-dynamic-sql reason: <role>\` if this is intentional infrastructure, e.g. partition-helper, runtime-infrastructure, compatibility-bootstrap)`;
5662
+ }
5663
+ blockers.push(...stmtBlockers);
5664
+ }
5665
+ }
5511
5666
  return stableSorted2(blockers, (value) => `${value.sourceFile}:${value.line ?? 0}:${value.kind}`);
5512
5667
  }
5513
5668
  function buildLocalBlindSpotBlockers(params) {
@@ -5522,7 +5677,7 @@ function buildLocalBlindSpotBlockers(params) {
5522
5677
  }),
5523
5678
  ...buildExtensionPlacementBlockers({
5524
5679
  sources: params.sources,
5525
- requiredFile: path12.posix.join("supabase", "schemas", "idempotent", "00_extensions.sql")
5680
+ requiredFile: detectExtensionFilePath()
5526
5681
  })
5527
5682
  ];
5528
5683
  return stableSorted2(
@@ -5917,6 +6072,19 @@ function buildManagedBoundaryMetadataByFile(files) {
5917
6072
  }
5918
6073
 
5919
6074
  // src/commands/db/sync/schema-guardrail-graph-nodes.ts
6075
+ function extractTriggerFunctionArgs(statement) {
6076
+ const execMatch = statement.match(
6077
+ /\bEXECUTE\s+(?:FUNCTION|PROCEDURE)\s+(?:(?:"[^"]+"|[A-Za-z_]\w*)\s*\.\s*)?(?:"[^"]+"|[A-Za-z_]\w*)\s*\(([^)]*)\)/i
6078
+ );
6079
+ if (!execMatch?.[1]) return [];
6080
+ const argsText = execMatch[1].trim();
6081
+ if (!argsText) return [];
6082
+ return argsText.split(",").map((arg) => arg.trim()).map((arg) => {
6083
+ const stringMatch = arg.match(/^'([^']*)'$/);
6084
+ if (stringMatch) return stringMatch[1];
6085
+ return null;
6086
+ }).filter((arg) => arg !== null);
6087
+ }
5920
6088
  function parseCreateTriggerStatement(statement) {
5921
6089
  const triggerRegex = /^\s*CREATE\s+TRIGGER\s+(?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))\s+(BEFORE|AFTER|INSTEAD\s+OF)\s+([\s\S]+?)\s+ON\s+(?:(?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))\s*\.\s*)?(?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))[\s\S]*?\bEXECUTE\s+(?:FUNCTION|PROCEDURE)\s+(?:(?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))\s*\.\s*)?(?:"([^"]+)"|([A-Za-z_][A-Za-z0-9_]*))/i;
5922
6090
  const match = statement.match(triggerRegex);
@@ -5933,13 +6101,15 @@ function parseCreateTriggerStatement(statement) {
5933
6101
  const schema = (match[5] ?? match[6] ?? "public").toLowerCase();
5934
6102
  const functionSchema = (match[9] ?? match[10] ?? "").toLowerCase();
5935
6103
  const functionName = (match[11] ?? match[12] ?? "").toLowerCase();
6104
+ const functionArgs = extractTriggerFunctionArgs(statement);
5936
6105
  return {
5937
6106
  qualifiedTable: `${schema}.${table}`,
5938
6107
  trigger: {
5939
6108
  name: triggerName,
5940
6109
  timing,
5941
6110
  event,
5942
- functionName: functionName ? functionSchema ? `${functionSchema}.${functionName}` : functionName : void 0
6111
+ functionName: functionName ? functionSchema ? `${functionSchema}.${functionName}` : functionName : void 0,
6112
+ functionArgs: functionArgs.length > 0 ? functionArgs : void 0
5943
6113
  }
5944
6114
  };
5945
6115
  }
@@ -6902,6 +7072,114 @@ function buildBoundaryGuidanceWarnings(params) {
6902
7072
  (value) => `${value.sourceFile}.${value.kind}.${value.suggestedDeclarativeFile ?? ""}.${value.suggestedIdempotentFile ?? ""}.${value.target}`
6903
7073
  );
6904
7074
  }
7075
+ function extractCaseWhenBranches(functionBody) {
7076
+ const branches = [];
7077
+ const whenPattern = /\bWHEN\s+'([^']+)'/gi;
7078
+ for (const match of functionBody.matchAll(whenPattern)) {
7079
+ if (match[1]) branches.push(match[1].toLowerCase());
7080
+ }
7081
+ return [...new Set(branches)];
7082
+ }
7083
+ function escapeForRegex(value) {
7084
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7085
+ }
7086
+ function findFunctionBody(qualifiedName, sources) {
7087
+ const rawName = qualifiedName.includes(".") ? qualifiedName.split(".")[1] : qualifiedName;
7088
+ if (!rawName) return null;
7089
+ const escaped = escapeForRegex(rawName);
7090
+ const pattern = new RegExp(
7091
+ `CREATE\\s+(?:OR\\s+REPLACE\\s+)?FUNCTION\\s+(?:(?:"[^"]+"|\\w+)\\.)?(?:"?${escaped}"?)\\s*\\(`,
7092
+ "i"
7093
+ );
7094
+ for (const file of [...sources.declarativeFiles, ...sources.idempotentFiles]) {
7095
+ if (!pattern.test(file.content)) continue;
7096
+ const bodyMatch = file.content.match(
7097
+ new RegExp(
7098
+ `CREATE\\s+(?:OR\\s+REPLACE\\s+)?FUNCTION\\s+(?:(?:"[^"]+"|\\w+)\\.)?(?:"?${escaped}"?)\\s*\\([^)]*\\)[\\s\\S]*?\\$\\w*\\$([\\s\\S]*?)\\$\\w*\\$`,
7099
+ "i"
7100
+ )
7101
+ );
7102
+ if (bodyMatch?.[1]) return bodyMatch[1];
7103
+ }
7104
+ return null;
7105
+ }
7106
+ function buildTriggerDispatchGapWarnings(params) {
7107
+ const argsByFunction = collectTriggerArgsByFunction(params.tableNodes);
7108
+ return validateDispatchCoverage(argsByFunction, params.sources);
7109
+ }
7110
+ function collectTriggerArgsByFunction(tableNodes) {
7111
+ const argsByFunction = /* @__PURE__ */ new Map();
7112
+ for (const [, tableNode] of tableNodes) {
7113
+ for (const trigger of tableNode.triggers) {
7114
+ if (!trigger.functionName || !trigger.functionArgs?.length) continue;
7115
+ for (const arg of trigger.functionArgs) {
7116
+ const entries = argsByFunction.get(trigger.functionName) ?? [];
7117
+ entries.push({
7118
+ arg: arg.toLowerCase(),
7119
+ triggerName: trigger.name,
7120
+ table: tableNode.qualifiedName
7121
+ });
7122
+ argsByFunction.set(trigger.functionName, entries);
7123
+ }
7124
+ }
7125
+ }
7126
+ return argsByFunction;
7127
+ }
7128
+ function validateDispatchCoverage(argsByFunction, sources) {
7129
+ const warnings = [];
7130
+ for (const [functionName, triggerArgs] of argsByFunction) {
7131
+ const body = findFunctionBody(functionName, sources);
7132
+ if (!body || !/\bCASE\b/i.test(body)) continue;
7133
+ const coveredBranches = extractCaseWhenBranches(body);
7134
+ if (coveredBranches.length === 0) continue;
7135
+ for (const { arg, triggerName, table } of triggerArgs) {
7136
+ if (!coveredBranches.includes(arg)) {
7137
+ warnings.push({
7138
+ sourceFile: table,
7139
+ kind: "trigger_dispatch_gap",
7140
+ target: `${functionName}('${arg}')`,
7141
+ reason: `Trigger "${triggerName}" on ${table} passes '${arg}' to ${functionName}(), but the function's CASE statement does not handle this value. Add a WHEN '${arg}' branch to prevent runtime errors.`
7142
+ });
7143
+ }
7144
+ }
7145
+ }
7146
+ return stableSorted4(warnings, (w) => `${w.sourceFile}:${w.target}`);
7147
+ }
7148
+ var CREATE_INDEX_PATTERN = /^\s*CREATE\s+(?:UNIQUE\s+)?(?:INDEX\s+)?(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?(?:"([^"]+)"|([A-Za-z_]\w*))\s+ON\b/gim;
7149
+ function extractIndexNames(content) {
7150
+ const names = /* @__PURE__ */ new Set();
7151
+ CREATE_INDEX_PATTERN.lastIndex = 0;
7152
+ for (const match of content.matchAll(CREATE_INDEX_PATTERN)) {
7153
+ const name = (match[1] ?? match[2] ?? "").toLowerCase();
7154
+ if (name) names.add(name);
7155
+ }
7156
+ return names;
7157
+ }
7158
+ function buildCrossLayerDuplicateIndexWarnings(sources) {
7159
+ const declarativeIndexes = /* @__PURE__ */ new Map();
7160
+ for (const file of sources.declarativeFiles) {
7161
+ for (const name of extractIndexNames(file.content)) {
7162
+ declarativeIndexes.set(name, file.relativePath);
7163
+ }
7164
+ }
7165
+ const warnings = [];
7166
+ for (const file of sources.idempotentFiles) {
7167
+ for (const name of extractIndexNames(file.content)) {
7168
+ const declarativeFile = declarativeIndexes.get(name);
7169
+ if (declarativeFile) {
7170
+ warnings.push({
7171
+ sourceFile: file.relativePath,
7172
+ kind: "trigger_function",
7173
+ // reuse existing kind for index cross-layer
7174
+ target: name,
7175
+ suggestedDeclarativeFile: declarativeFile,
7176
+ reason: `Index "${name}" is defined in both declarative (${declarativeFile}) and idempotent (${file.relativePath}). The idempotent copy is redundant \u2014 remove it or use a different index name.`
7177
+ });
7178
+ }
7179
+ }
7180
+ }
7181
+ return stableSorted4(warnings, (w) => `${w.sourceFile}:${w.target}`);
7182
+ }
6905
7183
 
6906
7184
  // src/commands/db/sync/schema-guardrail-graph.ts
6907
7185
  function loadSqlSources(targetDir, config) {
@@ -7031,12 +7309,19 @@ async function buildStaticGraph(targetDir, config, sources) {
7031
7309
  runtimeTables: loadRuntimeTablesManifest(targetDir),
7032
7310
  config
7033
7311
  });
7034
- const boundaryGuidanceWarnings = buildBoundaryGuidanceWarnings({
7035
- fileNodes,
7036
- schemaNodes,
7037
- functionClaims,
7038
- ownerFileByTable
7039
- });
7312
+ const boundaryGuidanceWarnings = [
7313
+ ...buildBoundaryGuidanceWarnings({
7314
+ fileNodes,
7315
+ schemaNodes,
7316
+ functionClaims,
7317
+ ownerFileByTable
7318
+ }),
7319
+ ...buildTriggerDispatchGapWarnings({
7320
+ tableNodes: tableNodesByName,
7321
+ sources
7322
+ }),
7323
+ ...buildCrossLayerDuplicateIndexWarnings(sources)
7324
+ ];
7040
7325
  const localBlindSpotBlockers = buildLocalBlindSpotBlockers({
7041
7326
  graph,
7042
7327
  sources,
@@ -7149,7 +7434,8 @@ function createCheckModePhases(report) {
7149
7434
  phase: "compare_generated_headers",
7150
7435
  details: {
7151
7436
  file: block.file,
7152
- target: block.target
7437
+ target: block.target,
7438
+ repair: "Run `runa db sync` to auto-regenerate headers"
7153
7439
  }
7154
7440
  }))
7155
7441
  })
@@ -7231,7 +7517,7 @@ function setFailure2(report, phase, code, message) {
7231
7517
  function stableSorted5(values, map) {
7232
7518
  return [...values].sort((a, b) => map(a).localeCompare(map(b)));
7233
7519
  }
7234
- function normalizeFileList4(files) {
7520
+ function normalizeFileList3(files) {
7235
7521
  return [...new Set(files)].sort((a, b) => a.localeCompare(b));
7236
7522
  }
7237
7523
  function renderList(values) {
@@ -7716,7 +8002,7 @@ function rewriteManagedHeaders(params) {
7716
8002
  )
7717
8003
  };
7718
8004
  }
7719
- params.report.headersRewritten = normalizeFileList4(params.report.headersRewritten);
8005
+ params.report.headersRewritten = normalizeFileList3(params.report.headersRewritten);
7720
8006
  params.report.rewritesRetainedOnDisk = params.report.headersRewritten.length > 0;
7721
8007
  params.report.staleBlocks = [];
7722
8008
  return null;
@@ -8073,7 +8359,7 @@ async function runProductionDdlOrderCheck(params) {
8073
8359
  }
8074
8360
  params.logger.info("\u{1F50D} Checking production DDL ordering...");
8075
8361
  try {
8076
- const { executePgSchemaDiffPlan } = await import('./pg-schema-diff-helpers-7377FS2D.js');
8362
+ const { executePgSchemaDiffPlan } = await import('./pg-schema-diff-helpers-JZO4GAQG.js');
8077
8363
  const { planOutput } = executePgSchemaDiffPlan(
8078
8364
  productionUrl,
8079
8365
  schemasDir,
@@ -11416,6 +11702,62 @@ function formatImportImpactReport(report, changedSymbols) {
11416
11702
  // src/commands/db/commands/db-sync/production-precheck.ts
11417
11703
  init_esm_shims();
11418
11704
 
11705
+ // src/commands/db/utils/changed-files-detector.ts
11706
+ init_esm_shims();
11707
+ function detectDefaultBranch() {
11708
+ const result = spawnSync("git", ["rev-parse", "--verify", "main"], {
11709
+ timeout: 5e3,
11710
+ encoding: "utf-8"
11711
+ });
11712
+ return result.status === 0 ? "main" : "master";
11713
+ }
11714
+ function getChangedSqlFiles() {
11715
+ try {
11716
+ const defaultBranch = detectDefaultBranch();
11717
+ const mergeBase = spawnSync("git", ["merge-base", "HEAD", defaultBranch], {
11718
+ timeout: 5e3,
11719
+ encoding: "utf-8"
11720
+ });
11721
+ let diffOutput;
11722
+ if (mergeBase.status === 0 && mergeBase.stdout.trim()) {
11723
+ const diff = spawnSync("git", ["diff", "--name-only", mergeBase.stdout.trim(), "HEAD"], {
11724
+ timeout: 5e3,
11725
+ encoding: "utf-8"
11726
+ });
11727
+ diffOutput = diff.stdout ?? "";
11728
+ } else {
11729
+ const diff = spawnSync("git", ["diff", "--name-only", "HEAD"], {
11730
+ timeout: 5e3,
11731
+ encoding: "utf-8"
11732
+ });
11733
+ diffOutput = diff.stdout ?? "";
11734
+ }
11735
+ const uncommitted = spawnSync("git", ["diff", "--name-only"], {
11736
+ timeout: 5e3,
11737
+ encoding: "utf-8"
11738
+ });
11739
+ const staged = spawnSync("git", ["diff", "--name-only", "--cached"], {
11740
+ timeout: 5e3,
11741
+ encoding: "utf-8"
11742
+ });
11743
+ const allChanged = [
11744
+ ...diffOutput.split("\n"),
11745
+ ...(uncommitted.stdout ?? "").split("\n"),
11746
+ ...(staged.stdout ?? "").split("\n")
11747
+ ].map((f) => f.trim()).filter((f) => f.length > 0 && f.endsWith(".sql"));
11748
+ return [...new Set(allChanged)].sort();
11749
+ } catch {
11750
+ return [];
11751
+ }
11752
+ }
11753
+ function classifyBlockerScope(blockerMessage, changedFiles) {
11754
+ const fileMatch = blockerMessage.match(
11755
+ /(supabase\/schemas\/(?:declarative|idempotent)\/[^\s:]+\.sql)/
11756
+ );
11757
+ if (!fileMatch) return "baseline";
11758
+ return changedFiles.has(fileMatch[1]) ? "current-change" : "baseline";
11759
+ }
11760
+
11419
11761
  // src/commands/db/commands/db-sync/plan-hazard-analyzer.ts
11420
11762
  init_esm_shims();
11421
11763
  function parseHazardType(hazard) {
@@ -11722,13 +12064,26 @@ async function collectLocalPrecheckBundle(strict) {
11722
12064
  const adjustedPlacementRisks = applyStrictModeToReport(placementRisks, strict);
11723
12065
  const adjustedExtensionRisks = applyStrictModeToReport(extensionRisks, strict);
11724
12066
  const duplicateOwnershipAnalysis = analyzeDuplicateFunctionOwnership(process.cwd());
11725
- const duplicateOwnershipBlockers = duplicateOwnershipAnalysis.findings.map((finding) => {
12067
+ let guardrailAllowlist = [];
12068
+ try {
12069
+ guardrailAllowlist = loadSchemaGuardrailConfig(process.cwd()).allowedDuplicateFunctions;
12070
+ } catch {
12071
+ }
12072
+ const duplicateOwnershipBlockers = duplicateOwnershipAnalysis.findings.filter(
12073
+ (finding) => !isAllowlistedDuplicateFunction({
12074
+ finding,
12075
+ allowlist: guardrailAllowlist,
12076
+ bodyHashes: /* @__PURE__ */ new Map()
12077
+ // Hash check skipped; name + signature matching still works
12078
+ })
12079
+ ).map((finding) => {
11726
12080
  const formatted = formatDuplicateFunctionOwnershipFinding(finding);
11727
12081
  return [
11728
12082
  formatted.summary,
11729
12083
  `declarative=${formatted.declarativeLocations.join(", ")}`,
11730
12084
  `idempotent=${formatted.idempotentLocations.join(", ")}`,
11731
- formatted.suggestion
12085
+ formatted.suggestion,
12086
+ "Allowlist: runa.config.ts database.schemaGuardrails.allowedDuplicateFunctions"
11732
12087
  ].join(" | ");
11733
12088
  });
11734
12089
  const hasLocalBlockers = duplicateOwnershipBlockers.length > 0 || hasReportBlockers(adjustedPlacementRisks) || hasReportBlockers(adjustedDeclarativeRisks) || hasReportBlockers(adjustedIdempotentRisks) || hasReportBlockers(adjustedExtensionRisks);
@@ -11795,32 +12150,185 @@ function printLocalFindingCompactSummary(logger4, local, topLimit) {
11795
12150
  topLimit
11796
12151
  );
11797
12152
  }
12153
+ var APPLY_BLOCKER_PATTERNS = [
12154
+ /Extension DDL/i,
12155
+ /DROP.*CONCURRENTLY/i,
12156
+ /DML.*declarative/i,
12157
+ /DO.*maintenance/i,
12158
+ /cross.*schema.*rls/i,
12159
+ /auth\.\*.*direct.*reference/i,
12160
+ /Duplicate function ownership/i
12161
+ ];
12162
+ var INFRA_DYNAMIC_SQL_INDICATOR = /idempotent file/i;
12163
+ function isApplyBlocker(message) {
12164
+ if (/EXECUTE|dynamic.*sql/i.test(message)) {
12165
+ return !INFRA_DYNAMIC_SQL_INDICATOR.test(message);
12166
+ }
12167
+ return APPLY_BLOCKER_PATTERNS.some((pattern) => pattern.test(message));
12168
+ }
12169
+ function categorizeBlockers(blockers) {
12170
+ const applyBlockers = [];
12171
+ const architectureDebt = [];
12172
+ for (const blocker of blockers) {
12173
+ if (isApplyBlocker(blocker)) {
12174
+ applyBlockers.push(blocker);
12175
+ } else {
12176
+ architectureDebt.push(blocker);
12177
+ }
12178
+ }
12179
+ return { applyBlockers, architectureDebt };
12180
+ }
11798
12181
  function printLocalFindingDetailedReport(logger4, local) {
11799
- logFindingSection(
12182
+ logLocalWarningsSections(logger4, local);
12183
+ const allBlockers = collectAllBlockers(local);
12184
+ const { applyBlockers, architectureDebt } = categorizeBlockers(allBlockers);
12185
+ const changedFiles = new Set(getChangedSqlFiles());
12186
+ if (changedFiles.size > 0) {
12187
+ logScopedBlockers(logger4, applyBlockers, architectureDebt, changedFiles);
12188
+ } else {
12189
+ logUnscopedBlockers(logger4, applyBlockers, architectureDebt);
12190
+ }
12191
+ }
12192
+ function logLocalWarningsSections(logger4, local) {
12193
+ logFindingSection(logger4, "Declarative risk warnings:", local.adjustedDeclarativeRisks.warnings);
12194
+ logFindingSection(logger4, "Idempotent risk warnings:", local.adjustedIdempotentRisks.warnings);
12195
+ logFindingSection(logger4, "Placement warnings:", local.adjustedPlacementRisks.warnings);
12196
+ logFindingSection(logger4, "Extension warnings:", local.adjustedExtensionRisks.warnings);
12197
+ }
12198
+ function collectAllBlockers(local) {
12199
+ return [
12200
+ ...local.duplicateOwnershipBlockers,
12201
+ ...local.adjustedPlacementRisks.blockers,
12202
+ ...local.adjustedDeclarativeRisks.blockers,
12203
+ ...local.adjustedIdempotentRisks.blockers,
12204
+ ...local.adjustedExtensionRisks.blockers
12205
+ ];
12206
+ }
12207
+ function logScopedBlockers(logger4, applyBlockers, architectureDebt, changedFiles) {
12208
+ const current = applyBlockers.filter(
12209
+ (b) => classifyBlockerScope(b, changedFiles) === "current-change"
12210
+ );
12211
+ const baseline = applyBlockers.filter(
12212
+ (b) => classifyBlockerScope(b, changedFiles) === "baseline"
12213
+ );
12214
+ const currentDebt = architectureDebt.filter(
12215
+ (b) => classifyBlockerScope(b, changedFiles) === "current-change"
12216
+ );
12217
+ const baselineDebt = architectureDebt.filter(
12218
+ (b) => classifyBlockerScope(b, changedFiles) === "baseline"
12219
+ );
12220
+ logBlockerGroup(
11800
12221
  logger4,
11801
- "Duplicate function ownership blockers:",
11802
- local.duplicateOwnershipBlockers
12222
+ "error",
12223
+ "[current change] Apply blockers",
12224
+ current,
12225
+ "fix before merging"
11803
12226
  );
11804
- logFindingSection(
12227
+ logBlockerGroup(logger4, "warn", "[current change] Architecture debt", currentDebt);
12228
+ logBlockerGroup(
11805
12229
  logger4,
11806
- "Risk checks on supabase/schemas/declarative/*.sql:",
11807
- local.adjustedDeclarativeRisks.warnings
12230
+ "error",
12231
+ "[repo baseline] Apply blockers",
12232
+ baseline,
12233
+ "pre-existing issues"
11808
12234
  );
11809
- logFindingSection(
12235
+ logBlockerGroup(
11810
12236
  logger4,
11811
- "Risk checks on supabase/schemas/idempotent/*.sql:",
11812
- local.adjustedIdempotentRisks.warnings
12237
+ "warn",
12238
+ "[repo baseline] Architecture debt",
12239
+ baselineDebt,
12240
+ "pre-existing"
11813
12241
  );
11814
- logFindingSection(
12242
+ logImpactSummary(logger4, {
12243
+ current: current.length + currentDebt.length,
12244
+ currentApply: current.length,
12245
+ currentDebt: currentDebt.length,
12246
+ baseline: baseline.length + baselineDebt.length,
12247
+ baselineApply: baseline.length,
12248
+ baselineDebt: baselineDebt.length,
12249
+ total: applyBlockers.length + architectureDebt.length
12250
+ });
12251
+ }
12252
+ var PRECHECK_COUNTS_PATH = join(".runa", "tmp", "precheck-counts.json");
12253
+ function loadPreviousPrecheckCounts() {
12254
+ try {
12255
+ const fullPath = join(process.cwd(), PRECHECK_COUNTS_PATH);
12256
+ if (!existsSync(fullPath)) return null;
12257
+ return JSON.parse(readFileSync(fullPath, "utf-8"));
12258
+ } catch {
12259
+ return null;
12260
+ }
12261
+ }
12262
+ function savePrecheckCounts(counts) {
12263
+ try {
12264
+ const fullPath = join(process.cwd(), PRECHECK_COUNTS_PATH);
12265
+ const dir = join(fullPath, "..");
12266
+ mkdirSync(dir, { recursive: true });
12267
+ writeFileSync(fullPath, JSON.stringify(counts, null, 2), "utf-8");
12268
+ } catch {
12269
+ }
12270
+ }
12271
+ function logImpactSummary(logger4, counts) {
12272
+ logger4.info("");
12273
+ logger4.info("Impact summary:");
12274
+ if (counts.current === 0 && counts.total > 0) {
12275
+ logger4.info(" Current change: no new issues introduced");
12276
+ } else if (counts.current > 0) {
12277
+ logger4.info(
12278
+ ` Regressions: ${counts.current} issue(s) from changed files (${counts.currentApply} apply, ${counts.currentDebt} debt)`
12279
+ );
12280
+ }
12281
+ if (counts.baseline > 0) {
12282
+ logger4.info(
12283
+ ` Baseline debt: ${counts.baseline} pre-existing issue(s) (${counts.baselineApply} apply, ${counts.baselineDebt} debt)`
12284
+ );
12285
+ }
12286
+ if (counts.total === 0) {
12287
+ logger4.info(" All clear \u2014 no blockers or debt detected");
12288
+ }
12289
+ const previous = loadPreviousPrecheckCounts();
12290
+ if (previous) {
12291
+ const delta = counts.total - previous.total;
12292
+ if (delta < 0) {
12293
+ logger4.info(` Improvement: ${Math.abs(delta)} fewer issue(s) than previous run`);
12294
+ } else if (delta > 0) {
12295
+ logger4.info(` Regression: ${delta} more issue(s) than previous run`);
12296
+ } else {
12297
+ logger4.info(" No change from previous run");
12298
+ }
12299
+ }
12300
+ savePrecheckCounts({
12301
+ total: counts.total,
12302
+ apply: counts.currentApply + counts.baselineApply,
12303
+ debt: counts.currentDebt + counts.baselineDebt,
12304
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
12305
+ });
12306
+ }
12307
+ function logUnscopedBlockers(logger4, applyBlockers, architectureDebt) {
12308
+ logBlockerGroup(
12309
+ logger4,
12310
+ "error",
12311
+ "Apply blockers",
12312
+ applyBlockers,
12313
+ "deployment will fail if not fixed"
12314
+ );
12315
+ logBlockerGroup(
11815
12316
  logger4,
11816
- "Schema-directory placement checks:",
11817
- local.adjustedPlacementRisks.warnings
12317
+ "warn",
12318
+ "Architecture debt",
12319
+ architectureDebt,
12320
+ "works but should be addressed"
11818
12321
  );
11819
- logFindingSection(logger4, "Extension checks:", local.adjustedExtensionRisks.warnings);
11820
- logFindingSection(logger4, "Schema-directory blockers:", local.adjustedPlacementRisks.blockers);
11821
- logFindingSection(logger4, "Declarative risk blockers:", local.adjustedDeclarativeRisks.blockers);
11822
- logFindingSection(logger4, "Idempotent risk blockers:", local.adjustedIdempotentRisks.blockers);
11823
- logFindingSection(logger4, "Extension blockers:", local.adjustedExtensionRisks.blockers);
12322
+ }
12323
+ function logBlockerGroup(logger4, level, title, items, subtitle) {
12324
+ if (items.length === 0) return;
12325
+ const suffix = subtitle ? ` \u2014 ${subtitle}` : "";
12326
+ const logFn = level === "error" ? logger4.error.bind(logger4) : logger4.warn.bind(logger4);
12327
+ logFn(`
12328
+ ${title} (${items.length})${suffix}:`);
12329
+ for (const item of items) {
12330
+ logger4.info(` ${item}`);
12331
+ }
11824
12332
  }
11825
12333
  function logLocalFindings(logger4, local, summary) {
11826
12334
  if (!local.hasLocalFindings) return;
@@ -12163,9 +12671,12 @@ async function runSyncAction(env, options) {
12163
12671
  const { dbTables, dbEnums } = await fetchDbTablesAndEnums(databaseUrl);
12164
12672
  const schemaDiffConfig = loadSchemaDiffConfig();
12165
12673
  const idempotentTables = extractTablesFromIdempotentSql(schemaDiffConfig.idempotentSqlDir);
12674
+ const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(
12675
+ schemaDiffConfig.idempotentSqlDir
12676
+ );
12166
12677
  const excludeFromOrphanDetection = mergeExcludedTables(
12167
12678
  schemaDiffConfig.excludeFromOrphanDetection,
12168
- idempotentTables
12679
+ [...idempotentTables, ...dynamicPatterns]
12169
12680
  );
12170
12681
  const diff = diffSchema({
12171
12682
  expectedTables,