@runa-ai/runa-cli 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/{chunk-Y5ANTCKE.js → chunk-EZ46JIEO.js} +5 -2
  2. package/dist/{chunk-ZWDWFMOX.js → chunk-HWR5NUUZ.js} +24 -3
  3. package/dist/{chunk-OXQISY3J.js → chunk-IR7SA2ME.js} +1 -1
  4. package/dist/{chunk-QDOR3GTD.js → chunk-LCJNIHZY.js} +82 -14
  5. package/dist/{chunk-JQXOVCOP.js → chunk-NIS77243.js} +8 -5
  6. package/dist/{chunk-URWDB7YL.js → chunk-O3M7A73M.js} +58 -2
  7. package/dist/{chunk-PAWNJA3N.js → chunk-XFXGFUAM.js} +1 -1
  8. package/dist/{chunk-IEKYTCYA.js → chunk-YTQS2O4H.js} +59 -0
  9. package/dist/{ci-FLTJ2UXB.js → ci-6XYG7XNX.js} +5 -5
  10. package/dist/{cli-THEA6T7N.js → cli-2XL3VESS.js} +14 -14
  11. package/dist/commands/build/contract.d.ts +2 -2
  12. package/dist/commands/build/machine.d.ts +6 -6
  13. package/dist/commands/ci/commands/ci-prod-types.d.ts +1 -1
  14. package/dist/commands/ci/machine/contract.d.ts +10 -10
  15. package/dist/commands/ci/machine/machine.d.ts +3 -3
  16. package/dist/commands/ci/utils/ci-summary.d.ts +3 -3
  17. package/dist/commands/db/apply/contract.d.ts +1 -1
  18. package/dist/commands/db/apply/helpers/pg-schema-diff-helpers.d.ts +6 -0
  19. package/dist/commands/db/apply/helpers/planner-artifact.d.ts +1 -1
  20. package/dist/commands/db/commands/db-preview-profile.d.ts +1 -1
  21. package/dist/commands/db/commands/db-sync/production-precheck.d.ts +0 -8
  22. package/dist/commands/db/preflight/contract.d.ts +1 -1
  23. package/dist/commands/db/sync/contract.d.ts +5 -5
  24. package/dist/commands/db/sync/machine.d.ts +2 -2
  25. package/dist/commands/db/sync/schema-guardrail-graph-guidance.d.ts +18 -1
  26. package/dist/commands/db/sync/schema-guardrail-graph-metadata.d.ts +1 -7
  27. package/dist/commands/db/sync/schema-guardrail-graph-nodes.d.ts +1 -1
  28. package/dist/commands/db/sync/schema-guardrail-graph-sql-helpers.d.ts +1 -1
  29. package/dist/commands/db/sync/schema-guardrail-types.d.ts +4 -2
  30. package/dist/commands/db/utils/changed-files-detector.d.ts +21 -0
  31. package/dist/commands/db/utils/duplicate-function-ownership-allowlist.d.ts +13 -0
  32. package/dist/commands/db/utils/schema-sync.d.ts +12 -0
  33. package/dist/commands/db/utils/sql-boundary-parser.d.ts +13 -0
  34. package/dist/commands/db/utils/sql-file-collector.d.ts +2 -0
  35. package/dist/commands/upgrade.d.ts +36 -0
  36. package/dist/constants/versions.d.ts +9 -0
  37. package/dist/{db-IDKQ44VX.js → db-4AGPISOW.js} +1560 -1006
  38. package/dist/{dev-LGSMDFJN.js → dev-QR55VDNZ.js} +1 -1
  39. package/dist/{error-handler-YRQWRDEF.js → error-handler-XUQOP4TU.js} +1 -2
  40. package/dist/{hotfix-RJIAPLAM.js → hotfix-JYHDY2M6.js} +1 -2
  41. package/dist/index.js +4 -4
  42. package/dist/{init-2O6ODG5Z.js → init-4UAWYY75.js} +1 -1
  43. package/dist/{license-OB7GVJQ2.js → license-M6ODBV4X.js} +140 -154
  44. package/dist/pg-schema-diff-helpers-JZO4GAQG.js +7 -0
  45. package/dist/{risk-detector-S7XQF4I2.js → risk-detector-GDDLISVE.js} +1 -1
  46. package/dist/{risk-detector-core-TGFKWHRS.js → risk-detector-core-YI3M6INI.js} +1 -1
  47. package/dist/{risk-detector-plpgsql-O32TUR34.js → risk-detector-plpgsql-4GWEQXUG.js} +1 -1
  48. package/dist/{template-check-VNNQQXCX.js → template-check-D35F2GDP.js} +4 -0
  49. package/dist/{upgrade-QZKEI3NJ.js → upgrade-X7P6WRD5.js} +190 -20
  50. package/dist/utils/license/index.d.ts +15 -24
  51. package/dist/utils/license/types.d.ts +3 -4
  52. package/dist/utils/template-access.d.ts +20 -0
  53. package/dist/utils/template-fetcher.d.ts +10 -7
  54. package/dist/{vuln-check-JRPMUHLF.js → vuln-check-LMDYYJUE.js} +1 -1
  55. package/dist/{vuln-checker-Q7LSHUHJ.js → vuln-checker-NHXLNZRM.js} +1 -1
  56. package/dist/{watch-RFVCEQLH.js → watch-4RHXVCQ3.js} +1 -1
  57. package/package.json +3 -3
  58. package/dist/chunk-ZZOXM6Q4.js +0 -8
  59. package/dist/pg-schema-diff-helpers-7377FS2D.js +0 -7
@@ -1,23 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'module';
3
3
  import { detectDatabaseStack, getStackPaths } from './chunk-MILCC3B6.js';
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';
4
+ import { categorizeRisks, detectSchemaRisks } from './chunk-XFXGFUAM.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-LCJNIHZY.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
- import './chunk-Y5ANTCKE.js';
11
+ import './chunk-EZ46JIEO.js';
13
12
  import { loadEnvFiles } from './chunk-IWVXI5O4.js';
14
- import './chunk-OXQISY3J.js';
13
+ import './chunk-IR7SA2ME.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
  }
@@ -1832,6 +1848,10 @@ async function collectSchemaRisks(sqlDir, sqlFiles) {
1832
1848
  }
1833
1849
  return allRisks;
1834
1850
  }
1851
+ function filterAllowlistedSchemaRisks(risks) {
1852
+ const policy = getBoundaryPolicy();
1853
+ return risks.filter((risk) => !findDeclarativeRiskAllowlistMatch(risk, policy));
1854
+ }
1835
1855
  function summarizeRisks(risks) {
1836
1856
  const summaries = /* @__PURE__ */ new Map();
1837
1857
  for (const risk of risks) {
@@ -1885,10 +1905,54 @@ function reportMediumRisks(result, logger4, mediumRisks) {
1885
1905
  logger4.info(` \u2022 ${summary}`);
1886
1906
  }
1887
1907
  }
1888
- function reportRiskGuidance(logger4, highRiskCount, lowRiskCount) {
1889
- if (lowRiskCount > 0) {
1890
- logger4.info(` Found ${lowRiskCount} LOW risk suggestion(s) (informational)`);
1908
+ var SHOW_LOW_RISKS = process.env.RUNA_DB_SHOW_LOW_RISKS === "1";
1909
+ function reportLowRisks(logger4, lowRisks) {
1910
+ if (lowRisks.length === 0) return;
1911
+ if (SHOW_LOW_RISKS) {
1912
+ logger4.info(` Found ${lowRisks.length} LOW risk suggestion(s):`);
1913
+ const summaries = summarizeRisks(lowRisks);
1914
+ for (const summary of summaries) {
1915
+ logger4.info(` ${summary.replace("[MEDIUM]", "[LOW]")}`);
1916
+ }
1917
+ } else {
1918
+ logger4.info(
1919
+ ` Found ${lowRisks.length} LOW risk suggestion(s) (informational; set RUNA_DB_SHOW_LOW_RISKS=1 to see details)`
1920
+ );
1921
+ }
1922
+ }
1923
+ function writeRiskJson(allRisks, categorized) {
1924
+ if (!SHOW_LOW_RISKS) return;
1925
+ const outputDir = path12.join(process.cwd(), ".runa", "tmp");
1926
+ const outputPath = path12.join(outputDir, "schema-risks.json");
1927
+ try {
1928
+ mkdirSync(outputDir, { recursive: true });
1929
+ const json = JSON.stringify(
1930
+ {
1931
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1932
+ summary: {
1933
+ high: categorized.high.length,
1934
+ medium: categorized.medium.length,
1935
+ low: categorized.low.length,
1936
+ total: allRisks.length
1937
+ },
1938
+ risks: allRisks.map((risk) => ({
1939
+ level: risk.level,
1940
+ file: risk.file,
1941
+ line: risk.line,
1942
+ description: risk.description,
1943
+ mitigation: risk.mitigation,
1944
+ reasonCode: risk.reasonCode
1945
+ }))
1946
+ },
1947
+ null,
1948
+ 2
1949
+ );
1950
+ writeFileSync(outputPath, json, "utf-8");
1951
+ } catch {
1891
1952
  }
1953
+ }
1954
+ function reportRiskGuidance(logger4, highRiskCount, lowRisks) {
1955
+ reportLowRisks(logger4, lowRisks);
1892
1956
  if (highRiskCount > 0) {
1893
1957
  logger4.info("");
1894
1958
  logger4.info(" AGENTS.md requires Supabase Auth Schema Independence:");
@@ -1906,7 +1970,7 @@ async function runSqlSchemaRiskCheck(result, logger4, step) {
1906
1970
  const sqlFiles = getDeclarativeSqlFiles(sqlDir, logger4);
1907
1971
  if (!sqlFiles) return;
1908
1972
  try {
1909
- const allRisks = await collectSchemaRisks(sqlDir, sqlFiles);
1973
+ const allRisks = filterAllowlistedSchemaRisks(await collectSchemaRisks(sqlDir, sqlFiles));
1910
1974
  if (allRisks.length === 0) {
1911
1975
  logger4.success(`Scanned ${sqlFiles.length} SQL file(s) - no violations`);
1912
1976
  return;
@@ -1914,7 +1978,8 @@ async function runSqlSchemaRiskCheck(result, logger4, step) {
1914
1978
  const categorized = categorizeRisks(applySyncPreflightRiskPolicy(allRisks));
1915
1979
  reportHighRisks(result, logger4, categorized.high);
1916
1980
  reportMediumRisks(result, logger4, categorized.medium);
1917
- reportRiskGuidance(logger4, categorized.high.length, categorized.low.length);
1981
+ reportRiskGuidance(logger4, categorized.high.length, categorized.low);
1982
+ writeRiskJson(allRisks, categorized);
1918
1983
  } catch (error) {
1919
1984
  const message = error instanceof Error ? error.message : "Unknown error";
1920
1985
  logger4.warn(`SQL schema risk check skipped: ${message}`);
@@ -1971,9 +2036,10 @@ async function runOrphanCheck(env, dbPackagePath, result, logger4, step) {
1971
2036
  } catch {
1972
2037
  }
1973
2038
  const idempotentTables = extractTablesFromIdempotentSql(idempotentSqlDir);
1974
- if (idempotentTables.length > 0) {
2039
+ const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(idempotentSqlDir);
2040
+ if (idempotentTables.length > 0 || dynamicPatterns.length > 0) {
1975
2041
  excludeFromOrphanDetection = [
1976
- .../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables])
2042
+ .../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables, ...dynamicPatterns])
1977
2043
  ];
1978
2044
  }
1979
2045
  const diff = diffSchema({
@@ -2591,664 +2657,991 @@ async function runDeclarativeDependencyCheck(result, logger4, step, strictOption
2591
2657
 
2592
2658
  // src/commands/db/utils/preflight-checks/duplicate-function-ownership-checks.ts
2593
2659
  init_esm_shims();
2594
- async function runDuplicateFunctionOwnershipCheck(result, logger4, step) {
2595
- logger4.step("Checking duplicate function ownership", step.next());
2596
- const analysis = analyzeDuplicateFunctionOwnership(process.cwd());
2597
- if (analysis.findings.length === 0) {
2598
- logger4.success("No duplicate declarative/idempotent function ownership detected");
2599
- return;
2600
- }
2601
- result.passed = false;
2602
- result.errors.push(`Found ${analysis.findings.length} duplicate function ownership finding(s)`);
2603
- logger4.error(`Found ${analysis.findings.length} duplicate function ownership finding(s):`);
2604
- logger4.info(` ${analysis.contractNote}`);
2605
- for (const finding of analysis.findings) {
2606
- const formatted = formatDuplicateFunctionOwnershipFinding(finding);
2607
- logger4.info(` [ERROR] ${formatted.summary}`);
2608
- for (const location of formatted.declarativeLocations) {
2609
- logger4.info(` declarative: ${location}`);
2610
- }
2611
- for (const location of formatted.idempotentLocations) {
2612
- logger4.info(` idempotent: ${location}`);
2613
- }
2614
- logger4.info(` ${formatted.suggestion}`);
2615
- }
2616
- }
2617
-
2618
- // src/commands/db/utils/preflight-checks/schema-boundary-checks.ts
2619
- init_esm_shims();
2620
2660
 
2621
- // src/commands/db/commands/db-sync/precheck-helpers.ts
2661
+ // src/commands/db/utils/duplicate-function-ownership-allowlist.ts
2622
2662
  init_esm_shims();
2623
- var SHOW_ALLOWLIST_REPORT = process.env.RUNA_DB_PRECHECK_ALLOWLIST_REPORT === "1";
2624
- var DIRECTORY_PLACEMENT_WARNING_PREFIX = " [misplacement] ";
2625
- function applyStrictModeToReport(report, strict) {
2626
- if (!strict) return report;
2627
- return {
2628
- blockers: [...report.blockers, ...report.warnings],
2629
- warnings: []
2630
- };
2631
- }
2632
- function formatAllowlistReason({
2633
- label,
2634
- ruleId,
2635
- reason,
2636
- rule
2637
- }) {
2638
- const meta = rule ? formatAllowlistMetadata(rule) : "";
2639
- return meta ? `[allowlist:${label}] ${ruleId}: ${reason} (${meta})` : `[allowlist:${label}] ${ruleId}: ${reason}`;
2640
- }
2641
2663
 
2642
- // src/commands/db/commands/db-sync/directory-placement-check.ts
2664
+ // src/commands/db/sync/schema-guardrail-config.ts
2643
2665
  init_esm_shims();
2644
2666
 
2645
- // src/commands/db/utils/schema-precheck-budget.ts
2667
+ // src/commands/db/sync/schema-guardrail-config-test-support.ts
2646
2668
  init_esm_shims();
2647
- var ONE_MB = 1024 * 1024;
2648
- var ONE_GB = 1024 * ONE_MB;
2649
- function parseIntEnv(name, fallback, min, max) {
2650
- const raw = process.env[name];
2651
- if (!raw) return fallback;
2652
- const value = Number.parseInt(raw, 10);
2653
- if (!Number.isFinite(value) || Number.isNaN(value)) return fallback;
2654
- const normalized = Math.trunc(value);
2655
- if (normalized < min) return min;
2656
- if (normalized > max) return max;
2657
- return normalized;
2658
- }
2659
- var RUNA_SCHEMA_PRECHECK_MAX_FILES = parseIntEnv(
2660
- "RUNA_SCHEMA_PRECHECK_MAX_FILES",
2661
- 3e3,
2662
- 100,
2663
- 2e4
2664
- );
2665
- var RUNA_SCHEMA_PRECHECK_MAX_TOTAL_BYTES = parseIntEnv(
2666
- "RUNA_SCHEMA_PRECHECK_MAX_TOTAL_BYTES",
2667
- 512 * ONE_MB,
2668
- 32 * ONE_MB,
2669
- 20 * ONE_GB
2670
- );
2671
- var RUNA_SCHEMA_PRECHECK_MAX_FILE_BYTES = parseIntEnv(
2672
- "RUNA_SCHEMA_PRECHECK_MAX_FILE_BYTES",
2673
- 64 * ONE_MB,
2674
- 1 * ONE_MB,
2675
- 512 * ONE_MB
2676
- );
2677
- var RUNA_PLAN_PRECHECK_MAX_BYTES = parseIntEnv(
2678
- "RUNA_PLAN_PRECHECK_MAX_BYTES",
2679
- 128 * ONE_MB,
2680
- 8 * ONE_MB,
2681
- 4 * ONE_GB
2682
- );
2683
- var RUNA_PLAN_PRECHECK_MAX_STATEMENTS = parseIntEnv(
2684
- "RUNA_PLAN_PRECHECK_MAX_STATEMENTS",
2685
- 4e3,
2686
- 100,
2687
- 5e4
2688
- );
2689
- function formatBytes2(value) {
2690
- if (value >= ONE_MB * 1024) return `${(value / (ONE_MB * 1024)).toFixed(1)} GB`;
2691
- if (value >= ONE_MB) return `${(value / ONE_MB).toFixed(1)} MB`;
2692
- return `${(value / 1024).toFixed(1)} KB`;
2693
- }
2694
- function buildBudgetExceededReason(context, files, bytes, maxFiles, maxBytes) {
2695
- return `${context}: budget exceeded (files=${files}/${maxFiles}, bytes=${formatBytes2(bytes)}/${formatBytes2(maxBytes)}). Stop and review with stricter scoping or increase RUNA_SCHEMA_PRECHECK_MAX_* settings only after approval.`;
2696
- }
2697
- function createSchemaPrecheckBudgetState() {
2698
- return {
2699
- scannedFiles: 0,
2700
- scannedBytes: 0,
2701
- maxFiles: RUNA_SCHEMA_PRECHECK_MAX_FILES,
2702
- maxBytes: RUNA_SCHEMA_PRECHECK_MAX_TOTAL_BYTES,
2703
- maxFileBytes: RUNA_SCHEMA_PRECHECK_MAX_FILE_BYTES
2704
- };
2669
+ function extractFirstStringMatch(content, fieldName) {
2670
+ const match = content.match(new RegExp(`${fieldName}\\s*:\\s*['"]([^'"]+)['"]`));
2671
+ return match?.[1];
2705
2672
  }
2706
- function shouldAbortSchemaPrecheckForBudget(state, filePath) {
2707
- const size = statSync(filePath).size;
2708
- if (size > state.maxFileBytes) {
2709
- return `Unscanned schema file ${filePath}: ${formatBytes2(size)} exceeds per-file limit ${formatBytes2(
2710
- state.maxFileBytes
2711
- )}`;
2712
- }
2713
- if (state.scannedFiles + 1 > state.maxFiles) {
2714
- return `Schema file scan budget exceeds ${state.maxFiles} files at ${path12.basename(filePath)}.`;
2673
+ function extractFirstNumberMatch(content, fieldName) {
2674
+ const match = content.match(new RegExp(`${fieldName}\\s*:\\s*(-?\\d+(?:\\.\\d+)?)`));
2675
+ if (!match?.[1]) {
2676
+ return void 0;
2715
2677
  }
2716
- const projectedBytes = state.scannedBytes + size;
2717
- if (projectedBytes > state.maxBytes) {
2718
- return `Schema scan budget exceeds total size limit ${formatBytes2(state.maxBytes)}.`;
2678
+ const parsed = Number(match[1]);
2679
+ return Number.isFinite(parsed) ? parsed : void 0;
2680
+ }
2681
+ function extractStringArrayMatch(content, fieldName) {
2682
+ const match = content.match(new RegExp(`${fieldName}\\s*:\\s*\\[([\\s\\S]*?)\\]`));
2683
+ if (!match?.[1]) {
2684
+ return [];
2719
2685
  }
2720
- state.scannedBytes = projectedBytes;
2721
- state.scannedFiles += 1;
2722
- return null;
2686
+ return Array.from(match[1].matchAll(/['"]([^'"]+)['"]/g), (entry) => entry[1] ?? "").filter(
2687
+ (value) => value.length > 0
2688
+ );
2723
2689
  }
2724
-
2725
- // src/commands/db/utils/sql-file-collector.ts
2726
- init_esm_shims();
2727
- function* collectSqlFilesRecursively(baseDir) {
2728
- const queue = [baseDir];
2729
- let index = 0;
2730
- while (index < queue.length) {
2731
- const currentDir = queue[index++];
2732
- 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
- }
2690
+ function extractNamedObjectBlock(content, fieldName) {
2691
+ const nameMatch = new RegExp(`${fieldName}\\s*:\\s*\\{`).exec(content);
2692
+ if (!nameMatch) {
2693
+ return null;
2694
+ }
2695
+ const startIndex = nameMatch.index + nameMatch[0].length - 1;
2696
+ let depth = 0;
2697
+ for (let index = startIndex; index < content.length; index += 1) {
2698
+ const current = content[index];
2699
+ if (current === "{") {
2700
+ depth += 1;
2701
+ } else if (current === "}") {
2702
+ depth -= 1;
2703
+ if (depth === 0) {
2704
+ return content.slice(startIndex + 1, index);
2744
2705
  }
2745
- } catch {
2746
2706
  }
2747
2707
  }
2708
+ return null;
2748
2709
  }
2749
-
2750
- // src/commands/db/commands/db-sync/boundary-classifier.ts
2751
- init_esm_shims();
2752
-
2753
- // src/commands/db/commands/db-sync/sql-parser.ts
2754
- init_esm_shims();
2755
- function isWordChar(char) {
2756
- const code = char.charCodeAt(0);
2757
- return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || char === "_";
2758
- }
2759
- function isWhitespaceChar(char) {
2760
- return char === " " || char === " " || char === "\n" || char === "\r" || char === "\f";
2761
- }
2762
- function parseWordAt(statement, start) {
2763
- if (start < 0 || start >= statement.length) return null;
2764
- const first = statement[start];
2765
- if (!first || !isWordChar(first)) return null;
2766
- let end = start + 1;
2767
- while (end < statement.length && isWordChar(statement[end] ?? "")) {
2768
- end += 1;
2769
- }
2770
- return { value: statement.slice(start, end), start, end };
2771
- }
2772
- function skipWhitespace(statement, start) {
2773
- let index = start;
2774
- while (index < statement.length && isWhitespaceChar(statement[index] ?? "")) {
2775
- index += 1;
2710
+ function extractAllowedDuplicateFunctions(content, normalizers) {
2711
+ const match = content.match(/allowedDuplicateFunctions\s*:\s*\[([\s\S]*?)\]/);
2712
+ if (!match?.[1]) {
2713
+ return [];
2776
2714
  }
2777
- return index;
2778
- }
2779
- function skipLineComment(statement, start) {
2780
- let index = start;
2781
- while (index < statement.length && statement[index] !== "\n") {
2782
- index += 1;
2715
+ const entries = [];
2716
+ for (const objectMatch of match[1].matchAll(/\{([\s\S]*?)\}/g)) {
2717
+ const objectBody = objectMatch[1] ?? "";
2718
+ const qualifiedName = extractFirstStringMatch(objectBody, "qualifiedName");
2719
+ const signature = extractFirstStringMatch(objectBody, "signature");
2720
+ const reason = extractFirstStringMatch(objectBody, "reason");
2721
+ if (!qualifiedName || !signature || !reason) {
2722
+ continue;
2723
+ }
2724
+ entries.push({
2725
+ qualifiedName: normalizers.normalizeFunctionQualifiedName(qualifiedName),
2726
+ signature: normalizers.normalizeAllowlistSignature(signature),
2727
+ reason,
2728
+ declarativeFile: extractFirstStringMatch(objectBody, "declarativeFile"),
2729
+ idempotentFile: extractFirstStringMatch(objectBody, "idempotentFile"),
2730
+ expectedBodyHash: extractFirstStringMatch(objectBody, "expectedBodyHash")
2731
+ });
2783
2732
  }
2784
- return index;
2733
+ return entries;
2785
2734
  }
2786
- function skipBlockComment(statement, start) {
2787
- let index = start;
2788
- while (index + 1 < statement.length && !(statement[index] === "*" && statement[index + 1] === "/")) {
2789
- index += 1;
2735
+ function tryLoadSchemaGuardrailConfigFromText(params) {
2736
+ const configPath = findRunaConfig(params.targetDir);
2737
+ if (!configPath || !existsSync(configPath)) {
2738
+ return null;
2790
2739
  }
2791
- return index + 1 < statement.length ? index + 2 : index;
2792
- }
2793
- function skipWhitespaceAndComments(statement, start) {
2794
- let index = Math.max(0, start);
2795
- while (index < statement.length) {
2796
- const afterWhitespace = skipWhitespace(statement, index);
2797
- if (afterWhitespace !== index) {
2798
- index = afterWhitespace;
2799
- continue;
2800
- }
2801
- if (statement[index] === "-" && statement[index + 1] === "-") {
2802
- index = skipLineComment(statement, index + 2);
2803
- continue;
2804
- }
2805
- if (statement[index] === "/" && statement[index + 1] === "*") {
2806
- index = skipBlockComment(statement, index + 2);
2807
- continue;
2808
- }
2809
- break;
2810
- }
2811
- return index;
2812
- }
2813
- function getPreviousWord(statement, start) {
2814
- let index = start - 1;
2815
- while (index >= 0 && isWhitespaceChar(statement[index] ?? "")) index -= 1;
2816
- if (index < 0 || !isWordChar(statement[index] ?? "")) return "";
2817
- const end = index;
2818
- while (index >= 0 && isWordChar(statement[index] ?? "")) index -= 1;
2819
- return statement.slice(index + 1, end + 1).toUpperCase();
2820
- }
2821
- function parseDollarQuotedLiteral(statement, start) {
2822
- if (statement[start] !== "$") return null;
2823
- let tagEnd = start + 1;
2824
- if (tagEnd >= statement.length) return null;
2825
- if (statement[tagEnd] === "$") {
2826
- const delimiter2 = "$$";
2827
- const bodyStart2 = tagEnd + 1;
2828
- const bodyEnd2 = statement.indexOf(delimiter2, bodyStart2);
2829
- if (bodyEnd2 < 0) return null;
2740
+ try {
2741
+ const content = readFileSync(configPath, "utf-8");
2742
+ const schemaGuardrailsBlock = extractNamedObjectBlock(content, "schemaGuardrails") ?? "";
2743
+ const pgSchemaDiffBlock = extractNamedObjectBlock(content, "pgSchemaDiff") ?? "";
2830
2744
  return {
2831
- body: statement.slice(bodyStart2, bodyEnd2),
2832
- next: bodyEnd2 + delimiter2.length
2745
+ ...params.defaults,
2746
+ declarativeSqlDir: extractFirstStringMatch(schemaGuardrailsBlock, "declarativeSqlDir") ?? params.defaults.declarativeSqlDir,
2747
+ allowedDuplicateFunctions: extractAllowedDuplicateFunctions(
2748
+ schemaGuardrailsBlock,
2749
+ params.normalizers
2750
+ ),
2751
+ generatedHeaderRewriteTargets: params.normalizers.normalizeFileList(
2752
+ extractStringArrayMatch(schemaGuardrailsBlock, "generatedHeaderRewriteTargets").map(
2753
+ (value) => params.normalizers.normalizePathForMatch(value)
2754
+ )
2755
+ ),
2756
+ semanticWarnings: {
2757
+ threshold: extractFirstNumberMatch(schemaGuardrailsBlock, "threshold") ?? params.defaults.semanticWarnings.threshold,
2758
+ maxCandidates: extractFirstNumberMatch(schemaGuardrailsBlock, "maxCandidates") ?? params.defaults.semanticWarnings.maxCandidates,
2759
+ ignorePairs: new Set(
2760
+ extractStringArrayMatch(schemaGuardrailsBlock, "ignorePairs").map(
2761
+ (value) => params.normalizers.normalizeSuppressionPair(value)
2762
+ )
2763
+ )
2764
+ },
2765
+ tableHeaderMaxWidth: extractFirstNumberMatch(schemaGuardrailsBlock, "tableHeaderMaxWidth") ?? params.defaults.tableHeaderMaxWidth,
2766
+ excludeFromOrphanDetection: extractStringArrayMatch(
2767
+ pgSchemaDiffBlock,
2768
+ "excludeFromOrphanDetection"
2769
+ ),
2770
+ idempotentSqlDir: extractFirstStringMatch(pgSchemaDiffBlock, "idempotentSqlDir") ?? params.defaults.idempotentSqlDir
2833
2771
  };
2772
+ } catch {
2773
+ return null;
2834
2774
  }
2835
- const firstChar = statement[tagEnd];
2836
- if (!firstChar || !/[A-Za-z_]/.test(firstChar)) return null;
2837
- while (tagEnd < statement.length && /[A-Za-z0-9_]/.test(statement[tagEnd] ?? "")) {
2838
- tagEnd += 1;
2839
- }
2840
- if (statement[tagEnd] !== "$") return null;
2841
- const tag = statement.slice(start + 1, tagEnd);
2842
- const delimiter = `$${tag}$`;
2843
- const bodyStart = tagEnd + 1;
2844
- const bodyEnd = statement.indexOf(delimiter, bodyStart);
2845
- if (bodyEnd < 0) return null;
2775
+ }
2776
+
2777
+ // src/commands/db/sync/schema-guardrail-config.ts
2778
+ var DEFAULT_TABLE_HEADER_MAX_WIDTH = 160;
2779
+ var DEFAULT_SEMANTIC_WARNING_THRESHOLD = 0.55;
2780
+ var DEFAULT_SEMANTIC_WARNING_MAX_CANDIDATES = 3;
2781
+ var GENERIC_SIMILARITY_COLUMNS = /* @__PURE__ */ new Set([
2782
+ "id",
2783
+ "created_at",
2784
+ "updated_at",
2785
+ "deleted_at",
2786
+ "scope_id"
2787
+ ]);
2788
+ function normalizePathForMatch(filePath) {
2789
+ return filePath.replaceAll("\\", "/");
2790
+ }
2791
+ function normalizeFunctionQualifiedName(value) {
2792
+ return value.trim().toLowerCase();
2793
+ }
2794
+ function normalizeAllowlistSignature(value) {
2795
+ return value.replace(/\s+/g, " ").trim();
2796
+ }
2797
+ function normalizeSuppressionPair(value) {
2798
+ return value.split("::").map((part) => part.trim().toLowerCase()).filter((part) => part.length > 0).sort((left, right) => left.localeCompare(right)).join("::");
2799
+ }
2800
+ function normalizeFileList(files) {
2801
+ return [...new Set(files)].sort((a, b) => a.localeCompare(b));
2802
+ }
2803
+ function isSchemaGuardrailTextFallbackAllowed() {
2804
+ return Boolean(process.env.VITEST);
2805
+ }
2806
+ function createDefaultSchemaGuardrailConfig() {
2846
2807
  return {
2847
- body: statement.slice(bodyStart, bodyEnd),
2848
- next: bodyEnd + delimiter.length
2808
+ declarativeSqlDir: "supabase/schemas/declarative",
2809
+ allowedDuplicateFunctions: [],
2810
+ generatedHeaderRewriteTargets: [],
2811
+ semanticWarnings: {
2812
+ threshold: DEFAULT_SEMANTIC_WARNING_THRESHOLD,
2813
+ maxCandidates: DEFAULT_SEMANTIC_WARNING_MAX_CANDIDATES,
2814
+ ignorePairs: /* @__PURE__ */ new Set()
2815
+ },
2816
+ tableHeaderMaxWidth: DEFAULT_TABLE_HEADER_MAX_WIDTH,
2817
+ excludeFromOrphanDetection: [],
2818
+ idempotentSqlDir: "supabase/schemas/idempotent"
2849
2819
  };
2850
2820
  }
2851
- function parseSingleQuotedLiteral(statement, start) {
2852
- if (statement[start] !== "'") return null;
2853
- let index = start + 1;
2854
- let body = "";
2855
- while (index < statement.length) {
2856
- const char = statement[index];
2857
- const next = statement[index + 1] ?? "";
2858
- if (char === "'") {
2859
- if (next === "'") {
2860
- body += "'";
2861
- index += 2;
2862
- continue;
2863
- }
2864
- return { body, next: index + 1 };
2865
- }
2866
- if (char === "\\" && index + 1 < statement.length) {
2867
- body += next;
2868
- index += 2;
2869
- continue;
2821
+ function loadAllowedDuplicatesFromSchemaOwnership(targetDir) {
2822
+ const candidates = [
2823
+ join(targetDir, "supabase", "schemas", "schema-ownership.json"),
2824
+ join(targetDir, "schema-ownership.json")
2825
+ ];
2826
+ for (const filePath of candidates) {
2827
+ if (!existsSync(filePath)) continue;
2828
+ try {
2829
+ const raw = JSON.parse(readFileSync(filePath, "utf-8"));
2830
+ const allowedDuplicates = raw?.rules?.allowed_duplicates;
2831
+ if (!Array.isArray(allowedDuplicates)) continue;
2832
+ return allowedDuplicates.filter(
2833
+ (entry) => typeof entry === "object" && entry !== null && "qualifiedName" in entry && typeof entry.qualifiedName === "string"
2834
+ ).map((entry) => ({
2835
+ qualifiedName: normalizeFunctionQualifiedName(entry.qualifiedName),
2836
+ signature: normalizeAllowlistSignature(entry.signature ?? ""),
2837
+ reason: entry.reason ?? "Loaded from schema-ownership.json"
2838
+ }));
2839
+ } catch {
2870
2840
  }
2871
- body += char;
2872
- index += 1;
2873
2841
  }
2874
- return null;
2842
+ return [];
2875
2843
  }
2876
- function parsePotentialEmbeddedSqlLiteral(statement, start, fragments) {
2877
- const index = skipWhitespaceAndComments(statement, start);
2878
- if (index >= statement.length) return index;
2879
- const maybePrefix = (statement[index] ?? "").toUpperCase();
2880
- if (maybePrefix === "E" && statement[index + 1] === "'") {
2881
- const parsed = parseSingleQuotedLiteral(statement, index + 1);
2882
- if (parsed && parsed.body.trim().length > 0) {
2883
- fragments.push(parsed.body);
2884
- return parsed.next;
2844
+ function loadSchemaGuardrailConfig(targetDir) {
2845
+ const defaults = createDefaultSchemaGuardrailConfig();
2846
+ try {
2847
+ const config = loadRunaConfig(targetDir);
2848
+ const databaseConfig = config.database ?? {};
2849
+ return {
2850
+ declarativeSqlDir: databaseConfig.schemaGuardrails?.declarativeSqlDir ?? defaults.declarativeSqlDir,
2851
+ allowedDuplicateFunctions: [
2852
+ ...databaseConfig.schemaGuardrails?.allowedDuplicateFunctions?.map((entry) => ({
2853
+ ...entry,
2854
+ qualifiedName: normalizeFunctionQualifiedName(entry.qualifiedName),
2855
+ signature: normalizeAllowlistSignature(entry.signature),
2856
+ declarativeFile: entry.declarativeFile ? normalizePathForMatch(entry.declarativeFile) : void 0,
2857
+ idempotentFile: entry.idempotentFile ? normalizePathForMatch(entry.idempotentFile) : void 0
2858
+ })) ?? [],
2859
+ // Fallback: merge schema-ownership.json allowed_duplicates if present
2860
+ ...loadAllowedDuplicatesFromSchemaOwnership(targetDir)
2861
+ ],
2862
+ generatedHeaderRewriteTargets: normalizeFileList(
2863
+ (databaseConfig.schemaGuardrails?.generatedHeaderRewriteTargets ?? []).map(
2864
+ (value) => normalizePathForMatch(value)
2865
+ )
2866
+ ),
2867
+ semanticWarnings: {
2868
+ threshold: databaseConfig.schemaGuardrails?.semanticWarnings?.threshold ?? defaults.semanticWarnings.threshold,
2869
+ maxCandidates: databaseConfig.schemaGuardrails?.semanticWarnings?.maxCandidates ?? defaults.semanticWarnings.maxCandidates,
2870
+ ignorePairs: new Set(
2871
+ (databaseConfig.schemaGuardrails?.semanticWarnings?.ignorePairs ?? []).map(
2872
+ (value) => normalizeSuppressionPair(value)
2873
+ )
2874
+ )
2875
+ },
2876
+ tableHeaderMaxWidth: databaseConfig.schemaGuardrails?.tableHeaderMaxWidth ?? defaults.tableHeaderMaxWidth,
2877
+ excludeFromOrphanDetection: databaseConfig.pgSchemaDiff?.excludeFromOrphanDetection ?? [],
2878
+ idempotentSqlDir: databaseConfig.pgSchemaDiff?.idempotentSqlDir ?? defaults.idempotentSqlDir
2879
+ };
2880
+ } catch (error) {
2881
+ if (isSchemaGuardrailTextFallbackAllowed()) {
2882
+ return tryLoadSchemaGuardrailConfigFromText({
2883
+ targetDir,
2884
+ defaults,
2885
+ normalizers: {
2886
+ normalizeAllowlistSignature,
2887
+ normalizeFileList,
2888
+ normalizeFunctionQualifiedName,
2889
+ normalizePathForMatch,
2890
+ normalizeSuppressionPair
2891
+ }
2892
+ }) ?? defaults;
2885
2893
  }
2886
- return index + 1;
2887
- }
2888
- const parsedDollar = parseDollarQuotedLiteral(statement, index);
2889
- if (parsedDollar) {
2890
- if (parsedDollar.body.trim().length > 0) fragments.push(parsedDollar.body);
2891
- return parsedDollar.next;
2894
+ throw error;
2892
2895
  }
2893
- if (statement[index] === "'") {
2894
- const parsed = parseSingleQuotedLiteral(statement, index);
2895
- if (parsed && parsed.body.trim().length > 0) {
2896
- fragments.push(parsed.body.replace(/\\'/g, "'"));
2897
- return parsed.next;
2898
- }
2899
- return index + 1;
2896
+ }
2897
+
2898
+ // src/commands/db/utils/duplicate-function-ownership-allowlist.ts
2899
+ function matchesDuplicateFunctionSignature(params) {
2900
+ return normalizeFunctionQualifiedName(params.entry.qualifiedName) === normalizeFunctionQualifiedName(params.finding.qualifiedName) && normalizeAllowlistSignature(params.entry.signature) === normalizeAllowlistSignature(params.finding.signature ?? "");
2901
+ }
2902
+ function matchesOptionalFilePath(params) {
2903
+ if (!params.configuredPath) {
2904
+ return true;
2900
2905
  }
2901
- return index + 1;
2906
+ return params.definitionFiles.some(
2907
+ (filePath) => normalizePathForMatch(filePath) === params.configuredPath
2908
+ );
2902
2909
  }
2903
- var EMBEDDABLE_SQL_HINT_WORDS = /* @__PURE__ */ new Set(["DO", "AS", "EXECUTE", "FORMAT"]);
2904
- var EMBEDDABLE_SQL_MAX_RECURSION_DEPTH = 3;
2905
- var PLPGSQL_RECURSIVE_PATTERN = /\b(?:DO|EXECUTE|FORMAT)\b/i;
2906
- function shouldRecurseIntoEmbeddableFragment(statement) {
2907
- return PLPGSQL_RECURSIVE_PATTERN.test(statement);
2910
+ function matchesExpectedBodyHash(params) {
2911
+ if (!params.entry.expectedBodyHash || !params.bodyHashes) {
2912
+ return true;
2913
+ }
2914
+ const definitions = [
2915
+ ...params.finding.declarativeDefinitions,
2916
+ ...params.finding.idempotentDefinitions
2917
+ ];
2918
+ if (definitions.length === 0) {
2919
+ return false;
2920
+ }
2921
+ return definitions.every((definition) => {
2922
+ const key = `${definition.layer}:${definition.file}:${definition.line}`;
2923
+ return params.bodyHashes?.get(key) === params.entry.expectedBodyHash;
2924
+ });
2908
2925
  }
2909
- function createFragmentCollector(fragments, depth, maxDepth, seen) {
2910
- return (fragment) => {
2911
- const candidate = fragment.trim();
2912
- if (!candidate) return;
2913
- if (!seen.has(candidate)) {
2914
- seen.add(candidate);
2915
- fragments.push(candidate);
2926
+ function isAllowlistedDuplicateFunction(params) {
2927
+ const { finding, allowlist, bodyHashes } = params;
2928
+ if (!finding.signature) return false;
2929
+ return allowlist.some((entry) => {
2930
+ if (!matchesDuplicateFunctionSignature({ entry, finding })) {
2931
+ return false;
2916
2932
  }
2917
- if (depth < maxDepth && shouldRecurseIntoEmbeddableFragment(candidate)) {
2918
- collectEmbeddableSqlFragments(candidate, fragments, depth + 1, maxDepth, seen);
2933
+ if (!matchesOptionalFilePath({
2934
+ configuredPath: entry.declarativeFile,
2935
+ definitionFiles: finding.declarativeDefinitions.map((definition) => definition.file)
2936
+ })) {
2937
+ return false;
2919
2938
  }
2920
- };
2939
+ if (!matchesOptionalFilePath({
2940
+ configuredPath: entry.idempotentFile,
2941
+ definitionFiles: finding.idempotentDefinitions.map((definition) => definition.file)
2942
+ })) {
2943
+ return false;
2944
+ }
2945
+ return matchesExpectedBodyHash({ entry, finding, bodyHashes });
2946
+ });
2921
2947
  }
2922
- function tryAdvanceWithinQuotedText(normalized, state) {
2923
- const char = normalized[state.index];
2924
- if (!char) return false;
2925
- if (state.inSingleQuote) {
2926
- if (char === "'" && normalized[state.index + 1] === "'") {
2927
- state.index += 2;
2928
- return true;
2929
- }
2930
- if (char === "'") {
2931
- state.inSingleQuote = false;
2932
- }
2933
- state.index += 1;
2934
- return true;
2948
+ function filterAllowlistedDuplicateFunctions(params) {
2949
+ return params.findings.filter(
2950
+ (finding) => !isAllowlistedDuplicateFunction({
2951
+ finding,
2952
+ allowlist: params.allowlist,
2953
+ bodyHashes: params.bodyHashes
2954
+ })
2955
+ );
2956
+ }
2957
+
2958
+ // src/commands/db/utils/preflight-checks/duplicate-function-ownership-checks.ts
2959
+ async function runDuplicateFunctionOwnershipCheck(result, logger4, step) {
2960
+ logger4.step("Checking duplicate function ownership", step.next());
2961
+ const analysis = analyzeDuplicateFunctionOwnership(process.cwd());
2962
+ let findings = analysis.findings;
2963
+ try {
2964
+ const allowlist = loadSchemaGuardrailConfig(process.cwd()).allowedDuplicateFunctions;
2965
+ findings = filterAllowlistedDuplicateFunctions({
2966
+ findings,
2967
+ allowlist
2968
+ });
2969
+ } catch {
2935
2970
  }
2936
- if (state.inDoubleQuote) {
2937
- if (char === '"' && normalized[state.index + 1] === '"') {
2938
- state.index += 2;
2939
- return true;
2971
+ if (findings.length === 0) {
2972
+ logger4.success("No duplicate declarative/idempotent function ownership detected");
2973
+ return;
2974
+ }
2975
+ result.passed = false;
2976
+ result.errors.push(`Found ${findings.length} duplicate function ownership finding(s)`);
2977
+ logger4.error(`Found ${findings.length} duplicate function ownership finding(s):`);
2978
+ logger4.info(` ${analysis.contractNote}`);
2979
+ for (const finding of findings) {
2980
+ const formatted = formatDuplicateFunctionOwnershipFinding(finding);
2981
+ logger4.info(` [ERROR] ${formatted.summary}`);
2982
+ for (const location of formatted.declarativeLocations) {
2983
+ logger4.info(` declarative: ${location}`);
2940
2984
  }
2941
- if (char === '"') {
2942
- state.inDoubleQuote = false;
2985
+ for (const location of formatted.idempotentLocations) {
2986
+ logger4.info(` idempotent: ${location}`);
2943
2987
  }
2944
- state.index += 1;
2945
- return true;
2988
+ logger4.info(` ${formatted.suggestion}`);
2946
2989
  }
2947
- return false;
2948
2990
  }
2949
- function tryAdvanceComment(normalized, state) {
2950
- if (normalized[state.index] === "-" && normalized[state.index + 1] === "-") {
2951
- state.index = skipLineComment(normalized, state.index + 2);
2952
- return true;
2953
- }
2954
- if (normalized[state.index] === "/" && normalized[state.index + 1] === "*") {
2955
- state.index = skipBlockComment(normalized, state.index + 2);
2956
- return true;
2957
- }
2958
- return false;
2991
+
2992
+ // src/commands/db/utils/preflight-checks/schema-boundary-checks.ts
2993
+ init_esm_shims();
2994
+
2995
+ // src/commands/db/commands/db-sync/precheck-helpers.ts
2996
+ init_esm_shims();
2997
+ var SHOW_ALLOWLIST_REPORT = process.env.RUNA_DB_PRECHECK_ALLOWLIST_REPORT === "1";
2998
+ var DIRECTORY_PLACEMENT_WARNING_PREFIX = " [misplacement] ";
2999
+ function applyStrictModeToReport(report, strict) {
3000
+ if (!strict) return report;
3001
+ return {
3002
+ blockers: [...report.blockers, ...report.warnings],
3003
+ warnings: []
3004
+ };
2959
3005
  }
2960
- function tryEnterQuotedText(normalized, state) {
2961
- if (normalized[state.index] === "'") {
2962
- state.inSingleQuote = true;
2963
- state.index += 1;
2964
- return true;
2965
- }
2966
- if (normalized[state.index] === '"') {
2967
- state.inDoubleQuote = true;
2968
- state.index += 1;
2969
- return true;
2970
- }
2971
- return false;
3006
+ function formatAllowlistReason({
3007
+ label,
3008
+ ruleId,
3009
+ reason,
3010
+ rule
3011
+ }) {
3012
+ const meta = rule ? formatAllowlistMetadata(rule) : "";
3013
+ return meta ? `[allowlist:${label}] ${ruleId}: ${reason} (${meta})` : `[allowlist:${label}] ${ruleId}: ${reason}`;
2972
3014
  }
2973
- function tryConsumeDollarQuotedFragment(statement, state, addFragment) {
2974
- if (statement[state.index] !== "$") return false;
2975
- const parsedDollar = parseDollarQuotedLiteral(statement, state.index);
2976
- if (!parsedDollar) return false;
2977
- const previousWord = getPreviousWord(statement, state.index);
2978
- if (previousWord && EMBEDDABLE_SQL_HINT_WORDS.has(previousWord)) {
2979
- addFragment(parsedDollar.body);
2980
- }
2981
- state.index = parsedDollar.next;
2982
- return true;
3015
+
3016
+ // src/commands/db/commands/db-sync/directory-placement-check.ts
3017
+ init_esm_shims();
3018
+
3019
+ // src/commands/db/utils/schema-precheck-budget.ts
3020
+ init_esm_shims();
3021
+ var ONE_MB = 1024 * 1024;
3022
+ var ONE_GB = 1024 * ONE_MB;
3023
+ function parseIntEnv(name, fallback, min, max) {
3024
+ const raw = process.env[name];
3025
+ if (!raw) return fallback;
3026
+ const value = Number.parseInt(raw, 10);
3027
+ if (!Number.isFinite(value) || Number.isNaN(value)) return fallback;
3028
+ const normalized = Math.trunc(value);
3029
+ if (normalized < min) return min;
3030
+ if (normalized > max) return max;
3031
+ return normalized;
2983
3032
  }
2984
- function tryExtractExecuteFormatLiteral(statement, normalized, tokenEnd) {
2985
- let cursor = skipWhitespaceAndComments(statement, tokenEnd);
2986
- const maybeFormat = parseWordAt(normalized, cursor);
2987
- if (!maybeFormat || maybeFormat.value !== "FORMAT") return null;
2988
- cursor = skipWhitespaceAndComments(statement, maybeFormat.end);
2989
- if (statement[cursor] !== "(") return null;
2990
- const afterParen = skipWhitespaceAndComments(statement, cursor + 1);
2991
- const formatCapture = [];
2992
- const parsed = parsePotentialEmbeddedSqlLiteral(statement, afterParen, formatCapture);
2993
- if (parsed <= afterParen || formatCapture.length === 0) return null;
2994
- return { fragment: formatCapture[0] ?? "", nextIndex: parsed };
3033
+ var RUNA_SCHEMA_PRECHECK_MAX_FILES = parseIntEnv(
3034
+ "RUNA_SCHEMA_PRECHECK_MAX_FILES",
3035
+ 3e3,
3036
+ 100,
3037
+ 2e4
3038
+ );
3039
+ var RUNA_SCHEMA_PRECHECK_MAX_TOTAL_BYTES = parseIntEnv(
3040
+ "RUNA_SCHEMA_PRECHECK_MAX_TOTAL_BYTES",
3041
+ 512 * ONE_MB,
3042
+ 32 * ONE_MB,
3043
+ 20 * ONE_GB
3044
+ );
3045
+ var RUNA_SCHEMA_PRECHECK_MAX_FILE_BYTES = parseIntEnv(
3046
+ "RUNA_SCHEMA_PRECHECK_MAX_FILE_BYTES",
3047
+ 64 * ONE_MB,
3048
+ 1 * ONE_MB,
3049
+ 512 * ONE_MB
3050
+ );
3051
+ var RUNA_PLAN_PRECHECK_MAX_BYTES = parseIntEnv(
3052
+ "RUNA_PLAN_PRECHECK_MAX_BYTES",
3053
+ 128 * ONE_MB,
3054
+ 8 * ONE_MB,
3055
+ 4 * ONE_GB
3056
+ );
3057
+ var RUNA_PLAN_PRECHECK_MAX_STATEMENTS = parseIntEnv(
3058
+ "RUNA_PLAN_PRECHECK_MAX_STATEMENTS",
3059
+ 4e3,
3060
+ 100,
3061
+ 5e4
3062
+ );
3063
+ function formatBytes2(value) {
3064
+ if (value >= ONE_MB * 1024) return `${(value / (ONE_MB * 1024)).toFixed(1)} GB`;
3065
+ if (value >= ONE_MB) return `${(value / ONE_MB).toFixed(1)} MB`;
3066
+ return `${(value / 1024).toFixed(1)} KB`;
2995
3067
  }
2996
- function tryExtractExecuteLiteral(statement, tokenEnd) {
2997
- const cursor = skipWhitespaceAndComments(statement, tokenEnd);
2998
- const executeCapture = [];
2999
- const parsed = parsePotentialEmbeddedSqlLiteral(statement, cursor, executeCapture);
3000
- if (parsed <= cursor || executeCapture.length === 0) return null;
3001
- return { fragment: executeCapture[0] ?? "", nextIndex: parsed };
3068
+ function buildBudgetExceededReason(context, files, bytes, maxFiles, maxBytes) {
3069
+ return `${context}: budget exceeded (files=${files}/${maxFiles}, bytes=${formatBytes2(bytes)}/${formatBytes2(maxBytes)}). Stop and review with stricter scoping or increase RUNA_SCHEMA_PRECHECK_MAX_* settings only after approval.`;
3002
3070
  }
3003
- function tryConsumeWordToken(statement, normalized, state, addFragment) {
3004
- const token = parseWordAt(normalized, state.index);
3005
- if (!token) return false;
3006
- if (token.value !== "EXECUTE") {
3007
- state.index = token.end;
3008
- return true;
3009
- }
3010
- const formatLiteral = tryExtractExecuteFormatLiteral(statement, normalized, token.end);
3011
- if (formatLiteral) {
3012
- addFragment(formatLiteral.fragment);
3013
- state.index = formatLiteral.nextIndex;
3014
- return true;
3071
+ function createSchemaPrecheckBudgetState() {
3072
+ return {
3073
+ scannedFiles: 0,
3074
+ scannedBytes: 0,
3075
+ maxFiles: RUNA_SCHEMA_PRECHECK_MAX_FILES,
3076
+ maxBytes: RUNA_SCHEMA_PRECHECK_MAX_TOTAL_BYTES,
3077
+ maxFileBytes: RUNA_SCHEMA_PRECHECK_MAX_FILE_BYTES
3078
+ };
3079
+ }
3080
+ function shouldAbortSchemaPrecheckForBudget(state, filePath) {
3081
+ const size = statSync(filePath).size;
3082
+ if (size > state.maxFileBytes) {
3083
+ return `Unscanned schema file ${filePath}: ${formatBytes2(size)} exceeds per-file limit ${formatBytes2(
3084
+ state.maxFileBytes
3085
+ )}`;
3015
3086
  }
3016
- const executeLiteral = tryExtractExecuteLiteral(statement, token.end);
3017
- if (executeLiteral) {
3018
- addFragment(executeLiteral.fragment);
3019
- state.index = executeLiteral.nextIndex;
3020
- return true;
3087
+ if (state.scannedFiles + 1 > state.maxFiles) {
3088
+ return `Schema file scan budget exceeds ${state.maxFiles} files at ${path12.basename(filePath)}.`;
3021
3089
  }
3022
- state.index = token.end;
3023
- return true;
3024
- }
3025
- function collectEmbeddableSqlFragments(statement, fragments, depth, maxDepth, seen) {
3026
- if (!statement || statement.trim().length === 0) return;
3027
- if (depth > maxDepth) return;
3028
- const normalized = statement.toUpperCase();
3029
- const state = {
3030
- index: 0,
3031
- inSingleQuote: false,
3032
- inDoubleQuote: false
3033
- };
3034
- const addFragment = createFragmentCollector(fragments, depth, maxDepth, seen);
3035
- while (state.index < normalized.length) {
3036
- if (tryAdvanceWithinQuotedText(normalized, state)) continue;
3037
- if (tryAdvanceComment(normalized, state)) continue;
3038
- if (tryEnterQuotedText(normalized, state)) continue;
3039
- if (tryConsumeDollarQuotedFragment(statement, state, addFragment)) continue;
3040
- if (tryConsumeWordToken(statement, normalized, state, addFragment)) continue;
3041
- state.index += 1;
3090
+ const projectedBytes = state.scannedBytes + size;
3091
+ if (projectedBytes > state.maxBytes) {
3092
+ return `Schema scan budget exceeds total size limit ${formatBytes2(state.maxBytes)}.`;
3042
3093
  }
3043
- }
3044
- function extractEmbeddableSqlFragments(statement) {
3045
- const fragments = [];
3046
- collectEmbeddableSqlFragments(
3047
- statement,
3048
- fragments,
3049
- 0,
3050
- EMBEDDABLE_SQL_MAX_RECURSION_DEPTH,
3051
- /* @__PURE__ */ new Set()
3052
- );
3053
- return dedupeAndSort(fragments);
3054
- }
3055
- function normalizeSqlForPlacementCheck(content) {
3056
- return blankDollarQuotedBodies(stripSqlComments(content)).toUpperCase();
3057
- }
3058
- function isPartitionOfCreateTable(statement) {
3059
- return /\bPARTITION\s+OF\b/.test(statement);
3060
- }
3061
- function dedupeAndSort(lines) {
3062
- const seen = /* @__PURE__ */ new Set();
3063
- return [...lines].sort((a, b) => a.localeCompare(b)).filter((line) => {
3064
- if (seen.has(line)) return false;
3065
- seen.add(line);
3066
- return true;
3067
- });
3094
+ state.scannedBytes = projectedBytes;
3095
+ state.scannedFiles += 1;
3096
+ return null;
3068
3097
  }
3069
3098
 
3070
- // src/commands/db/commands/db-sync/risk-reporter.ts
3099
+ // src/commands/db/utils/sql-file-collector.ts
3071
3100
  init_esm_shims();
3072
-
3073
- // src/commands/db/commands/db-sync/types.ts
3074
- init_esm_shims();
3075
- var DIRECTORY_RISK_ORDER = ["high", "medium", "low"];
3076
- var PLAN_BOUNDARY_CONTEXT_FILE = "pg-schema-diff plan.sql";
3077
- var GRANT_STATEMENT_RULE_TEXT = "Grant/REVOKE statements are usually idempotent/bootstrap ACL setup and should be treated as such";
3078
-
3079
- // src/commands/db/commands/db-sync/risk-reporter.ts
3080
- var DIRECTORY_RISK_WEIGHT = {
3081
- high: 3,
3082
- medium: 2,
3083
- low: 1
3084
- };
3085
- function getDirectoryRiskWeight(level) {
3086
- return DIRECTORY_RISK_WEIGHT[level];
3101
+ var IGNORED_DIRECTORY_NAMES = /* @__PURE__ */ new Set([
3102
+ "compat",
3103
+ "archive",
3104
+ "legacy",
3105
+ "deprecated",
3106
+ "backup",
3107
+ "node_modules",
3108
+ ".git",
3109
+ "dist",
3110
+ "build"
3111
+ ]);
3112
+ function shouldSkipDirectory(name) {
3113
+ return IGNORED_DIRECTORY_NAMES.has(name.toLowerCase());
3087
3114
  }
3088
- function compareDirectoryRiskPriority(left, right) {
3089
- const weightDelta = getDirectoryRiskWeight(left.level) - getDirectoryRiskWeight(right.level);
3090
- if (weightDelta !== 0) {
3091
- return weightDelta;
3115
+ function scanDirectory(dir, queue, sqlFiles) {
3116
+ try {
3117
+ const entries = readdirSync(dir, { withFileTypes: true });
3118
+ for (const entry of entries) {
3119
+ const fullPath = path12.join(dir, entry.name);
3120
+ if (entry.isDirectory()) {
3121
+ if (!shouldSkipDirectory(entry.name)) queue.push(fullPath);
3122
+ } else if (entry.isFile() && entry.name.endsWith(".sql")) {
3123
+ sqlFiles.push(fullPath);
3124
+ }
3125
+ }
3126
+ } catch {
3092
3127
  }
3093
- const leftLine = left.line ?? 0;
3094
- const rightLine = right.line ?? 0;
3095
- if (leftLine !== rightLine) {
3096
- return leftLine - rightLine;
3128
+ }
3129
+ function* collectSqlFilesRecursively(baseDir) {
3130
+ const queue = [baseDir];
3131
+ const sqlFiles = [];
3132
+ let index = 0;
3133
+ while (index < queue.length) {
3134
+ const currentDir = queue[index++];
3135
+ if (!currentDir) continue;
3136
+ scanDirectory(currentDir, queue, sqlFiles);
3137
+ }
3138
+ for (const file of sqlFiles) {
3139
+ yield file;
3097
3140
  }
3098
- return left.message.localeCompare(right.message);
3099
3141
  }
3100
- function selectHighestPriorityRisk(entries) {
3101
- if (entries.length === 0) return void 0;
3102
- const byPriority = [...entries].sort((left, right) => {
3103
- const priorityDelta = compareDirectoryRiskPriority(right, left);
3104
- if (priorityDelta !== 0) return priorityDelta;
3105
- const leftMessage = left.message.toLowerCase();
3106
- const rightMessage = right.message.toLowerCase();
3107
- return leftMessage.localeCompare(rightMessage);
3108
- });
3109
- return byPriority[0];
3142
+
3143
+ // src/commands/db/commands/db-sync/boundary-classifier.ts
3144
+ init_esm_shims();
3145
+
3146
+ // src/commands/db/commands/db-sync/sql-parser.ts
3147
+ init_esm_shims();
3148
+ function isWordChar(char) {
3149
+ const code = char.charCodeAt(0);
3150
+ return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || char === "_";
3110
3151
  }
3111
- function buildDirectoryRiskKey(entry) {
3112
- return `${entry.level}:${entry.file}:${entry.line ?? 0}:${entry.message}`;
3152
+ function isWhitespaceChar(char) {
3153
+ return char === " " || char === " " || char === "\n" || char === "\r" || char === "\f";
3113
3154
  }
3114
- function dedupeDirectoryRisksBySeverity(entries) {
3115
- const byLocationAndMessage = /* @__PURE__ */ new Map();
3116
- for (const entry of entries) {
3117
- const key = `${entry.file}:${entry.line ?? 0}:${entry.message}`;
3118
- const existing = byLocationAndMessage.get(key);
3119
- if (!existing || getDirectoryRiskWeight(entry.level) > getDirectoryRiskWeight(existing.level)) {
3120
- byLocationAndMessage.set(key, entry);
3121
- }
3155
+ function parseWordAt(statement, start) {
3156
+ if (start < 0 || start >= statement.length) return null;
3157
+ const first = statement[start];
3158
+ if (!first || !isWordChar(first)) return null;
3159
+ let end = start + 1;
3160
+ while (end < statement.length && isWordChar(statement[end] ?? "")) {
3161
+ end += 1;
3122
3162
  }
3123
- return [...byLocationAndMessage.values()].sort((left, right) => {
3124
- const priorityDelta = getDirectoryRiskWeight(right.level) - getDirectoryRiskWeight(left.level);
3125
- if (priorityDelta !== 0) return priorityDelta;
3126
- const levelDelta = DIRECTORY_RISK_ORDER.indexOf(right.level) - DIRECTORY_RISK_ORDER.indexOf(left.level);
3127
- if (levelDelta !== 0) return levelDelta;
3128
- const leftLine = left.line ?? 0;
3129
- const rightLine = right.line ?? 0;
3130
- if (leftLine !== rightLine) return leftLine - rightLine;
3131
- return left.message.localeCompare(right.message);
3132
- });
3163
+ return { value: statement.slice(start, end), start, end };
3133
3164
  }
3134
- function printList(title, items, logger4) {
3135
- if (items.length === 0) return;
3136
- logger4.warn(`
3137
- ${title} (${items.length}):`);
3138
- for (const item of items) {
3139
- logger4.info(` \u2022 ${item}`);
3165
+ function skipWhitespace(statement, start) {
3166
+ let index = start;
3167
+ while (index < statement.length && isWhitespaceChar(statement[index] ?? "")) {
3168
+ index += 1;
3140
3169
  }
3170
+ return index;
3141
3171
  }
3142
- function stripCommonPrefix(filePath) {
3143
- return filePath.startsWith(process.cwd()) ? filePath.replace(`${process.cwd()}/`, "") : filePath;
3172
+ function skipLineComment(statement, start) {
3173
+ let index = start;
3174
+ while (index < statement.length && statement[index] !== "\n") {
3175
+ index += 1;
3176
+ }
3177
+ return index;
3144
3178
  }
3145
- function formatDeclarativeRiskMessage(risk) {
3146
- const line = risk.line ? `:${risk.line}` : "";
3147
- return `${stripCommonPrefix(risk.file)}${line} ${risk.description}${risk.mitigation ? ` | Fix: ${risk.mitigation}` : ""}`;
3179
+ function skipBlockComment(statement, start) {
3180
+ let index = start;
3181
+ while (index + 1 < statement.length && !(statement[index] === "*" && statement[index + 1] === "/")) {
3182
+ index += 1;
3183
+ }
3184
+ return index + 1 < statement.length ? index + 2 : index;
3148
3185
  }
3149
- function buildCompactFindingSummary(rawItems) {
3150
- const grouped = /* @__PURE__ */ new Map();
3151
- for (const item of rawItems) {
3152
- const withoutPrefix = item.replace(/^\s*\[[^\]]+\]\s*/, "");
3153
- const delimiter = ": ";
3154
- const separator = withoutPrefix.indexOf(delimiter);
3155
- if (separator <= 0) {
3156
- const existing2 = grouped.get(withoutPrefix);
3157
- if (!existing2) {
3158
- grouped.set(withoutPrefix, { message: withoutPrefix, count: 1, samples: [] });
3159
- } else {
3160
- existing2.count += 1;
3161
- }
3186
+ function skipWhitespaceAndComments(statement, start) {
3187
+ let index = Math.max(0, start);
3188
+ while (index < statement.length) {
3189
+ const afterWhitespace = skipWhitespace(statement, index);
3190
+ if (afterWhitespace !== index) {
3191
+ index = afterWhitespace;
3162
3192
  continue;
3163
3193
  }
3164
- const source = withoutPrefix.slice(0, separator).trim();
3165
- const message = withoutPrefix.slice(separator + delimiter.length).trim();
3166
- const existing = grouped.get(message);
3167
- if (!existing) {
3168
- grouped.set(message, { message, count: 1, samples: source ? [source] : [] });
3194
+ if (statement[index] === "-" && statement[index + 1] === "-") {
3195
+ index = skipLineComment(statement, index + 2);
3169
3196
  continue;
3170
3197
  }
3171
- existing.count += 1;
3172
- if (source && !existing.samples.includes(source)) {
3173
- existing.samples.push(source);
3174
- }
3175
- }
3176
- return [...grouped.values()].sort((left, right) => {
3177
- if (right.count !== left.count) return right.count - left.count;
3178
- return left.message.localeCompare(right.message);
3179
- });
3180
- }
3181
- function printCompactSummary(logger4, title, rawItems, topLimit) {
3182
- if (rawItems.length === 0) {
3183
- return;
3184
- }
3185
- const findingSummary = buildCompactFindingSummary(rawItems);
3186
- const top = findingSummary.slice(0, Math.max(1, topLimit));
3187
- logger4.warn(`
3188
- ${title} (total ${rawItems.length} item(s), ${top.length} reason pattern(s))`);
3189
- for (const finding of top) {
3190
- logger4.info(` - ${finding.count}x ${finding.message}`);
3191
- if (finding.samples.length > 0) {
3192
- logger4.info(` samples:`);
3193
- for (const sample of finding.samples.slice(0, 2)) {
3194
- logger4.info(` - ${sample}`);
3195
- }
3198
+ if (statement[index] === "/" && statement[index + 1] === "*") {
3199
+ index = skipBlockComment(statement, index + 2);
3200
+ continue;
3196
3201
  }
3202
+ break;
3197
3203
  }
3198
- const extra = findingSummary.length - top.length;
3199
- if (extra > 0) {
3200
- logger4.info(` ... and ${extra} more reason patterns`);
3201
- }
3202
- }
3203
-
3204
- // src/commands/db/commands/db-sync/boundary-classifier.ts
3205
- function isBoundaryRelevantDdlStatement(statement) {
3206
- return /^\s*(?:CREATE|ALTER|DROP|RENAME|TRUNCATE|COMMENT)\b/i.test(statement);
3204
+ return index;
3207
3205
  }
3208
- function isPlanBoundaryAmbiguous(statement) {
3209
- const trimmed = statement.trim();
3210
- if (!trimmed) return false;
3211
- if (/^\s*--/.test(trimmed)) return false;
3212
- return isBoundaryRelevantDdlStatement(trimmed);
3206
+ function getPreviousWord(statement, start) {
3207
+ let index = start - 1;
3208
+ while (index >= 0 && isWhitespaceChar(statement[index] ?? "")) index -= 1;
3209
+ if (index < 0 || !isWordChar(statement[index] ?? "")) return "";
3210
+ const end = index;
3211
+ while (index >= 0 && isWordChar(statement[index] ?? "")) index -= 1;
3212
+ return statement.slice(index + 1, end + 1).toUpperCase();
3213
3213
  }
3214
- function classifyUnknownObjectBoundary(file, object, fileType, policy) {
3215
- const inDeclarative = policy.declarativePreferredObjects.has(object);
3216
- const inIdempotent = policy.idempotentPreferredObjects.has(object);
3217
- if (fileType === "declarative") {
3218
- if (inIdempotent && !inDeclarative) {
3219
- return {
3220
- file,
3221
- level: "high",
3222
- message: `Object "${object}" is idempotent-preferred and should be in supabase/schemas/idempotent`
3223
- };
3224
- }
3225
- if (!inDeclarative && !inIdempotent) {
3226
- return {
3227
- file,
3228
- level: "medium",
3229
- message: `Unrecognized declarative object "${object}" detected; review placement (runtime/setup candidate)`
3230
- };
3231
- }
3232
- return null;
3233
- }
3234
- if (inDeclarative && !inIdempotent) {
3214
+ function parseDollarQuotedLiteral(statement, start) {
3215
+ if (statement[start] !== "$") return null;
3216
+ let tagEnd = start + 1;
3217
+ if (tagEnd >= statement.length) return null;
3218
+ if (statement[tagEnd] === "$") {
3219
+ const delimiter2 = "$$";
3220
+ const bodyStart2 = tagEnd + 1;
3221
+ const bodyEnd2 = statement.indexOf(delimiter2, bodyStart2);
3222
+ if (bodyEnd2 < 0) return null;
3235
3223
  return {
3236
- file,
3237
- level: "high",
3238
- message: `Object "${object}" is declarative-preferred and should be in supabase/schemas/declarative`
3224
+ body: statement.slice(bodyStart2, bodyEnd2),
3225
+ next: bodyEnd2 + delimiter2.length
3239
3226
  };
3240
3227
  }
3241
- if (!inDeclarative && !inIdempotent) {
3242
- return {
3243
- file,
3244
- level: "high",
3245
- message: `Unrecognized idempotent object "${object}" detected; review placement (likely declarative SSOT object)`
3246
- };
3228
+ const firstChar = statement[tagEnd];
3229
+ if (!firstChar || !/[A-Za-z_]/.test(firstChar)) return null;
3230
+ while (tagEnd < statement.length && /[A-Za-z0-9_]/.test(statement[tagEnd] ?? "")) {
3231
+ tagEnd += 1;
3247
3232
  }
3248
- return null;
3233
+ if (statement[tagEnd] !== "$") return null;
3234
+ const tag = statement.slice(start + 1, tagEnd);
3235
+ const delimiter = `$${tag}$`;
3236
+ const bodyStart = tagEnd + 1;
3237
+ const bodyEnd = statement.indexOf(delimiter, bodyStart);
3238
+ if (bodyEnd < 0) return null;
3239
+ return {
3240
+ body: statement.slice(bodyStart, bodyEnd),
3241
+ next: bodyEnd + delimiter.length
3242
+ };
3249
3243
  }
3250
- function matchesRiskRule(rule, statement) {
3251
- return typeof rule.pattern === "function" ? rule.pattern(statement) : rule.pattern.test(statement);
3244
+ function parseSingleQuotedLiteral(statement, start) {
3245
+ if (statement[start] !== "'") return null;
3246
+ let index = start + 1;
3247
+ let body = "";
3248
+ while (index < statement.length) {
3249
+ const char = statement[index];
3250
+ const next = statement[index + 1] ?? "";
3251
+ if (char === "'") {
3252
+ if (next === "'") {
3253
+ body += "'";
3254
+ index += 2;
3255
+ continue;
3256
+ }
3257
+ return { body, next: index + 1 };
3258
+ }
3259
+ if (char === "\\" && index + 1 < statement.length) {
3260
+ body += next;
3261
+ index += 2;
3262
+ continue;
3263
+ }
3264
+ body += char;
3265
+ index += 1;
3266
+ }
3267
+ return null;
3268
+ }
3269
+ function parsePotentialEmbeddedSqlLiteral(statement, start, fragments) {
3270
+ const index = skipWhitespaceAndComments(statement, start);
3271
+ if (index >= statement.length) return index;
3272
+ const maybePrefix = (statement[index] ?? "").toUpperCase();
3273
+ if (maybePrefix === "E" && statement[index + 1] === "'") {
3274
+ const parsed = parseSingleQuotedLiteral(statement, index + 1);
3275
+ if (parsed && parsed.body.trim().length > 0) {
3276
+ fragments.push(parsed.body);
3277
+ return parsed.next;
3278
+ }
3279
+ return index + 1;
3280
+ }
3281
+ const parsedDollar = parseDollarQuotedLiteral(statement, index);
3282
+ if (parsedDollar) {
3283
+ if (parsedDollar.body.trim().length > 0) fragments.push(parsedDollar.body);
3284
+ return parsedDollar.next;
3285
+ }
3286
+ if (statement[index] === "'") {
3287
+ const parsed = parseSingleQuotedLiteral(statement, index);
3288
+ if (parsed && parsed.body.trim().length > 0) {
3289
+ fragments.push(parsed.body.replace(/\\'/g, "'"));
3290
+ return parsed.next;
3291
+ }
3292
+ return index + 1;
3293
+ }
3294
+ return index + 1;
3295
+ }
3296
+ var EMBEDDABLE_SQL_HINT_WORDS = /* @__PURE__ */ new Set(["DO", "AS", "EXECUTE", "FORMAT"]);
3297
+ var EMBEDDABLE_SQL_MAX_RECURSION_DEPTH = 3;
3298
+ var PLPGSQL_RECURSIVE_PATTERN = /\b(?:DO|EXECUTE|FORMAT)\b/i;
3299
+ function shouldRecurseIntoEmbeddableFragment(statement) {
3300
+ return PLPGSQL_RECURSIVE_PATTERN.test(statement);
3301
+ }
3302
+ function createFragmentCollector(fragments, depth, maxDepth, seen) {
3303
+ return (fragment) => {
3304
+ const candidate = fragment.trim();
3305
+ if (!candidate) return;
3306
+ if (!seen.has(candidate)) {
3307
+ seen.add(candidate);
3308
+ fragments.push(candidate);
3309
+ }
3310
+ if (depth < maxDepth && shouldRecurseIntoEmbeddableFragment(candidate)) {
3311
+ collectEmbeddableSqlFragments(candidate, fragments, depth + 1, maxDepth, seen);
3312
+ }
3313
+ };
3314
+ }
3315
+ function tryAdvanceWithinQuotedText(normalized, state) {
3316
+ const char = normalized[state.index];
3317
+ if (!char) return false;
3318
+ if (state.inSingleQuote) {
3319
+ if (char === "'" && normalized[state.index + 1] === "'") {
3320
+ state.index += 2;
3321
+ return true;
3322
+ }
3323
+ if (char === "'") {
3324
+ state.inSingleQuote = false;
3325
+ }
3326
+ state.index += 1;
3327
+ return true;
3328
+ }
3329
+ if (state.inDoubleQuote) {
3330
+ if (char === '"' && normalized[state.index + 1] === '"') {
3331
+ state.index += 2;
3332
+ return true;
3333
+ }
3334
+ if (char === '"') {
3335
+ state.inDoubleQuote = false;
3336
+ }
3337
+ state.index += 1;
3338
+ return true;
3339
+ }
3340
+ return false;
3341
+ }
3342
+ function tryAdvanceComment(normalized, state) {
3343
+ if (normalized[state.index] === "-" && normalized[state.index + 1] === "-") {
3344
+ state.index = skipLineComment(normalized, state.index + 2);
3345
+ return true;
3346
+ }
3347
+ if (normalized[state.index] === "/" && normalized[state.index + 1] === "*") {
3348
+ state.index = skipBlockComment(normalized, state.index + 2);
3349
+ return true;
3350
+ }
3351
+ return false;
3352
+ }
3353
+ function tryEnterQuotedText(normalized, state) {
3354
+ if (normalized[state.index] === "'") {
3355
+ state.inSingleQuote = true;
3356
+ state.index += 1;
3357
+ return true;
3358
+ }
3359
+ if (normalized[state.index] === '"') {
3360
+ state.inDoubleQuote = true;
3361
+ state.index += 1;
3362
+ return true;
3363
+ }
3364
+ return false;
3365
+ }
3366
+ function tryConsumeDollarQuotedFragment(statement, state, addFragment) {
3367
+ if (statement[state.index] !== "$") return false;
3368
+ const parsedDollar = parseDollarQuotedLiteral(statement, state.index);
3369
+ if (!parsedDollar) return false;
3370
+ const previousWord = getPreviousWord(statement, state.index);
3371
+ if (previousWord && EMBEDDABLE_SQL_HINT_WORDS.has(previousWord)) {
3372
+ addFragment(parsedDollar.body);
3373
+ }
3374
+ state.index = parsedDollar.next;
3375
+ return true;
3376
+ }
3377
+ function tryExtractExecuteFormatLiteral(statement, normalized, tokenEnd) {
3378
+ let cursor = skipWhitespaceAndComments(statement, tokenEnd);
3379
+ const maybeFormat = parseWordAt(normalized, cursor);
3380
+ if (!maybeFormat || maybeFormat.value !== "FORMAT") return null;
3381
+ cursor = skipWhitespaceAndComments(statement, maybeFormat.end);
3382
+ if (statement[cursor] !== "(") return null;
3383
+ const afterParen = skipWhitespaceAndComments(statement, cursor + 1);
3384
+ const formatCapture = [];
3385
+ const parsed = parsePotentialEmbeddedSqlLiteral(statement, afterParen, formatCapture);
3386
+ if (parsed <= afterParen || formatCapture.length === 0) return null;
3387
+ return { fragment: formatCapture[0] ?? "", nextIndex: parsed };
3388
+ }
3389
+ function tryExtractExecuteLiteral(statement, tokenEnd) {
3390
+ const cursor = skipWhitespaceAndComments(statement, tokenEnd);
3391
+ const executeCapture = [];
3392
+ const parsed = parsePotentialEmbeddedSqlLiteral(statement, cursor, executeCapture);
3393
+ if (parsed <= cursor || executeCapture.length === 0) return null;
3394
+ return { fragment: executeCapture[0] ?? "", nextIndex: parsed };
3395
+ }
3396
+ function tryConsumeWordToken(statement, normalized, state, addFragment) {
3397
+ const token = parseWordAt(normalized, state.index);
3398
+ if (!token) return false;
3399
+ if (token.value !== "EXECUTE") {
3400
+ state.index = token.end;
3401
+ return true;
3402
+ }
3403
+ const formatLiteral = tryExtractExecuteFormatLiteral(statement, normalized, token.end);
3404
+ if (formatLiteral) {
3405
+ addFragment(formatLiteral.fragment);
3406
+ state.index = formatLiteral.nextIndex;
3407
+ return true;
3408
+ }
3409
+ const executeLiteral = tryExtractExecuteLiteral(statement, token.end);
3410
+ if (executeLiteral) {
3411
+ addFragment(executeLiteral.fragment);
3412
+ state.index = executeLiteral.nextIndex;
3413
+ return true;
3414
+ }
3415
+ state.index = token.end;
3416
+ return true;
3417
+ }
3418
+ function collectEmbeddableSqlFragments(statement, fragments, depth, maxDepth, seen) {
3419
+ if (!statement || statement.trim().length === 0) return;
3420
+ if (depth > maxDepth) return;
3421
+ const normalized = statement.toUpperCase();
3422
+ const state = {
3423
+ index: 0,
3424
+ inSingleQuote: false,
3425
+ inDoubleQuote: false
3426
+ };
3427
+ const addFragment = createFragmentCollector(fragments, depth, maxDepth, seen);
3428
+ while (state.index < normalized.length) {
3429
+ if (tryAdvanceWithinQuotedText(normalized, state)) continue;
3430
+ if (tryAdvanceComment(normalized, state)) continue;
3431
+ if (tryEnterQuotedText(normalized, state)) continue;
3432
+ if (tryConsumeDollarQuotedFragment(statement, state, addFragment)) continue;
3433
+ if (tryConsumeWordToken(statement, normalized, state, addFragment)) continue;
3434
+ state.index += 1;
3435
+ }
3436
+ }
3437
+ function extractEmbeddableSqlFragments(statement) {
3438
+ const fragments = [];
3439
+ collectEmbeddableSqlFragments(
3440
+ statement,
3441
+ fragments,
3442
+ 0,
3443
+ EMBEDDABLE_SQL_MAX_RECURSION_DEPTH,
3444
+ /* @__PURE__ */ new Set()
3445
+ );
3446
+ return dedupeAndSort(fragments);
3447
+ }
3448
+ function normalizeSqlForPlacementCheck(content) {
3449
+ return blankDollarQuotedBodies(stripSqlComments(content)).toUpperCase();
3450
+ }
3451
+ function isPartitionOfCreateTable(statement) {
3452
+ return /\bPARTITION\s+OF\b/.test(statement);
3453
+ }
3454
+ function dedupeAndSort(lines) {
3455
+ const seen = /* @__PURE__ */ new Set();
3456
+ return [...lines].sort((a, b) => a.localeCompare(b)).filter((line) => {
3457
+ if (seen.has(line)) return false;
3458
+ seen.add(line);
3459
+ return true;
3460
+ });
3461
+ }
3462
+
3463
+ // src/commands/db/commands/db-sync/risk-reporter.ts
3464
+ init_esm_shims();
3465
+
3466
+ // src/commands/db/commands/db-sync/types.ts
3467
+ init_esm_shims();
3468
+ var DIRECTORY_RISK_ORDER = ["high", "medium", "low"];
3469
+ var PLAN_BOUNDARY_CONTEXT_FILE = "pg-schema-diff plan.sql";
3470
+ var GRANT_STATEMENT_RULE_TEXT = "Grant/REVOKE statements are usually idempotent/bootstrap ACL setup and should be treated as such";
3471
+
3472
+ // src/commands/db/commands/db-sync/risk-reporter.ts
3473
+ var DIRECTORY_RISK_WEIGHT = {
3474
+ high: 3,
3475
+ medium: 2,
3476
+ low: 1
3477
+ };
3478
+ function getDirectoryRiskWeight(level) {
3479
+ return DIRECTORY_RISK_WEIGHT[level];
3480
+ }
3481
+ function compareDirectoryRiskPriority(left, right) {
3482
+ const weightDelta = getDirectoryRiskWeight(left.level) - getDirectoryRiskWeight(right.level);
3483
+ if (weightDelta !== 0) {
3484
+ return weightDelta;
3485
+ }
3486
+ const leftLine = left.line ?? 0;
3487
+ const rightLine = right.line ?? 0;
3488
+ if (leftLine !== rightLine) {
3489
+ return leftLine - rightLine;
3490
+ }
3491
+ return left.message.localeCompare(right.message);
3492
+ }
3493
+ function selectHighestPriorityRisk(entries) {
3494
+ if (entries.length === 0) return void 0;
3495
+ const byPriority = [...entries].sort((left, right) => {
3496
+ const priorityDelta = compareDirectoryRiskPriority(right, left);
3497
+ if (priorityDelta !== 0) return priorityDelta;
3498
+ const leftMessage = left.message.toLowerCase();
3499
+ const rightMessage = right.message.toLowerCase();
3500
+ return leftMessage.localeCompare(rightMessage);
3501
+ });
3502
+ return byPriority[0];
3503
+ }
3504
+ function buildDirectoryRiskKey(entry) {
3505
+ return `${entry.level}:${entry.file}:${entry.line ?? 0}:${entry.message}`;
3506
+ }
3507
+ function dedupeDirectoryRisksBySeverity(entries) {
3508
+ const byLocationAndMessage = /* @__PURE__ */ new Map();
3509
+ for (const entry of entries) {
3510
+ const key = `${entry.file}:${entry.line ?? 0}:${entry.message}`;
3511
+ const existing = byLocationAndMessage.get(key);
3512
+ if (!existing || getDirectoryRiskWeight(entry.level) > getDirectoryRiskWeight(existing.level)) {
3513
+ byLocationAndMessage.set(key, entry);
3514
+ }
3515
+ }
3516
+ return [...byLocationAndMessage.values()].sort((left, right) => {
3517
+ const priorityDelta = getDirectoryRiskWeight(right.level) - getDirectoryRiskWeight(left.level);
3518
+ if (priorityDelta !== 0) return priorityDelta;
3519
+ const levelDelta = DIRECTORY_RISK_ORDER.indexOf(right.level) - DIRECTORY_RISK_ORDER.indexOf(left.level);
3520
+ if (levelDelta !== 0) return levelDelta;
3521
+ const leftLine = left.line ?? 0;
3522
+ const rightLine = right.line ?? 0;
3523
+ if (leftLine !== rightLine) return leftLine - rightLine;
3524
+ return left.message.localeCompare(right.message);
3525
+ });
3526
+ }
3527
+ function printList(title, items, logger4) {
3528
+ if (items.length === 0) return;
3529
+ logger4.warn(`
3530
+ ${title} (${items.length}):`);
3531
+ for (const item of items) {
3532
+ logger4.info(` \u2022 ${item}`);
3533
+ }
3534
+ }
3535
+ function stripCommonPrefix(filePath) {
3536
+ return filePath.startsWith(process.cwd()) ? filePath.replace(`${process.cwd()}/`, "") : filePath;
3537
+ }
3538
+ function formatDeclarativeRiskMessage(risk) {
3539
+ const line = risk.line ? `:${risk.line}` : "";
3540
+ return `${stripCommonPrefix(risk.file)}${line} ${risk.description}${risk.mitigation ? ` | Fix: ${risk.mitigation}` : ""}`;
3541
+ }
3542
+ function buildCompactFindingSummary(rawItems) {
3543
+ const grouped = /* @__PURE__ */ new Map();
3544
+ for (const item of rawItems) {
3545
+ const withoutPrefix = item.replace(/^\s*\[[^\]]+\]\s*/, "");
3546
+ const delimiter = ": ";
3547
+ const separator = withoutPrefix.indexOf(delimiter);
3548
+ if (separator <= 0) {
3549
+ const existing2 = grouped.get(withoutPrefix);
3550
+ if (!existing2) {
3551
+ grouped.set(withoutPrefix, { message: withoutPrefix, count: 1, samples: [] });
3552
+ } else {
3553
+ existing2.count += 1;
3554
+ }
3555
+ continue;
3556
+ }
3557
+ const source = withoutPrefix.slice(0, separator).trim();
3558
+ const message = withoutPrefix.slice(separator + delimiter.length).trim();
3559
+ const existing = grouped.get(message);
3560
+ if (!existing) {
3561
+ grouped.set(message, { message, count: 1, samples: source ? [source] : [] });
3562
+ continue;
3563
+ }
3564
+ existing.count += 1;
3565
+ if (source && !existing.samples.includes(source)) {
3566
+ existing.samples.push(source);
3567
+ }
3568
+ }
3569
+ return [...grouped.values()].sort((left, right) => {
3570
+ if (right.count !== left.count) return right.count - left.count;
3571
+ return left.message.localeCompare(right.message);
3572
+ });
3573
+ }
3574
+ function printCompactSummary(logger4, title, rawItems, topLimit) {
3575
+ if (rawItems.length === 0) {
3576
+ return;
3577
+ }
3578
+ const findingSummary = buildCompactFindingSummary(rawItems);
3579
+ const top = findingSummary.slice(0, Math.max(1, topLimit));
3580
+ logger4.warn(`
3581
+ ${title} (total ${rawItems.length} item(s), ${top.length} reason pattern(s))`);
3582
+ for (const finding of top) {
3583
+ logger4.info(` - ${finding.count}x ${finding.message}`);
3584
+ if (finding.samples.length > 0) {
3585
+ logger4.info(` samples:`);
3586
+ for (const sample of finding.samples.slice(0, 2)) {
3587
+ logger4.info(` - ${sample}`);
3588
+ }
3589
+ }
3590
+ }
3591
+ const extra = findingSummary.length - top.length;
3592
+ if (extra > 0) {
3593
+ logger4.info(` ... and ${extra} more reason patterns`);
3594
+ }
3595
+ }
3596
+
3597
+ // src/commands/db/commands/db-sync/boundary-classifier.ts
3598
+ function isBoundaryRelevantDdlStatement(statement) {
3599
+ return /^\s*(?:CREATE|ALTER|DROP|RENAME|TRUNCATE|COMMENT)\b/i.test(statement);
3600
+ }
3601
+ function isPlanBoundaryAmbiguous(statement) {
3602
+ const trimmed = statement.trim();
3603
+ if (!trimmed) return false;
3604
+ if (/^\s*--/.test(trimmed)) return false;
3605
+ return isBoundaryRelevantDdlStatement(trimmed);
3606
+ }
3607
+ function classifyUnknownObjectBoundary(file, object, fileType, policy) {
3608
+ const inDeclarative = policy.declarativePreferredObjects.has(object);
3609
+ const inIdempotent = policy.idempotentPreferredObjects.has(object);
3610
+ if (fileType === "declarative") {
3611
+ if (inIdempotent && !inDeclarative) {
3612
+ return {
3613
+ file,
3614
+ level: "high",
3615
+ message: `Object "${object}" is idempotent-preferred and should be in supabase/schemas/idempotent`
3616
+ };
3617
+ }
3618
+ if (!inDeclarative && !inIdempotent) {
3619
+ return {
3620
+ file,
3621
+ level: "medium",
3622
+ message: `Unrecognized declarative object "${object}" detected; review placement (runtime/setup candidate)`
3623
+ };
3624
+ }
3625
+ return null;
3626
+ }
3627
+ if (inDeclarative && !inIdempotent) {
3628
+ return {
3629
+ file,
3630
+ level: "high",
3631
+ message: `Object "${object}" is declarative-preferred and should be in supabase/schemas/declarative`
3632
+ };
3633
+ }
3634
+ if (!inDeclarative && !inIdempotent) {
3635
+ return {
3636
+ file,
3637
+ level: "high",
3638
+ message: `Unrecognized idempotent object "${object}" detected; review placement (likely declarative SSOT object)`
3639
+ };
3640
+ }
3641
+ return null;
3642
+ }
3643
+ function matchesRiskRule(rule, statement) {
3644
+ return typeof rule.pattern === "function" ? rule.pattern(statement) : rule.pattern.test(statement);
3252
3645
  }
3253
3646
  function collectRuleBasedCandidates(params) {
3254
3647
  const candidates = [];
@@ -3557,12 +3950,18 @@ function classifyIdempotentMisplacementRisk(file, content, boundaryPolicy) {
3557
3950
  skipUnknown: (statement, unknownObject) => isPartitionOfCreateTable(statement) && unknownObject === "TABLE"
3558
3951
  });
3559
3952
  }
3953
+ var DYNAMIC_SQL_DOWNGRADEABLE_PATTERNS = [
3954
+ "Function DDL should be in declarative",
3955
+ "Trigger DDL should be in declarative"
3956
+ ];
3957
+ var DROP_IF_EXISTS_CLEANUP = /^\s*DROP\s+(?:FUNCTION|TRIGGER|VIEW|INDEX|POLICY|TYPE|SEQUENCE)\s+IF\s+EXISTS\b/i;
3560
3958
  function classifyFileMisplacementRisks(params) {
3561
3959
  const risks = [];
3562
3960
  const relative2 = path12.relative(process.cwd(), params.file);
3563
3961
  const normalized = normalizeSqlForPlacementCheck(params.content);
3564
3962
  const statements = splitSqlStatements(normalized);
3565
3963
  const seenMessages = /* @__PURE__ */ new Set();
3964
+ const hasAllowDynamicSqlAnnotation = params.fileType === "idempotent" && ALLOW_DYNAMIC_SQL_ANNOTATION.test(params.content);
3566
3965
  for (const { statement, line } of statements) {
3567
3966
  const candidates = collectRuleBasedCandidates({
3568
3967
  statement,
@@ -3584,6 +3983,14 @@ function classifyFileMisplacementRisks(params) {
3584
3983
  }
3585
3984
  const best = selectHighestPriorityRisk(candidates);
3586
3985
  if (!best) continue;
3986
+ if (params.fileType === "idempotent") {
3987
+ if (DROP_IF_EXISTS_CLEANUP.test(statement)) {
3988
+ best.level = "low";
3989
+ }
3990
+ if (hasAllowDynamicSqlAnnotation && DYNAMIC_SQL_DOWNGRADEABLE_PATTERNS.some((p) => best.message.includes(p))) {
3991
+ best.level = "low";
3992
+ }
3993
+ }
3587
3994
  const key = buildDirectoryRiskKey(best);
3588
3995
  if (seenMessages.has(key)) continue;
3589
3996
  seenMessages.add(key);
@@ -3789,7 +4196,7 @@ init_esm_shims();
3789
4196
  var riskDetectorLoader = null;
3790
4197
  function loadRiskDetectorModule() {
3791
4198
  if (!riskDetectorLoader) {
3792
- riskDetectorLoader = import('./risk-detector-S7XQF4I2.js').then((module) => ({
4199
+ riskDetectorLoader = import('./risk-detector-GDDLISVE.js').then((module) => ({
3793
4200
  detectSchemaRisks: module.detectSchemaRisks
3794
4201
  })).catch((error) => {
3795
4202
  riskDetectorLoader = null;
@@ -4388,9 +4795,10 @@ var reconcile = fromPromise(
4388
4795
  } catch {
4389
4796
  }
4390
4797
  const idempotentTables = extractTablesFromIdempotentSql(idempotentSqlDir);
4391
- if (idempotentTables.length > 0) {
4798
+ const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(idempotentSqlDir);
4799
+ if (idempotentTables.length > 0 || dynamicPatterns.length > 0) {
4392
4800
  excludeFromOrphanDetection = [
4393
- .../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables])
4801
+ .../* @__PURE__ */ new Set([...excludeFromOrphanDetection, ...idempotentTables, ...dynamicPatterns])
4394
4802
  ];
4395
4803
  }
4396
4804
  const diff = diffSchema({
@@ -4883,357 +5291,150 @@ var dbSyncMachine = setup({
4883
5291
  meta: { e2e: e2eMeta.preflight },
4884
5292
  invoke: {
4885
5293
  src: "preflight",
4886
- input: ({ context }) => ({ ctx: assertStepCtx(context) }),
4887
- onDone: [
4888
- {
4889
- guard: ({ event }) => event.output.passed === false,
4890
- target: "failed",
4891
- actions: assign({
4892
- error: ({ event }) => event.output.error ?? "Preflight failed"
4893
- })
4894
- },
4895
- {
4896
- target: "snapshot",
4897
- actions: assign({ preflightPassed: true })
4898
- }
4899
- ],
4900
- onError: {
4901
- target: "failed",
4902
- actions: assign({
4903
- error: ({ event }) => event.error instanceof Error ? event.error.message : "Preflight failed"
4904
- })
4905
- }
4906
- }
4907
- },
4908
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4909
- // Step 4: Snapshot (optional)
4910
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4911
- snapshot: {
4912
- meta: { e2e: e2eMeta.snapshot },
4913
- invoke: {
4914
- src: "snapshot",
4915
- input: ({ context }) => ({
4916
- ctx: assertStepCtx(context),
4917
- autoSnapshot: context.stepCtx?.autoSnapshot ?? false
4918
- }),
4919
- onDone: {
4920
- target: "sync",
4921
- actions: assign({
4922
- snapshotCreated: ({ event }) => event.output.created
4923
- })
4924
- },
4925
- onError: {
4926
- // Non-critical, continue to sync
4927
- target: "sync",
4928
- actions: assign({ snapshotCreated: false })
4929
- }
4930
- }
4931
- },
4932
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4933
- // Step 5: Sync Schema
4934
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4935
- sync: {
4936
- meta: { e2e: e2eMeta.sync },
4937
- invoke: {
4938
- src: "syncSchema",
4939
- input: ({ context }) => ({ ctx: assertStepCtx(context) }),
4940
- onDone: [
4941
- {
4942
- guard: ({ event }) => event.output.applied === false && event.output.error != null,
4943
- target: "report",
4944
- actions: assign({
4945
- applied: false,
4946
- applyCommitted: ({ event }) => event.output.applyCommitted ?? false,
4947
- stepsCompleted: ({ event }) => event.output.stepsCompleted,
4948
- error: ({ event }) => event.output.error ?? "Sync failed"
4949
- })
4950
- },
4951
- {
4952
- target: "report",
4953
- actions: assign({
4954
- applied: ({ event }) => event.output.applied,
4955
- applyCommitted: ({ event }) => event.output.applyCommitted ?? false,
4956
- stepsCompleted: ({ event }) => event.output.stepsCompleted
4957
- })
4958
- }
4959
- ],
4960
- onError: {
4961
- target: "report",
4962
- actions: assign({
4963
- applied: false,
4964
- applyCommitted: false,
4965
- error: ({ event }) => event.error instanceof Error ? event.error.message : "Sync failed"
4966
- })
4967
- }
4968
- }
4969
- },
4970
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4971
- // Step 6: Write Report
4972
- // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4973
- report: {
4974
- meta: { e2e: e2eMeta.report },
4975
- invoke: {
4976
- src: "writeReport",
4977
- input: ({ context }) => ({
4978
- ctx: assertStepCtx(context),
4979
- startTime: context.startTime,
4980
- stepsCompleted: context.stepsCompleted,
4981
- success: context.error == null,
4982
- error: context.error ?? void 0
4983
- }),
5294
+ input: ({ context }) => ({ ctx: assertStepCtx(context) }),
4984
5295
  onDone: [
4985
5296
  {
4986
- guard: ({ context }) => context.error != null,
5297
+ guard: ({ event }) => event.output.passed === false,
4987
5298
  target: "failed",
4988
- actions: assign({ reportWritten: true })
5299
+ actions: assign({
5300
+ error: ({ event }) => event.output.error ?? "Preflight failed"
5301
+ })
4989
5302
  },
4990
5303
  {
4991
- target: "done",
4992
- actions: assign({ reportWritten: true })
5304
+ target: "snapshot",
5305
+ actions: assign({ preflightPassed: true })
4993
5306
  }
4994
5307
  ],
4995
5308
  onError: {
4996
5309
  target: "failed",
4997
- actions: assign({ reportWritten: false })
5310
+ actions: assign({
5311
+ error: ({ event }) => event.error instanceof Error ? event.error.message : "Preflight failed"
5312
+ })
4998
5313
  }
4999
5314
  }
5000
5315
  },
5001
5316
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5002
- // Final States
5317
+ // Step 4: Snapshot (optional)
5003
5318
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5004
- done: {
5005
- meta: { e2e: e2eMeta.done },
5006
- type: "final"
5007
- },
5008
- failed: {
5009
- meta: { e2e: e2eMeta.failed },
5010
- type: "final"
5011
- }
5012
- },
5013
- output: ({ context }) => buildMachineOutput(context)
5014
- });
5015
- function getDbSyncStateName(snapshot2) {
5016
- return snapshot2.value;
5017
- }
5018
- function isDbSyncComplete(snapshot2) {
5019
- return snapshot2.status === "done";
5020
- }
5021
- function getDbSyncError(snapshot2) {
5022
- return snapshot2.context.error;
5023
- }
5024
-
5025
- // src/commands/db/sync/guardrail-orchestrator.ts
5026
- init_esm_shims();
5027
-
5028
- // src/commands/db/sync/schema-guardrail.ts
5029
- init_esm_shims();
5030
-
5031
- // src/commands/db/sync/schema-guardrail-config.ts
5032
- init_esm_shims();
5033
-
5034
- // src/commands/db/sync/schema-guardrail-config-test-support.ts
5035
- init_esm_shims();
5036
- function extractFirstStringMatch(content, fieldName) {
5037
- const match = content.match(new RegExp(`${fieldName}\\s*:\\s*['"]([^'"]+)['"]`));
5038
- return match?.[1];
5039
- }
5040
- function extractFirstNumberMatch(content, fieldName) {
5041
- const match = content.match(new RegExp(`${fieldName}\\s*:\\s*(-?\\d+(?:\\.\\d+)?)`));
5042
- if (!match?.[1]) {
5043
- return void 0;
5044
- }
5045
- const parsed = Number(match[1]);
5046
- return Number.isFinite(parsed) ? parsed : void 0;
5047
- }
5048
- function extractStringArrayMatch(content, fieldName) {
5049
- const match = content.match(new RegExp(`${fieldName}\\s*:\\s*\\[([\\s\\S]*?)\\]`));
5050
- if (!match?.[1]) {
5051
- return [];
5052
- }
5053
- return Array.from(match[1].matchAll(/['"]([^'"]+)['"]/g), (entry) => entry[1] ?? "").filter(
5054
- (value) => value.length > 0
5055
- );
5056
- }
5057
- function extractNamedObjectBlock(content, fieldName) {
5058
- const nameMatch = new RegExp(`${fieldName}\\s*:\\s*\\{`).exec(content);
5059
- if (!nameMatch) {
5060
- return null;
5061
- }
5062
- const startIndex = nameMatch.index + nameMatch[0].length - 1;
5063
- let depth = 0;
5064
- for (let index = startIndex; index < content.length; index += 1) {
5065
- const current = content[index];
5066
- if (current === "{") {
5067
- depth += 1;
5068
- } else if (current === "}") {
5069
- depth -= 1;
5070
- if (depth === 0) {
5071
- return content.slice(startIndex + 1, index);
5072
- }
5073
- }
5074
- }
5075
- return null;
5076
- }
5077
- function extractAllowedDuplicateFunctions(content, normalizers) {
5078
- const match = content.match(/allowedDuplicateFunctions\s*:\s*\[([\s\S]*?)\]/);
5079
- if (!match?.[1]) {
5080
- return [];
5081
- }
5082
- const entries = [];
5083
- for (const objectMatch of match[1].matchAll(/\{([\s\S]*?)\}/g)) {
5084
- const objectBody = objectMatch[1] ?? "";
5085
- const qualifiedName = extractFirstStringMatch(objectBody, "qualifiedName");
5086
- const signature = extractFirstStringMatch(objectBody, "signature");
5087
- const reason = extractFirstStringMatch(objectBody, "reason");
5088
- if (!qualifiedName || !signature || !reason) {
5089
- continue;
5090
- }
5091
- entries.push({
5092
- qualifiedName: normalizers.normalizeFunctionQualifiedName(qualifiedName),
5093
- signature: normalizers.normalizeAllowlistSignature(signature),
5094
- reason,
5095
- declarativeFile: extractFirstStringMatch(objectBody, "declarativeFile"),
5096
- idempotentFile: extractFirstStringMatch(objectBody, "idempotentFile"),
5097
- expectedBodyHash: extractFirstStringMatch(objectBody, "expectedBodyHash")
5098
- });
5099
- }
5100
- return entries;
5101
- }
5102
- function tryLoadSchemaGuardrailConfigFromText(params) {
5103
- const configPath = findRunaConfig(params.targetDir);
5104
- if (!configPath || !existsSync(configPath)) {
5105
- return null;
5106
- }
5107
- try {
5108
- const content = readFileSync(configPath, "utf-8");
5109
- const schemaGuardrailsBlock = extractNamedObjectBlock(content, "schemaGuardrails") ?? "";
5110
- const pgSchemaDiffBlock = extractNamedObjectBlock(content, "pgSchemaDiff") ?? "";
5111
- return {
5112
- ...params.defaults,
5113
- declarativeSqlDir: extractFirstStringMatch(schemaGuardrailsBlock, "declarativeSqlDir") ?? params.defaults.declarativeSqlDir,
5114
- allowedDuplicateFunctions: extractAllowedDuplicateFunctions(
5115
- schemaGuardrailsBlock,
5116
- params.normalizers
5117
- ),
5118
- generatedHeaderRewriteTargets: params.normalizers.normalizeFileList(
5119
- extractStringArrayMatch(schemaGuardrailsBlock, "generatedHeaderRewriteTargets").map(
5120
- (value) => params.normalizers.normalizePathForMatch(value)
5121
- )
5122
- ),
5123
- semanticWarnings: {
5124
- threshold: extractFirstNumberMatch(schemaGuardrailsBlock, "threshold") ?? params.defaults.semanticWarnings.threshold,
5125
- maxCandidates: extractFirstNumberMatch(schemaGuardrailsBlock, "maxCandidates") ?? params.defaults.semanticWarnings.maxCandidates,
5126
- ignorePairs: new Set(
5127
- extractStringArrayMatch(schemaGuardrailsBlock, "ignorePairs").map(
5128
- (value) => params.normalizers.normalizeSuppressionPair(value)
5129
- )
5130
- )
5131
- },
5132
- tableHeaderMaxWidth: extractFirstNumberMatch(schemaGuardrailsBlock, "tableHeaderMaxWidth") ?? params.defaults.tableHeaderMaxWidth,
5133
- excludeFromOrphanDetection: extractStringArrayMatch(
5134
- pgSchemaDiffBlock,
5135
- "excludeFromOrphanDetection"
5136
- ),
5137
- idempotentSqlDir: extractFirstStringMatch(pgSchemaDiffBlock, "idempotentSqlDir") ?? params.defaults.idempotentSqlDir
5138
- };
5139
- } catch {
5140
- return null;
5141
- }
5142
- }
5143
-
5144
- // src/commands/db/sync/schema-guardrail-config.ts
5145
- var DEFAULT_TABLE_HEADER_MAX_WIDTH = 160;
5146
- var DEFAULT_SEMANTIC_WARNING_THRESHOLD = 0.55;
5147
- var DEFAULT_SEMANTIC_WARNING_MAX_CANDIDATES = 3;
5148
- var GENERIC_SIMILARITY_COLUMNS = /* @__PURE__ */ new Set([
5149
- "id",
5150
- "created_at",
5151
- "updated_at",
5152
- "deleted_at",
5153
- "scope_id"
5154
- ]);
5155
- function normalizePathForMatch(filePath) {
5156
- return filePath.replaceAll("\\", "/");
5157
- }
5158
- function normalizeFunctionQualifiedName(value) {
5159
- return value.trim().toLowerCase();
5160
- }
5161
- function normalizeAllowlistSignature(value) {
5162
- return value.replace(/\s+/g, " ").trim();
5163
- }
5164
- function normalizeSuppressionPair(value) {
5165
- return value.split("::").map((part) => part.trim().toLowerCase()).filter((part) => part.length > 0).sort((left, right) => left.localeCompare(right)).join("::");
5166
- }
5167
- function normalizeFileList(files) {
5168
- return [...new Set(files)].sort((a, b) => a.localeCompare(b));
5169
- }
5170
- function isSchemaGuardrailTextFallbackAllowed() {
5171
- return Boolean(process.env.VITEST);
5172
- }
5173
- function createDefaultSchemaGuardrailConfig() {
5174
- return {
5175
- declarativeSqlDir: "supabase/schemas/declarative",
5176
- allowedDuplicateFunctions: [],
5177
- generatedHeaderRewriteTargets: [],
5178
- semanticWarnings: {
5179
- threshold: DEFAULT_SEMANTIC_WARNING_THRESHOLD,
5180
- maxCandidates: DEFAULT_SEMANTIC_WARNING_MAX_CANDIDATES,
5181
- ignorePairs: /* @__PURE__ */ new Set()
5182
- },
5183
- tableHeaderMaxWidth: DEFAULT_TABLE_HEADER_MAX_WIDTH,
5184
- excludeFromOrphanDetection: [],
5185
- idempotentSqlDir: "supabase/schemas/idempotent"
5186
- };
5187
- }
5188
- function loadSchemaGuardrailConfig(targetDir) {
5189
- const defaults = createDefaultSchemaGuardrailConfig();
5190
- try {
5191
- const config = loadRunaConfig(targetDir);
5192
- const databaseConfig = config.database ?? {};
5193
- return {
5194
- 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
- })) ?? [],
5202
- generatedHeaderRewriteTargets: normalizeFileList(
5203
- (databaseConfig.schemaGuardrails?.generatedHeaderRewriteTargets ?? []).map(
5204
- (value) => normalizePathForMatch(value)
5205
- )
5206
- ),
5207
- semanticWarnings: {
5208
- threshold: databaseConfig.schemaGuardrails?.semanticWarnings?.threshold ?? defaults.semanticWarnings.threshold,
5209
- maxCandidates: databaseConfig.schemaGuardrails?.semanticWarnings?.maxCandidates ?? defaults.semanticWarnings.maxCandidates,
5210
- ignorePairs: new Set(
5211
- (databaseConfig.schemaGuardrails?.semanticWarnings?.ignorePairs ?? []).map(
5212
- (value) => normalizeSuppressionPair(value)
5213
- )
5214
- )
5215
- },
5216
- tableHeaderMaxWidth: databaseConfig.schemaGuardrails?.tableHeaderMaxWidth ?? defaults.tableHeaderMaxWidth,
5217
- excludeFromOrphanDetection: databaseConfig.pgSchemaDiff?.excludeFromOrphanDetection ?? [],
5218
- idempotentSqlDir: databaseConfig.pgSchemaDiff?.idempotentSqlDir ?? defaults.idempotentSqlDir
5219
- };
5220
- } catch (error) {
5221
- if (isSchemaGuardrailTextFallbackAllowed()) {
5222
- return tryLoadSchemaGuardrailConfigFromText({
5223
- targetDir,
5224
- defaults,
5225
- normalizers: {
5226
- normalizeAllowlistSignature,
5227
- normalizeFileList,
5228
- normalizeFunctionQualifiedName,
5229
- normalizePathForMatch,
5230
- normalizeSuppressionPair
5319
+ snapshot: {
5320
+ meta: { e2e: e2eMeta.snapshot },
5321
+ invoke: {
5322
+ src: "snapshot",
5323
+ input: ({ context }) => ({
5324
+ ctx: assertStepCtx(context),
5325
+ autoSnapshot: context.stepCtx?.autoSnapshot ?? false
5326
+ }),
5327
+ onDone: {
5328
+ target: "sync",
5329
+ actions: assign({
5330
+ snapshotCreated: ({ event }) => event.output.created
5331
+ })
5332
+ },
5333
+ onError: {
5334
+ // Non-critical, continue to sync
5335
+ target: "sync",
5336
+ actions: assign({ snapshotCreated: false })
5231
5337
  }
5232
- }) ?? defaults;
5338
+ }
5339
+ },
5340
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5341
+ // Step 5: Sync Schema
5342
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5343
+ sync: {
5344
+ meta: { e2e: e2eMeta.sync },
5345
+ invoke: {
5346
+ src: "syncSchema",
5347
+ input: ({ context }) => ({ ctx: assertStepCtx(context) }),
5348
+ onDone: [
5349
+ {
5350
+ guard: ({ event }) => event.output.applied === false && event.output.error != null,
5351
+ target: "report",
5352
+ actions: assign({
5353
+ applied: false,
5354
+ applyCommitted: ({ event }) => event.output.applyCommitted ?? false,
5355
+ stepsCompleted: ({ event }) => event.output.stepsCompleted,
5356
+ error: ({ event }) => event.output.error ?? "Sync failed"
5357
+ })
5358
+ },
5359
+ {
5360
+ target: "report",
5361
+ actions: assign({
5362
+ applied: ({ event }) => event.output.applied,
5363
+ applyCommitted: ({ event }) => event.output.applyCommitted ?? false,
5364
+ stepsCompleted: ({ event }) => event.output.stepsCompleted
5365
+ })
5366
+ }
5367
+ ],
5368
+ onError: {
5369
+ target: "report",
5370
+ actions: assign({
5371
+ applied: false,
5372
+ applyCommitted: false,
5373
+ error: ({ event }) => event.error instanceof Error ? event.error.message : "Sync failed"
5374
+ })
5375
+ }
5376
+ }
5377
+ },
5378
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5379
+ // Step 6: Write Report
5380
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5381
+ report: {
5382
+ meta: { e2e: e2eMeta.report },
5383
+ invoke: {
5384
+ src: "writeReport",
5385
+ input: ({ context }) => ({
5386
+ ctx: assertStepCtx(context),
5387
+ startTime: context.startTime,
5388
+ stepsCompleted: context.stepsCompleted,
5389
+ success: context.error == null,
5390
+ error: context.error ?? void 0
5391
+ }),
5392
+ onDone: [
5393
+ {
5394
+ guard: ({ context }) => context.error != null,
5395
+ target: "failed",
5396
+ actions: assign({ reportWritten: true })
5397
+ },
5398
+ {
5399
+ target: "done",
5400
+ actions: assign({ reportWritten: true })
5401
+ }
5402
+ ],
5403
+ onError: {
5404
+ target: "failed",
5405
+ actions: assign({ reportWritten: false })
5406
+ }
5407
+ }
5408
+ },
5409
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5410
+ // Final States
5411
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5412
+ done: {
5413
+ meta: { e2e: e2eMeta.done },
5414
+ type: "final"
5415
+ },
5416
+ failed: {
5417
+ meta: { e2e: e2eMeta.failed },
5418
+ type: "final"
5233
5419
  }
5234
- throw error;
5235
- }
5420
+ },
5421
+ output: ({ context }) => buildMachineOutput(context)
5422
+ });
5423
+ function getDbSyncStateName(snapshot2) {
5424
+ return snapshot2.value;
5425
+ }
5426
+ function isDbSyncComplete(snapshot2) {
5427
+ return snapshot2.status === "done";
5236
5428
  }
5429
+ function getDbSyncError(snapshot2) {
5430
+ return snapshot2.context.error;
5431
+ }
5432
+
5433
+ // src/commands/db/sync/guardrail-orchestrator.ts
5434
+ init_esm_shims();
5435
+
5436
+ // src/commands/db/sync/schema-guardrail.ts
5437
+ init_esm_shims();
5237
5438
 
5238
5439
  // src/commands/db/sync/schema-guardrail-graph.ts
5239
5440
  init_esm_shims();
@@ -5266,8 +5467,13 @@ function loadExtraTableFilters(targetDir) {
5266
5467
  const idempotentManagedTables = new Set(
5267
5468
  extractTablesFromIdempotentSql(idempotentDirectory, targetDir)
5268
5469
  );
5470
+ const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(
5471
+ idempotentDirectory,
5472
+ targetDir
5473
+ );
5474
+ const allExclusions = [...config.excludeFromOrphanDetection, ...dynamicPatterns];
5269
5475
  return {
5270
- exclusionMatcher: buildTablePatternMatcher(config.excludeFromOrphanDetection),
5476
+ exclusionMatcher: buildTablePatternMatcher(allExclusions),
5271
5477
  idempotentManagedTables
5272
5478
  };
5273
5479
  }
@@ -5480,24 +5686,36 @@ function collectExecuteOccurrencesFromBody(body, startLine) {
5480
5686
  }
5481
5687
  return occurrences.sort((left, right) => left.line - right.line);
5482
5688
  }
5689
+ var PARTITION_INFRA_PATTERNS = [
5690
+ /\bPARTITION\b/i,
5691
+ /\bATTACH\b/i,
5692
+ /\bDETACH\b/i,
5693
+ /\bCREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\b/i,
5694
+ /\bdefault\s*partition\b/i,
5695
+ /\brange_partition\b/i
5696
+ ];
5697
+ function isPartitionInfrastructure(body) {
5698
+ return PARTITION_INFRA_PATTERNS.some((pattern) => pattern.test(body));
5699
+ }
5483
5700
  function createDynamicSqlBlockersForStatement(params) {
5484
5701
  const body = extractFirstDollarBody(params.statement, params.line);
5485
5702
  if (!body) {
5486
5703
  return [];
5487
5704
  }
5488
5705
  const executeOccurrences = collectExecuteOccurrencesFromBody(body.body, body.startLine);
5706
+ const isInfra = isPartitionInfrastructure(body.body);
5489
5707
  return executeOccurrences.map((occurrence) => ({
5490
- kind: "dynamic-sql",
5708
+ kind: isInfra ? "dynamic-sql-infra" : "dynamic-sql",
5491
5709
  sourceFile: params.sourceFile,
5492
5710
  line: occurrence.line,
5493
5711
  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."
5712
+ 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
5713
  }));
5496
5714
  }
5497
5715
  function buildDynamicSqlBlockers(params) {
5498
5716
  const blockers = [];
5499
- const allFiles = [...params.sources.declarativeFiles, ...params.sources.idempotentFiles];
5500
- for (const file of allFiles) {
5717
+ for (const file of params.sources.declarativeFiles) {
5718
+ if (ALLOW_DYNAMIC_SQL_ANNOTATION.test(file.content)) continue;
5501
5719
  for (const parsed of splitSqlStatements(file.content)) {
5502
5720
  blockers.push(
5503
5721
  ...createDynamicSqlBlockersForStatement({
@@ -5508,6 +5726,21 @@ function buildDynamicSqlBlockers(params) {
5508
5726
  );
5509
5727
  }
5510
5728
  }
5729
+ for (const file of params.sources.idempotentFiles) {
5730
+ if (ALLOW_DYNAMIC_SQL_ANNOTATION.test(file.content)) continue;
5731
+ for (const parsed of splitSqlStatements(file.content)) {
5732
+ const stmtBlockers = createDynamicSqlBlockersForStatement({
5733
+ sourceFile: file.relativePath,
5734
+ statement: parsed.statement,
5735
+ line: parsed.line
5736
+ });
5737
+ for (const blocker of stmtBlockers) {
5738
+ blocker.kind = "dynamic-sql-infra";
5739
+ 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)`;
5740
+ }
5741
+ blockers.push(...stmtBlockers);
5742
+ }
5743
+ }
5511
5744
  return stableSorted2(blockers, (value) => `${value.sourceFile}:${value.line ?? 0}:${value.kind}`);
5512
5745
  }
5513
5746
  function buildLocalBlindSpotBlockers(params) {
@@ -5522,7 +5755,7 @@ function buildLocalBlindSpotBlockers(params) {
5522
5755
  }),
5523
5756
  ...buildExtensionPlacementBlockers({
5524
5757
  sources: params.sources,
5525
- requiredFile: path12.posix.join("supabase", "schemas", "idempotent", "00_extensions.sql")
5758
+ requiredFile: detectExtensionFilePath()
5526
5759
  })
5527
5760
  ];
5528
5761
  return stableSorted2(
@@ -5805,37 +6038,6 @@ function buildFunctionBodyHashMap(files, layer) {
5805
6038
  }
5806
6039
  return hashes;
5807
6040
  }
5808
- function isAllowlistedDuplicateFunction(params) {
5809
- const { finding, allowlist, bodyHashes } = params;
5810
- if (!finding.signature) return false;
5811
- return allowlist.some((entry) => {
5812
- if (normalizeFunctionQualifiedName(entry.qualifiedName) !== normalizeFunctionQualifiedName(finding.qualifiedName)) {
5813
- return false;
5814
- }
5815
- if (normalizeAllowlistSignature(entry.signature) !== normalizeAllowlistSignature(finding.signature ?? "")) {
5816
- return false;
5817
- }
5818
- if (entry.declarativeFile && !finding.declarativeDefinitions.some(
5819
- (definition) => normalizePathForMatch(definition.file) === entry.declarativeFile
5820
- )) {
5821
- return false;
5822
- }
5823
- if (entry.idempotentFile && !finding.idempotentDefinitions.some(
5824
- (definition) => normalizePathForMatch(definition.file) === entry.idempotentFile
5825
- )) {
5826
- return false;
5827
- }
5828
- if (!entry.expectedBodyHash) {
5829
- return true;
5830
- }
5831
- const definitions = [...finding.declarativeDefinitions, ...finding.idempotentDefinitions];
5832
- if (definitions.length === 0) return false;
5833
- return definitions.every((definition) => {
5834
- const key = `${definition.layer}:${definition.file}:${definition.line}`;
5835
- return bodyHashes.get(key) === entry.expectedBodyHash;
5836
- });
5837
- });
5838
- }
5839
6041
  function normalizeQualifiedObjectRef(schema, name, isFunctionCall) {
5840
6042
  return `${schema}.${name}${isFunctionCall ? "()" : ""}`;
5841
6043
  }
@@ -5917,6 +6119,19 @@ function buildManagedBoundaryMetadataByFile(files) {
5917
6119
  }
5918
6120
 
5919
6121
  // src/commands/db/sync/schema-guardrail-graph-nodes.ts
6122
+ function extractTriggerFunctionArgs(statement) {
6123
+ const execMatch = statement.match(
6124
+ /\bEXECUTE\s+(?:FUNCTION|PROCEDURE)\s+(?:(?:"[^"]+"|[A-Za-z_]\w*)\s*\.\s*)?(?:"[^"]+"|[A-Za-z_]\w*)\s*\(([^)]*)\)/i
6125
+ );
6126
+ if (!execMatch?.[1]) return [];
6127
+ const argsText = execMatch[1].trim();
6128
+ if (!argsText) return [];
6129
+ return argsText.split(",").map((arg) => arg.trim()).map((arg) => {
6130
+ const stringMatch = arg.match(/^'([^']*)'$/);
6131
+ if (stringMatch) return stringMatch[1];
6132
+ return null;
6133
+ }).filter((arg) => arg !== null);
6134
+ }
5920
6135
  function parseCreateTriggerStatement(statement) {
5921
6136
  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
6137
  const match = statement.match(triggerRegex);
@@ -5933,13 +6148,15 @@ function parseCreateTriggerStatement(statement) {
5933
6148
  const schema = (match[5] ?? match[6] ?? "public").toLowerCase();
5934
6149
  const functionSchema = (match[9] ?? match[10] ?? "").toLowerCase();
5935
6150
  const functionName = (match[11] ?? match[12] ?? "").toLowerCase();
6151
+ const functionArgs = extractTriggerFunctionArgs(statement);
5936
6152
  return {
5937
6153
  qualifiedTable: `${schema}.${table}`,
5938
6154
  trigger: {
5939
6155
  name: triggerName,
5940
6156
  timing,
5941
6157
  event,
5942
- functionName: functionName ? functionSchema ? `${functionSchema}.${functionName}` : functionName : void 0
6158
+ functionName: functionName ? functionSchema ? `${functionSchema}.${functionName}` : functionName : void 0,
6159
+ functionArgs: functionArgs.length > 0 ? functionArgs : void 0
5943
6160
  }
5944
6161
  };
5945
6162
  }
@@ -6902,6 +7119,114 @@ function buildBoundaryGuidanceWarnings(params) {
6902
7119
  (value) => `${value.sourceFile}.${value.kind}.${value.suggestedDeclarativeFile ?? ""}.${value.suggestedIdempotentFile ?? ""}.${value.target}`
6903
7120
  );
6904
7121
  }
7122
+ function extractCaseWhenBranches(functionBody) {
7123
+ const branches = [];
7124
+ const whenPattern = /\bWHEN\s+'([^']+)'/gi;
7125
+ for (const match of functionBody.matchAll(whenPattern)) {
7126
+ if (match[1]) branches.push(match[1].toLowerCase());
7127
+ }
7128
+ return [...new Set(branches)];
7129
+ }
7130
+ function escapeForRegex(value) {
7131
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7132
+ }
7133
+ function findFunctionBody(qualifiedName, sources) {
7134
+ const rawName = qualifiedName.includes(".") ? qualifiedName.split(".")[1] : qualifiedName;
7135
+ if (!rawName) return null;
7136
+ const escaped = escapeForRegex(rawName);
7137
+ const pattern = new RegExp(
7138
+ `CREATE\\s+(?:OR\\s+REPLACE\\s+)?FUNCTION\\s+(?:(?:"[^"]+"|\\w+)\\.)?(?:"?${escaped}"?)\\s*\\(`,
7139
+ "i"
7140
+ );
7141
+ for (const file of [...sources.declarativeFiles, ...sources.idempotentFiles]) {
7142
+ if (!pattern.test(file.content)) continue;
7143
+ const bodyMatch = file.content.match(
7144
+ new RegExp(
7145
+ `CREATE\\s+(?:OR\\s+REPLACE\\s+)?FUNCTION\\s+(?:(?:"[^"]+"|\\w+)\\.)?(?:"?${escaped}"?)\\s*\\([^)]*\\)[\\s\\S]*?\\$\\w*\\$([\\s\\S]*?)\\$\\w*\\$`,
7146
+ "i"
7147
+ )
7148
+ );
7149
+ if (bodyMatch?.[1]) return bodyMatch[1];
7150
+ }
7151
+ return null;
7152
+ }
7153
+ function buildTriggerDispatchGapWarnings(params) {
7154
+ const argsByFunction = collectTriggerArgsByFunction(params.tableNodes);
7155
+ return validateDispatchCoverage(argsByFunction, params.sources);
7156
+ }
7157
+ function collectTriggerArgsByFunction(tableNodes) {
7158
+ const argsByFunction = /* @__PURE__ */ new Map();
7159
+ for (const [, tableNode] of tableNodes) {
7160
+ for (const trigger of tableNode.triggers) {
7161
+ if (!trigger.functionName || !trigger.functionArgs?.length) continue;
7162
+ for (const arg of trigger.functionArgs) {
7163
+ const entries = argsByFunction.get(trigger.functionName) ?? [];
7164
+ entries.push({
7165
+ arg: arg.toLowerCase(),
7166
+ triggerName: trigger.name,
7167
+ table: tableNode.qualifiedName
7168
+ });
7169
+ argsByFunction.set(trigger.functionName, entries);
7170
+ }
7171
+ }
7172
+ }
7173
+ return argsByFunction;
7174
+ }
7175
+ function validateDispatchCoverage(argsByFunction, sources) {
7176
+ const warnings = [];
7177
+ for (const [functionName, triggerArgs] of argsByFunction) {
7178
+ const body = findFunctionBody(functionName, sources);
7179
+ if (!body || !/\bCASE\b/i.test(body)) continue;
7180
+ const coveredBranches = extractCaseWhenBranches(body);
7181
+ if (coveredBranches.length === 0) continue;
7182
+ for (const { arg, triggerName, table } of triggerArgs) {
7183
+ if (!coveredBranches.includes(arg)) {
7184
+ warnings.push({
7185
+ sourceFile: table,
7186
+ kind: "trigger_dispatch_gap",
7187
+ target: `${functionName}('${arg}')`,
7188
+ 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.`
7189
+ });
7190
+ }
7191
+ }
7192
+ }
7193
+ return stableSorted4(warnings, (w) => `${w.sourceFile}:${w.target}`);
7194
+ }
7195
+ 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;
7196
+ function extractIndexNames(content) {
7197
+ const names = /* @__PURE__ */ new Set();
7198
+ CREATE_INDEX_PATTERN.lastIndex = 0;
7199
+ for (const match of content.matchAll(CREATE_INDEX_PATTERN)) {
7200
+ const name = (match[1] ?? match[2] ?? "").toLowerCase();
7201
+ if (name) names.add(name);
7202
+ }
7203
+ return names;
7204
+ }
7205
+ function buildCrossLayerDuplicateIndexWarnings(sources) {
7206
+ const declarativeIndexes = /* @__PURE__ */ new Map();
7207
+ for (const file of sources.declarativeFiles) {
7208
+ for (const name of extractIndexNames(file.content)) {
7209
+ declarativeIndexes.set(name, file.relativePath);
7210
+ }
7211
+ }
7212
+ const warnings = [];
7213
+ for (const file of sources.idempotentFiles) {
7214
+ for (const name of extractIndexNames(file.content)) {
7215
+ const declarativeFile = declarativeIndexes.get(name);
7216
+ if (declarativeFile) {
7217
+ warnings.push({
7218
+ sourceFile: file.relativePath,
7219
+ kind: "trigger_function",
7220
+ // reuse existing kind for index cross-layer
7221
+ target: name,
7222
+ suggestedDeclarativeFile: declarativeFile,
7223
+ 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.`
7224
+ });
7225
+ }
7226
+ }
7227
+ }
7228
+ return stableSorted4(warnings, (w) => `${w.sourceFile}:${w.target}`);
7229
+ }
6905
7230
 
6906
7231
  // src/commands/db/sync/schema-guardrail-graph.ts
6907
7232
  function loadSqlSources(targetDir, config) {
@@ -7031,12 +7356,19 @@ async function buildStaticGraph(targetDir, config, sources) {
7031
7356
  runtimeTables: loadRuntimeTablesManifest(targetDir),
7032
7357
  config
7033
7358
  });
7034
- const boundaryGuidanceWarnings = buildBoundaryGuidanceWarnings({
7035
- fileNodes,
7036
- schemaNodes,
7037
- functionClaims,
7038
- ownerFileByTable
7039
- });
7359
+ const boundaryGuidanceWarnings = [
7360
+ ...buildBoundaryGuidanceWarnings({
7361
+ fileNodes,
7362
+ schemaNodes,
7363
+ functionClaims,
7364
+ ownerFileByTable
7365
+ }),
7366
+ ...buildTriggerDispatchGapWarnings({
7367
+ tableNodes: tableNodesByName,
7368
+ sources
7369
+ }),
7370
+ ...buildCrossLayerDuplicateIndexWarnings(sources)
7371
+ ];
7040
7372
  const localBlindSpotBlockers = buildLocalBlindSpotBlockers({
7041
7373
  graph,
7042
7374
  sources,
@@ -7149,7 +7481,8 @@ function createCheckModePhases(report) {
7149
7481
  phase: "compare_generated_headers",
7150
7482
  details: {
7151
7483
  file: block.file,
7152
- target: block.target
7484
+ target: block.target,
7485
+ repair: "Run `runa db sync` to auto-regenerate headers"
7153
7486
  }
7154
7487
  }))
7155
7488
  })
@@ -7231,7 +7564,7 @@ function setFailure2(report, phase, code, message) {
7231
7564
  function stableSorted5(values, map) {
7232
7565
  return [...values].sort((a, b) => map(a).localeCompare(map(b)));
7233
7566
  }
7234
- function normalizeFileList4(files) {
7567
+ function normalizeFileList3(files) {
7235
7568
  return [...new Set(files)].sort((a, b) => a.localeCompare(b));
7236
7569
  }
7237
7570
  function renderList(values) {
@@ -7716,7 +8049,7 @@ function rewriteManagedHeaders(params) {
7716
8049
  )
7717
8050
  };
7718
8051
  }
7719
- params.report.headersRewritten = normalizeFileList4(params.report.headersRewritten);
8052
+ params.report.headersRewritten = normalizeFileList3(params.report.headersRewritten);
7720
8053
  params.report.rewritesRetainedOnDisk = params.report.headersRewritten.length > 0;
7721
8054
  params.report.staleBlocks = [];
7722
8055
  return null;
@@ -8073,7 +8406,7 @@ async function runProductionDdlOrderCheck(params) {
8073
8406
  }
8074
8407
  params.logger.info("\u{1F50D} Checking production DDL ordering...");
8075
8408
  try {
8076
- const { executePgSchemaDiffPlan } = await import('./pg-schema-diff-helpers-7377FS2D.js');
8409
+ const { executePgSchemaDiffPlan } = await import('./pg-schema-diff-helpers-JZO4GAQG.js');
8077
8410
  const { planOutput } = executePgSchemaDiffPlan(
8078
8411
  productionUrl,
8079
8412
  schemasDir,
@@ -11416,6 +11749,62 @@ function formatImportImpactReport(report, changedSymbols) {
11416
11749
  // src/commands/db/commands/db-sync/production-precheck.ts
11417
11750
  init_esm_shims();
11418
11751
 
11752
+ // src/commands/db/utils/changed-files-detector.ts
11753
+ init_esm_shims();
11754
+ function detectDefaultBranch() {
11755
+ const result = spawnSync("git", ["rev-parse", "--verify", "main"], {
11756
+ timeout: 5e3,
11757
+ encoding: "utf-8"
11758
+ });
11759
+ return result.status === 0 ? "main" : "master";
11760
+ }
11761
+ function getChangedSqlFiles() {
11762
+ try {
11763
+ const defaultBranch = detectDefaultBranch();
11764
+ const mergeBase = spawnSync("git", ["merge-base", "HEAD", defaultBranch], {
11765
+ timeout: 5e3,
11766
+ encoding: "utf-8"
11767
+ });
11768
+ let diffOutput;
11769
+ if (mergeBase.status === 0 && mergeBase.stdout.trim()) {
11770
+ const diff = spawnSync("git", ["diff", "--name-only", mergeBase.stdout.trim(), "HEAD"], {
11771
+ timeout: 5e3,
11772
+ encoding: "utf-8"
11773
+ });
11774
+ diffOutput = diff.stdout ?? "";
11775
+ } else {
11776
+ const diff = spawnSync("git", ["diff", "--name-only", "HEAD"], {
11777
+ timeout: 5e3,
11778
+ encoding: "utf-8"
11779
+ });
11780
+ diffOutput = diff.stdout ?? "";
11781
+ }
11782
+ const uncommitted = spawnSync("git", ["diff", "--name-only"], {
11783
+ timeout: 5e3,
11784
+ encoding: "utf-8"
11785
+ });
11786
+ const staged = spawnSync("git", ["diff", "--name-only", "--cached"], {
11787
+ timeout: 5e3,
11788
+ encoding: "utf-8"
11789
+ });
11790
+ const allChanged = [
11791
+ ...diffOutput.split("\n"),
11792
+ ...(uncommitted.stdout ?? "").split("\n"),
11793
+ ...(staged.stdout ?? "").split("\n")
11794
+ ].map((f) => f.trim()).filter((f) => f.length > 0 && f.endsWith(".sql"));
11795
+ return [...new Set(allChanged)].sort();
11796
+ } catch {
11797
+ return [];
11798
+ }
11799
+ }
11800
+ function classifyBlockerScope(blockerMessage, changedFiles) {
11801
+ const fileMatch = blockerMessage.match(
11802
+ /(supabase\/schemas\/(?:declarative|idempotent)\/[^\s:]+\.sql)/
11803
+ );
11804
+ if (!fileMatch) return "baseline";
11805
+ return changedFiles.has(fileMatch[1]) ? "current-change" : "baseline";
11806
+ }
11807
+
11419
11808
  // src/commands/db/commands/db-sync/plan-hazard-analyzer.ts
11420
11809
  init_esm_shims();
11421
11810
  function parseHazardType(hazard) {
@@ -11722,13 +12111,22 @@ async function collectLocalPrecheckBundle(strict) {
11722
12111
  const adjustedPlacementRisks = applyStrictModeToReport(placementRisks, strict);
11723
12112
  const adjustedExtensionRisks = applyStrictModeToReport(extensionRisks, strict);
11724
12113
  const duplicateOwnershipAnalysis = analyzeDuplicateFunctionOwnership(process.cwd());
11725
- const duplicateOwnershipBlockers = duplicateOwnershipAnalysis.findings.map((finding) => {
12114
+ let guardrailAllowlist = [];
12115
+ try {
12116
+ guardrailAllowlist = loadSchemaGuardrailConfig(process.cwd()).allowedDuplicateFunctions;
12117
+ } catch {
12118
+ }
12119
+ const duplicateOwnershipBlockers = filterAllowlistedDuplicateFunctions({
12120
+ findings: duplicateOwnershipAnalysis.findings,
12121
+ allowlist: guardrailAllowlist
12122
+ }).map((finding) => {
11726
12123
  const formatted = formatDuplicateFunctionOwnershipFinding(finding);
11727
12124
  return [
11728
12125
  formatted.summary,
11729
12126
  `declarative=${formatted.declarativeLocations.join(", ")}`,
11730
12127
  `idempotent=${formatted.idempotentLocations.join(", ")}`,
11731
- formatted.suggestion
12128
+ formatted.suggestion,
12129
+ "Allowlist: runa.config.ts database.schemaGuardrails.allowedDuplicateFunctions"
11732
12130
  ].join(" | ");
11733
12131
  });
11734
12132
  const hasLocalBlockers = duplicateOwnershipBlockers.length > 0 || hasReportBlockers(adjustedPlacementRisks) || hasReportBlockers(adjustedDeclarativeRisks) || hasReportBlockers(adjustedIdempotentRisks) || hasReportBlockers(adjustedExtensionRisks);
@@ -11795,32 +12193,185 @@ function printLocalFindingCompactSummary(logger4, local, topLimit) {
11795
12193
  topLimit
11796
12194
  );
11797
12195
  }
12196
+ var APPLY_BLOCKER_PATTERNS = [
12197
+ /Extension DDL/i,
12198
+ /DROP.*CONCURRENTLY/i,
12199
+ /DML.*declarative/i,
12200
+ /DO.*maintenance/i,
12201
+ /cross.*schema.*rls/i,
12202
+ /auth\.\*.*direct.*reference/i,
12203
+ /Duplicate function ownership/i
12204
+ ];
12205
+ var INFRA_DYNAMIC_SQL_INDICATOR = /idempotent file/i;
12206
+ function isApplyBlocker(message) {
12207
+ if (/EXECUTE|dynamic.*sql/i.test(message)) {
12208
+ return !INFRA_DYNAMIC_SQL_INDICATOR.test(message);
12209
+ }
12210
+ return APPLY_BLOCKER_PATTERNS.some((pattern) => pattern.test(message));
12211
+ }
12212
+ function categorizeBlockers(blockers) {
12213
+ const applyBlockers = [];
12214
+ const architectureDebt = [];
12215
+ for (const blocker of blockers) {
12216
+ if (isApplyBlocker(blocker)) {
12217
+ applyBlockers.push(blocker);
12218
+ } else {
12219
+ architectureDebt.push(blocker);
12220
+ }
12221
+ }
12222
+ return { applyBlockers, architectureDebt };
12223
+ }
11798
12224
  function printLocalFindingDetailedReport(logger4, local) {
11799
- logFindingSection(
12225
+ logLocalWarningsSections(logger4, local);
12226
+ const allBlockers = collectAllBlockers(local);
12227
+ const { applyBlockers, architectureDebt } = categorizeBlockers(allBlockers);
12228
+ const changedFiles = new Set(getChangedSqlFiles());
12229
+ if (changedFiles.size > 0) {
12230
+ logScopedBlockers(logger4, applyBlockers, architectureDebt, changedFiles);
12231
+ } else {
12232
+ logUnscopedBlockers(logger4, applyBlockers, architectureDebt);
12233
+ }
12234
+ }
12235
+ function logLocalWarningsSections(logger4, local) {
12236
+ logFindingSection(logger4, "Declarative risk warnings:", local.adjustedDeclarativeRisks.warnings);
12237
+ logFindingSection(logger4, "Idempotent risk warnings:", local.adjustedIdempotentRisks.warnings);
12238
+ logFindingSection(logger4, "Placement warnings:", local.adjustedPlacementRisks.warnings);
12239
+ logFindingSection(logger4, "Extension warnings:", local.adjustedExtensionRisks.warnings);
12240
+ }
12241
+ function collectAllBlockers(local) {
12242
+ return [
12243
+ ...local.duplicateOwnershipBlockers,
12244
+ ...local.adjustedPlacementRisks.blockers,
12245
+ ...local.adjustedDeclarativeRisks.blockers,
12246
+ ...local.adjustedIdempotentRisks.blockers,
12247
+ ...local.adjustedExtensionRisks.blockers
12248
+ ];
12249
+ }
12250
+ function logScopedBlockers(logger4, applyBlockers, architectureDebt, changedFiles) {
12251
+ const current = applyBlockers.filter(
12252
+ (b) => classifyBlockerScope(b, changedFiles) === "current-change"
12253
+ );
12254
+ const baseline = applyBlockers.filter(
12255
+ (b) => classifyBlockerScope(b, changedFiles) === "baseline"
12256
+ );
12257
+ const currentDebt = architectureDebt.filter(
12258
+ (b) => classifyBlockerScope(b, changedFiles) === "current-change"
12259
+ );
12260
+ const baselineDebt = architectureDebt.filter(
12261
+ (b) => classifyBlockerScope(b, changedFiles) === "baseline"
12262
+ );
12263
+ logBlockerGroup(
11800
12264
  logger4,
11801
- "Duplicate function ownership blockers:",
11802
- local.duplicateOwnershipBlockers
12265
+ "error",
12266
+ "[current change] Apply blockers",
12267
+ current,
12268
+ "fix before merging"
11803
12269
  );
11804
- logFindingSection(
12270
+ logBlockerGroup(logger4, "warn", "[current change] Architecture debt", currentDebt);
12271
+ logBlockerGroup(
11805
12272
  logger4,
11806
- "Risk checks on supabase/schemas/declarative/*.sql:",
11807
- local.adjustedDeclarativeRisks.warnings
12273
+ "error",
12274
+ "[repo baseline] Apply blockers",
12275
+ baseline,
12276
+ "pre-existing issues"
11808
12277
  );
11809
- logFindingSection(
12278
+ logBlockerGroup(
11810
12279
  logger4,
11811
- "Risk checks on supabase/schemas/idempotent/*.sql:",
11812
- local.adjustedIdempotentRisks.warnings
12280
+ "warn",
12281
+ "[repo baseline] Architecture debt",
12282
+ baselineDebt,
12283
+ "pre-existing"
11813
12284
  );
11814
- logFindingSection(
12285
+ logImpactSummary(logger4, {
12286
+ current: current.length + currentDebt.length,
12287
+ currentApply: current.length,
12288
+ currentDebt: currentDebt.length,
12289
+ baseline: baseline.length + baselineDebt.length,
12290
+ baselineApply: baseline.length,
12291
+ baselineDebt: baselineDebt.length,
12292
+ total: applyBlockers.length + architectureDebt.length
12293
+ });
12294
+ }
12295
+ var PRECHECK_COUNTS_PATH = join(".runa", "tmp", "precheck-counts.json");
12296
+ function loadPreviousPrecheckCounts() {
12297
+ try {
12298
+ const fullPath = join(process.cwd(), PRECHECK_COUNTS_PATH);
12299
+ if (!existsSync(fullPath)) return null;
12300
+ return JSON.parse(readFileSync(fullPath, "utf-8"));
12301
+ } catch {
12302
+ return null;
12303
+ }
12304
+ }
12305
+ function savePrecheckCounts(counts) {
12306
+ try {
12307
+ const fullPath = join(process.cwd(), PRECHECK_COUNTS_PATH);
12308
+ const dir = join(fullPath, "..");
12309
+ mkdirSync(dir, { recursive: true });
12310
+ writeFileSync(fullPath, JSON.stringify(counts, null, 2), "utf-8");
12311
+ } catch {
12312
+ }
12313
+ }
12314
+ function logImpactSummary(logger4, counts) {
12315
+ logger4.info("");
12316
+ logger4.info("Impact summary:");
12317
+ if (counts.current === 0 && counts.total > 0) {
12318
+ logger4.info(" Current change: no new issues introduced");
12319
+ } else if (counts.current > 0) {
12320
+ logger4.info(
12321
+ ` Regressions: ${counts.current} issue(s) from changed files (${counts.currentApply} apply, ${counts.currentDebt} debt)`
12322
+ );
12323
+ }
12324
+ if (counts.baseline > 0) {
12325
+ logger4.info(
12326
+ ` Baseline debt: ${counts.baseline} pre-existing issue(s) (${counts.baselineApply} apply, ${counts.baselineDebt} debt)`
12327
+ );
12328
+ }
12329
+ if (counts.total === 0) {
12330
+ logger4.info(" All clear \u2014 no blockers or debt detected");
12331
+ }
12332
+ const previous = loadPreviousPrecheckCounts();
12333
+ if (previous) {
12334
+ const delta = counts.total - previous.total;
12335
+ if (delta < 0) {
12336
+ logger4.info(` Improvement: ${Math.abs(delta)} fewer issue(s) than previous run`);
12337
+ } else if (delta > 0) {
12338
+ logger4.info(` Regression: ${delta} more issue(s) than previous run`);
12339
+ } else {
12340
+ logger4.info(" No change from previous run");
12341
+ }
12342
+ }
12343
+ savePrecheckCounts({
12344
+ total: counts.total,
12345
+ apply: counts.currentApply + counts.baselineApply,
12346
+ debt: counts.currentDebt + counts.baselineDebt,
12347
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
12348
+ });
12349
+ }
12350
+ function logUnscopedBlockers(logger4, applyBlockers, architectureDebt) {
12351
+ logBlockerGroup(
11815
12352
  logger4,
11816
- "Schema-directory placement checks:",
11817
- local.adjustedPlacementRisks.warnings
12353
+ "error",
12354
+ "Apply blockers",
12355
+ applyBlockers,
12356
+ "deployment will fail if not fixed"
11818
12357
  );
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);
12358
+ logBlockerGroup(
12359
+ logger4,
12360
+ "warn",
12361
+ "Architecture debt",
12362
+ architectureDebt,
12363
+ "works but should be addressed"
12364
+ );
12365
+ }
12366
+ function logBlockerGroup(logger4, level, title, items, subtitle) {
12367
+ if (items.length === 0) return;
12368
+ const suffix = subtitle ? ` \u2014 ${subtitle}` : "";
12369
+ const logFn = level === "error" ? logger4.error.bind(logger4) : logger4.warn.bind(logger4);
12370
+ logFn(`
12371
+ ${title} (${items.length})${suffix}:`);
12372
+ for (const item of items) {
12373
+ logger4.info(` ${item}`);
12374
+ }
11824
12375
  }
11825
12376
  function logLocalFindings(logger4, local, summary) {
11826
12377
  if (!local.hasLocalFindings) return;
@@ -12163,9 +12714,12 @@ async function runSyncAction(env, options) {
12163
12714
  const { dbTables, dbEnums } = await fetchDbTablesAndEnums(databaseUrl);
12164
12715
  const schemaDiffConfig = loadSchemaDiffConfig();
12165
12716
  const idempotentTables = extractTablesFromIdempotentSql(schemaDiffConfig.idempotentSqlDir);
12717
+ const dynamicPatterns = extractDynamicTablePatternsFromIdempotentSql(
12718
+ schemaDiffConfig.idempotentSqlDir
12719
+ );
12166
12720
  const excludeFromOrphanDetection = mergeExcludedTables(
12167
12721
  schemaDiffConfig.excludeFromOrphanDetection,
12168
- idempotentTables
12722
+ [...idempotentTables, ...dynamicPatterns]
12169
12723
  );
12170
12724
  const diff = diffSchema({
12171
12725
  expectedTables,