@runa-ai/runa-cli 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/{build-BXUJKYHC.js → build-HUDIP6KU.js} +153 -164
  2. package/dist/{cache-H63JKFYH.js → cache-N7WNPEYF.js} +2 -3
  3. package/dist/check-LOMVIRHX.js +12 -0
  4. package/dist/{chunk-HPYJPB5Y.js → chunk-2APB25TT.js} +44 -10
  5. package/dist/chunk-3WDV32GA.js +33 -0
  6. package/dist/chunk-5FT3F36G.js +59 -0
  7. package/dist/{chunk-7QV7U6NI.js → chunk-6FAU4IGR.js} +2 -1
  8. package/dist/{chunk-CE3DEYFT.js → chunk-7B5C6U2K.js} +2 -208
  9. package/dist/{chunk-GOGRLQNP.js → chunk-AFY3TX4I.js} +1 -1
  10. package/dist/{chunk-KWX3JHCY.js → chunk-AKZAN4BC.js} +6 -1
  11. package/dist/{chunk-XJBQINSA.js → chunk-CCW3PLQY.js} +2 -2
  12. package/dist/{chunk-IBVVGH6X.js → chunk-EMB6IZFT.js} +17 -4
  13. package/dist/chunk-FHG3ILE4.js +2011 -0
  14. package/dist/{chunk-22CS6EMA.js → chunk-H2AHNI75.js} +1 -1
  15. package/dist/{chunk-UU55OH7P.js → chunk-KE6QJBZG.js} +2 -3
  16. package/dist/{check-6AB5NGWK.js → chunk-QM53IQHM.js} +14 -12
  17. package/dist/{chunk-RRGQCUKT.js → chunk-WJXC4MVY.js} +30 -3
  18. package/dist/chunk-XDCHRVE3.js +215 -0
  19. package/dist/{chunk-P7U52PBY.js → chunk-Z4Z5DNW4.js} +49 -2
  20. package/dist/{ci-V3PIG2GI.js → ci-XY6IKEDC.js} +1938 -238
  21. package/dist/cli/contract-output.d.ts +1 -0
  22. package/dist/{cli-GFRZCJQR.js → cli-UZA4RBNQ.js} +216 -173
  23. package/dist/commands/build/actors/validate.d.ts +2 -0
  24. package/dist/commands/check/commands/check.d.ts +8 -3
  25. package/dist/commands/ci/machine/actors/db/collect-schema-stats.d.ts +12 -6
  26. package/dist/commands/ci/machine/actors/db/production-preview.d.ts +10 -0
  27. package/dist/commands/ci/machine/actors/db/schema-canonical-diff.d.ts +77 -0
  28. package/dist/commands/ci/machine/actors/db/schema-stats.d.ts +11 -0
  29. package/dist/commands/ci/machine/actors/db/sync-schema.d.ts +9 -1
  30. package/dist/commands/ci/machine/commands/machine-runner.d.ts +2 -0
  31. package/dist/commands/ci/machine/formatters/sections/production-schema-status.d.ts +30 -0
  32. package/dist/commands/ci/machine/formatters/sections/schema-matrix.d.ts +3 -3
  33. package/dist/commands/ci/machine/helpers.d.ts +8 -0
  34. package/dist/commands/ci/machine/machine.d.ts +57 -4
  35. package/dist/commands/ci/machine/types.d.ts +2 -0
  36. package/dist/commands/ci/utils/execa-helpers.d.ts +1 -0
  37. package/dist/commands/db/commands/db-sync/error-classifier.d.ts +9 -0
  38. package/dist/commands/dev/actors/index.d.ts +5 -0
  39. package/dist/commands/dev/actors/tables-manifest.d.ts +16 -0
  40. package/dist/commands/dev/contract.d.ts +1 -1
  41. package/dist/commands/dev/guards.d.ts +24 -0
  42. package/dist/commands/dev/machine.d.ts +22 -3
  43. package/dist/commands/dev/types.d.ts +2 -0
  44. package/dist/commands/doctor.d.ts +9 -0
  45. package/dist/commands/inject-test-attrs/defaults.d.ts +9 -0
  46. package/dist/commands/template-check/commands/template-check.d.ts +1 -0
  47. package/dist/commands/template-check/contract.d.ts +1 -0
  48. package/dist/commands/utils/machine-state-logging.d.ts +20 -0
  49. package/dist/commands/utils/repo-root.d.ts +2 -0
  50. package/dist/constants/versions.d.ts +1 -1
  51. package/dist/{db-HR7CREX2.js → db-Q3GF7JWP.js} +518 -2234
  52. package/dist/{dev-A7RW6XQV.js → dev-5YXNPTCJ.js} +168 -49
  53. package/dist/doctor-MZLOA53G.js +44 -0
  54. package/dist/{env-B47Z4747.js → env-GMB3THRG.js} +6 -7
  55. package/dist/{env-files-K2C7O7L5.js → env-files-2UIUYLLR.js} +2 -2
  56. package/dist/{error-handler-4EYSDOSE.js → error-handler-HEXBRNVV.js} +2 -2
  57. package/dist/{hotfix-CULKKMGS.js → hotfix-NDTPY2T4.js} +4 -4
  58. package/dist/index.js +4 -4
  59. package/dist/{init-ELK5QCWR.js → init-U4VCRHTD.js} +5 -6
  60. package/dist/{inject-test-attrs-Y5UD5P7Q.js → inject-test-attrs-P44BVTQS.js} +5 -18
  61. package/dist/{link-C43JRZWY.js → link-VSNDVZZD.js} +2 -3
  62. package/dist/manifest-TMFLESHW.js +19 -0
  63. package/dist/{risk-detector-BXUY2WKS.js → risk-detector-4U6ZJ2G5.js} +1 -1
  64. package/dist/{risk-detector-core-O7I7SPR7.js → risk-detector-core-TK4OAI3N.js} +2 -2
  65. package/dist/{risk-detector-plpgsql-SGMVKYJP.js → risk-detector-plpgsql-HWKS4OLR.js} +37 -7
  66. package/dist/{status-IJ4ZWHMX.js → status-UTKS63AB.js} +2 -3
  67. package/dist/{telemetry-FN7V727Y.js → telemetry-P56UBLZ2.js} +2 -3
  68. package/dist/{template-check-PNG5NQ5H.js → template-check-FFJVDLBF.js} +63 -35
  69. package/dist/{test-QYXE5UVW.js → test-V4KQL574.js} +34 -10
  70. package/dist/{test-gen-QPWOIEHU.js → test-gen-FS4CEY3P.js} +2 -3
  71. package/dist/{upgrade-3SLWVNAC.js → upgrade-7TWORWBV.js} +18 -6
  72. package/dist/{validate-SM4PXPS7.js → validate-CAAW4Y44.js} +2 -3
  73. package/dist/{vuln-check-TYQNEFS7.js → vuln-check-6CMNPSBR.js} +3 -4
  74. package/dist/{vuln-checker-2QXGN5YT.js → vuln-checker-EJJTNDNE.js} +413 -140
  75. package/dist/{watch-UCDVOQAH.js → watch-PNTKZYFB.js} +1 -1
  76. package/dist/{workflow-ZB5Q2PFY.js → workflow-H75N4BXX.js} +3 -4
  77. package/package.json +2 -2
  78. package/dist/chunk-JT5SUTWE.js +0 -9
  79. package/dist/chunk-M47WJJVS.js +0 -71
  80. package/dist/manifest-2NOQ2IMK.js +0 -32
  81. package/dist/{chunk-MNPMZERI.js → chunk-644FVGIQ.js} +1 -1
@@ -1,18 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'module';
3
- import { normalizeDatabaseUrlForDdl, enhanceConnectionError, parseBoolish, parsePostgresUrl, buildPsqlArgs, buildPsqlEnv, detectAppSchemas, formatSchemasForSql } from './chunk-CE3DEYFT.js';
4
- import { detectEnvironment } from './chunk-JMJP4A47.js';
5
- import { ensureRunaTmpDir, runLogged } from './chunk-7QV7U6NI.js';
3
+ import { normalizeDatabaseUrlForDdl, parseBoolish, enhanceConnectionError, detectAppSchemas, formatSchemasForSql } from './chunk-XDCHRVE3.js';
4
+ import { isPathContained } from './chunk-DRSUEMAK.js';
6
5
  import './chunk-QDF7QXBL.js';
7
- import { getSnapshotStateName, isSnapshotComplete } from './chunk-IBVVGH6X.js';
8
- import { getSafeEnv, getFilteredEnv, redactSecrets } from './chunk-II7VYQEM.js';
6
+ import { getSnapshotStateName, isSnapshotComplete } from './chunk-EMB6IZFT.js';
9
7
  import { writeEnvLocal, startAppBackground, waitForAppReady, executePrSetupBase, createErrorOutput } from './chunk-HD74F6W2.js';
8
+ import { parsePostgresUrl, buildPsqlArgs, buildPsqlEnv, psqlSyncQuery } from './chunk-7B5C6U2K.js';
9
+ import { ensureRunaTmpDir, runLogged } from './chunk-6FAU4IGR.js';
10
+ import { createMachineStateChangeLogger } from './chunk-5FT3F36G.js';
11
+ import { getSafeEnv, getFilteredEnv, redactSecrets } from './chunk-II7VYQEM.js';
10
12
  import { init_constants, detectSupabasePorts } from './chunk-VM3IWOT5.js';
11
- import { isPathContained } from './chunk-DRSUEMAK.js';
12
- import { emitJsonSuccess } from './chunk-UU55OH7P.js';
13
- import './chunk-RRGQCUKT.js';
14
- import './chunk-JT5SUTWE.js';
13
+ import { emitJsonSuccess } from './chunk-KE6QJBZG.js';
14
+ import './chunk-WJXC4MVY.js';
15
15
  import { setOutputFormat } from './chunk-HKUWEGUX.js';
16
+ import { detectEnvironment } from './chunk-JMJP4A47.js';
16
17
  import { init_esm_shims, __require } from './chunk-VRXHCR5K.js';
17
18
  import { Command } from 'commander';
18
19
  import { spawnSync, spawn, execFileSync } from 'child_process';
@@ -24,9 +25,9 @@ import { existsSync, readFileSync, readdirSync, promises, lstatSync, statSync }
24
25
  import { resolve4 } from 'dns/promises';
25
26
  import net, { isIP } from 'net';
26
27
  import { fromPromise, setup, assign, createActor } from 'xstate';
27
- import postgres from 'postgres';
28
+ import { randomUUID, createHash } from 'crypto';
28
29
  import { execa } from 'execa';
29
- import 'crypto';
30
+ import postgres from 'postgres';
30
31
  import { glob } from 'glob';
31
32
 
32
33
  createRequire(import.meta.url);
@@ -1883,24 +1884,41 @@ var ciProdApplyCommand = new Command("prod-apply").description("Apply production
1883
1884
 
1884
1885
  // src/commands/ci/commands/ci-static.ts
1885
1886
  init_esm_shims();
1886
- function getChecks() {
1887
- return [
1888
- {
1887
+ function hasNativeActionlint() {
1888
+ try {
1889
+ const result = spawnSync("actionlint", ["-version"], {
1890
+ encoding: "utf-8",
1891
+ stdio: ["ignore", "pipe", "pipe"],
1892
+ timeout: 5e3
1893
+ });
1894
+ return result.status === 0;
1895
+ } catch {
1896
+ return false;
1897
+ }
1898
+ }
1899
+ function createActionlintCheck(mode) {
1900
+ if (mode === "local" && hasNativeActionlint()) {
1901
+ return {
1889
1902
  name: "actionlint",
1890
1903
  label: "Workflow Lint",
1891
1904
  description: "GitHub Actions syntax validation",
1892
- command: "docker",
1893
- args: [
1894
- "run",
1895
- "--rm",
1896
- "-v",
1897
- `${process.cwd()}:/repo`,
1898
- "-w",
1899
- "/repo",
1900
- "rhysd/actionlint:1.7.9"
1901
- ],
1905
+ command: "actionlint",
1906
+ args: ["-color"],
1902
1907
  category: "workflow"
1903
- },
1908
+ };
1909
+ }
1910
+ return {
1911
+ name: "actionlint",
1912
+ label: "Workflow Lint",
1913
+ description: "GitHub Actions syntax validation",
1914
+ command: "docker",
1915
+ args: ["run", "--rm", "-v", `${process.cwd()}:/repo`, "-w", "/repo", "rhysd/actionlint:1.7.9"],
1916
+ category: "workflow"
1917
+ };
1918
+ }
1919
+ function getChecks(mode) {
1920
+ return [
1921
+ createActionlintCheck(mode),
1904
1922
  {
1905
1923
  name: "type-check",
1906
1924
  label: "Type Check",
@@ -2013,7 +2031,7 @@ function buildStepSummaryMarkdown2(summary) {
2013
2031
  return lines.join("\n");
2014
2032
  }
2015
2033
  function getChecksToRun(options) {
2016
- let checks = getChecks().filter((c) => {
2034
+ let checks = getChecks(options.mode).filter((c) => {
2017
2035
  if (c.category === "workflow") return true;
2018
2036
  if (c.category === "core") return true;
2019
2037
  if (c.category === "build") return options.includeBuild;
@@ -2498,9 +2516,434 @@ var applySeedsActor = fromPromise(
2498
2516
  // src/commands/ci/machine/actors/db/collect-schema-stats.ts
2499
2517
  init_esm_shims();
2500
2518
 
2501
- // src/commands/ci/machine/actors/db/schema-stats.ts
2519
+ // src/commands/ci/machine/actors/db/schema-canonical-diff.ts
2502
2520
  init_esm_shims();
2521
+ function createUnavailableCanonicalSnapshot(error) {
2522
+ return {
2523
+ available: false,
2524
+ objects: [],
2525
+ error
2526
+ };
2527
+ }
2503
2528
  var EXCLUDED_SCHEMAS = [
2529
+ "pg_catalog",
2530
+ "pg_toast",
2531
+ "information_schema",
2532
+ "auth",
2533
+ "storage",
2534
+ "realtime",
2535
+ "_realtime",
2536
+ "supabase_functions",
2537
+ "supabase_migrations",
2538
+ "vault",
2539
+ "pgsodium",
2540
+ "pgsodium_masks",
2541
+ "graphql",
2542
+ "graphql_public",
2543
+ "extensions",
2544
+ "net",
2545
+ "cron",
2546
+ "tests"
2547
+ ];
2548
+ function normalizeWhitespace(value) {
2549
+ return (value ?? "").replace(/\s+/g, " ").trim();
2550
+ }
2551
+ function stableHash(payload) {
2552
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex").slice(0, 16);
2553
+ }
2554
+ function createCanonicalTableColumnPayload(row) {
2555
+ return {
2556
+ name: row.column_name,
2557
+ type: normalizeWhitespace(row.data_type),
2558
+ notNull: row.not_null,
2559
+ default: normalizeWhitespace(row.default_expr),
2560
+ identity: row.identity_kind ?? "",
2561
+ generated: row.generated_kind ?? ""
2562
+ };
2563
+ }
2564
+ function sortCanonicalTableColumns(columns) {
2565
+ return [...columns].sort((left, right) => left.name.localeCompare(right.name));
2566
+ }
2567
+ function createCanonicalObject(kind, schema, name, label, payload, parentName) {
2568
+ return {
2569
+ kind,
2570
+ schema,
2571
+ name,
2572
+ key: `${kind}:${schema}:${name}`,
2573
+ label,
2574
+ parentName,
2575
+ signature: stableHash(payload)
2576
+ };
2577
+ }
2578
+ function buildSchemaListSql(schemaNames) {
2579
+ return schemaNames.map((schemaName) => `'${schemaName}'`).join(", ");
2580
+ }
2581
+ async function getUserSchemas(sql) {
2582
+ const excludeList = buildSchemaListSql(EXCLUDED_SCHEMAS);
2583
+ const rows = await sql`
2584
+ SELECT nspname
2585
+ FROM pg_namespace
2586
+ WHERE nspname NOT IN (${sql.unsafe(excludeList)})
2587
+ AND nspname NOT LIKE 'pg_%'
2588
+ ORDER BY nspname
2589
+ `;
2590
+ return rows.map((row) => row.nspname);
2591
+ }
2592
+ async function queryTableColumns(sql, schemaList) {
2593
+ return await sql`
2594
+ SELECT
2595
+ n.nspname AS schema_name,
2596
+ c.relname AS table_name,
2597
+ a.attnum::int AS attnum,
2598
+ a.attname AS column_name,
2599
+ format_type(a.atttypid, a.atttypmod) AS data_type,
2600
+ a.attnotnull AS not_null,
2601
+ pg_get_expr(ad.adbin, ad.adrelid, true) AS default_expr,
2602
+ NULLIF(a.attidentity, '') AS identity_kind,
2603
+ NULLIF(a.attgenerated, '') AS generated_kind
2604
+ FROM pg_class c
2605
+ JOIN pg_namespace n ON n.oid = c.relnamespace
2606
+ JOIN pg_attribute a ON a.attrelid = c.oid
2607
+ LEFT JOIN pg_attrdef ad ON ad.adrelid = c.oid AND ad.adnum = a.attnum
2608
+ WHERE n.nspname IN (${sql.unsafe(schemaList)})
2609
+ AND c.relkind IN ('r', 'p')
2610
+ AND a.attnum > 0
2611
+ AND NOT a.attisdropped
2612
+ ORDER BY n.nspname, c.relname, a.attnum
2613
+ `;
2614
+ }
2615
+ async function queryTableFlags(sql, schemaList) {
2616
+ return await sql`
2617
+ SELECT
2618
+ n.nspname AS schema_name,
2619
+ c.relname AS table_name,
2620
+ c.relrowsecurity AS rls_enabled,
2621
+ c.relforcerowsecurity AS rls_forced
2622
+ FROM pg_class c
2623
+ JOIN pg_namespace n ON n.oid = c.relnamespace
2624
+ WHERE n.nspname IN (${sql.unsafe(schemaList)})
2625
+ AND c.relkind IN ('r', 'p')
2626
+ `;
2627
+ }
2628
+ async function queryTableConstraints(sql, schemaList) {
2629
+ return await sql`
2630
+ SELECT
2631
+ n.nspname AS schema_name,
2632
+ c.relname AS table_name,
2633
+ con.conname AS constraint_name,
2634
+ con.contype AS constraint_type,
2635
+ pg_get_constraintdef(con.oid, true) AS definition
2636
+ FROM pg_constraint con
2637
+ JOIN pg_class c ON c.oid = con.conrelid
2638
+ JOIN pg_namespace n ON n.oid = c.relnamespace
2639
+ WHERE n.nspname IN (${sql.unsafe(schemaList)})
2640
+ AND c.relkind IN ('r', 'p')
2641
+ ORDER BY n.nspname, c.relname, con.conname
2642
+ `;
2643
+ }
2644
+ function buildTableObjects(columns, flags, constraints) {
2645
+ const tables = /* @__PURE__ */ new Map();
2646
+ for (const row of columns) {
2647
+ const key = `${row.schema_name}.${row.table_name}`;
2648
+ const entry = tables.get(key) ?? {
2649
+ schema: row.schema_name,
2650
+ table: row.table_name,
2651
+ columns: [],
2652
+ constraints: [],
2653
+ rlsEnabled: false,
2654
+ rlsForced: false
2655
+ };
2656
+ entry.columns.push(createCanonicalTableColumnPayload(row));
2657
+ tables.set(key, entry);
2658
+ }
2659
+ for (const row of flags) {
2660
+ const key = `${row.schema_name}.${row.table_name}`;
2661
+ const entry = tables.get(key) ?? {
2662
+ schema: row.schema_name,
2663
+ table: row.table_name,
2664
+ columns: [],
2665
+ constraints: [],
2666
+ rlsEnabled: false,
2667
+ rlsForced: false
2668
+ };
2669
+ entry.rlsEnabled = row.rls_enabled;
2670
+ entry.rlsForced = row.rls_forced;
2671
+ tables.set(key, entry);
2672
+ }
2673
+ for (const row of constraints) {
2674
+ const key = `${row.schema_name}.${row.table_name}`;
2675
+ const entry = tables.get(key) ?? {
2676
+ schema: row.schema_name,
2677
+ table: row.table_name,
2678
+ columns: [],
2679
+ constraints: [],
2680
+ rlsEnabled: false,
2681
+ rlsForced: false
2682
+ };
2683
+ entry.constraints.push({
2684
+ name: row.constraint_name,
2685
+ type: row.constraint_type,
2686
+ definition: normalizeWhitespace(row.definition)
2687
+ });
2688
+ tables.set(key, entry);
2689
+ }
2690
+ return Array.from(tables.values()).map(
2691
+ (table) => createCanonicalObject("table", table.schema, table.table, `${table.schema}.${table.table}`, {
2692
+ columns: sortCanonicalTableColumns(table.columns),
2693
+ constraints: table.constraints,
2694
+ rlsEnabled: table.rlsEnabled,
2695
+ rlsForced: table.rlsForced
2696
+ })
2697
+ );
2698
+ }
2699
+ async function queryFunctions(sql, schemaList) {
2700
+ return await sql`
2701
+ SELECT
2702
+ n.nspname AS schema_name,
2703
+ p.proname AS object_name,
2704
+ format('%I.%I(%s)', n.nspname, p.proname, pg_get_function_identity_arguments(p.oid)) AS object_key,
2705
+ format('%I.%I(%s)', n.nspname, p.proname, pg_get_function_identity_arguments(p.oid)) AS object_label,
2706
+ pg_get_functiondef(p.oid) AS definition
2707
+ FROM pg_proc p
2708
+ JOIN pg_namespace n ON p.pronamespace = n.oid
2709
+ WHERE n.nspname IN (${sql.unsafe(schemaList)})
2710
+ AND p.prokind = 'f'
2711
+ AND NOT EXISTS (
2712
+ SELECT 1 FROM pg_depend d
2713
+ WHERE d.objid = p.oid
2714
+ AND d.deptype = 'e'
2715
+ )
2716
+ AND p.proname NOT LIKE 'supabase_%'
2717
+ AND p.proname NOT LIKE 'postgrest_%'
2718
+ AND p.proname NOT LIKE 'pgrst_%'
2719
+ AND p.proname NOT LIKE '_pgrst_%'
2720
+ ORDER BY n.nspname, p.proname
2721
+ `;
2722
+ }
2723
+ async function queryPolicies(sql, schemaList) {
2724
+ return await sql`
2725
+ SELECT
2726
+ schemaname AS schema_name,
2727
+ policyname AS object_name,
2728
+ format('%I.%I.%I', schemaname, tablename, policyname) AS object_key,
2729
+ format('%I.%I policy %I', schemaname, tablename, policyname) AS object_label,
2730
+ tablename AS parent_name,
2731
+ jsonb_build_object(
2732
+ 'table', tablename,
2733
+ 'permissive', permissive,
2734
+ 'roles', COALESCE(roles, ARRAY[]::text[]),
2735
+ 'command', cmd,
2736
+ 'using', COALESCE(qual, ''),
2737
+ 'withCheck', COALESCE(with_check, '')
2738
+ )::text AS definition
2739
+ FROM pg_policies
2740
+ WHERE schemaname IN (${sql.unsafe(schemaList)})
2741
+ ORDER BY schemaname, tablename, policyname
2742
+ `;
2743
+ }
2744
+ async function queryIndexes(sql, schemaList) {
2745
+ return await sql`
2746
+ SELECT
2747
+ n.nspname AS schema_name,
2748
+ i.relname AS object_name,
2749
+ format('%I.%I', n.nspname, i.relname) AS object_key,
2750
+ format('%I.%I on %I', n.nspname, i.relname, t.relname) AS object_label,
2751
+ t.relname AS parent_name,
2752
+ jsonb_build_object(
2753
+ 'table', t.relname,
2754
+ 'definition',
2755
+ regexp_replace(pg_get_indexdef(i.oid), '^CREATE( UNIQUE)? INDEX [^ ]+ ON ', 'CREATE\\1 INDEX ON '),
2756
+ 'predicate',
2757
+ COALESCE(pg_get_expr(idx.indpred, idx.indrelid, true), ''),
2758
+ 'unique',
2759
+ idx.indisunique
2760
+ )::text AS definition
2761
+ FROM pg_index idx
2762
+ JOIN pg_class i ON i.oid = idx.indexrelid
2763
+ JOIN pg_class t ON t.oid = idx.indrelid
2764
+ JOIN pg_namespace n ON n.oid = i.relnamespace
2765
+ WHERE n.nspname IN (${sql.unsafe(schemaList)})
2766
+ AND NOT EXISTS (
2767
+ SELECT 1 FROM pg_constraint c
2768
+ WHERE c.conindid = idx.indexrelid
2769
+ )
2770
+ ORDER BY n.nspname, t.relname, i.relname
2771
+ `;
2772
+ }
2773
+ async function queryTriggers(sql, schemaList) {
2774
+ return await sql`
2775
+ SELECT
2776
+ n.nspname AS schema_name,
2777
+ t.tgname AS object_name,
2778
+ format('%I.%I.%I', n.nspname, c.relname, t.tgname) AS object_key,
2779
+ format('%I.%I trigger %I', n.nspname, c.relname, t.tgname) AS object_label,
2780
+ c.relname AS parent_name,
2781
+ pg_get_triggerdef(t.oid, true) AS definition
2782
+ FROM pg_trigger t
2783
+ JOIN pg_class c ON t.tgrelid = c.oid
2784
+ JOIN pg_namespace n ON c.relnamespace = n.oid
2785
+ WHERE n.nspname IN (${sql.unsafe(schemaList)})
2786
+ AND NOT t.tgisinternal
2787
+ ORDER BY n.nspname, c.relname, t.tgname
2788
+ `;
2789
+ }
2790
+ function rowsToObjects(kind, rows) {
2791
+ return rows.map(
2792
+ (row) => createCanonicalObject(
2793
+ kind,
2794
+ row.schema_name,
2795
+ row.object_key,
2796
+ row.object_label,
2797
+ normalizeWhitespace(row.definition),
2798
+ row.parent_name ?? void 0
2799
+ )
2800
+ );
2801
+ }
2802
+ async function getCanonicalSchemaSnapshot(databaseUrl) {
2803
+ let sql = null;
2804
+ try {
2805
+ const isRemoteSupabase = databaseUrl.includes(".supabase.co");
2806
+ sql = postgres(databaseUrl, {
2807
+ max: 1,
2808
+ idle_timeout: 10,
2809
+ connect_timeout: 10,
2810
+ ...isRemoteSupabase && { ssl: "require" }
2811
+ });
2812
+ const userSchemas = await getUserSchemas(sql);
2813
+ if (userSchemas.length === 0) {
2814
+ return { available: true, objects: [] };
2815
+ }
2816
+ const schemaList = buildSchemaListSql(userSchemas);
2817
+ const [columns, flags, constraints, functions, policies, indexes, triggers] = await Promise.all(
2818
+ [
2819
+ queryTableColumns(sql, schemaList),
2820
+ queryTableFlags(sql, schemaList),
2821
+ queryTableConstraints(sql, schemaList),
2822
+ queryFunctions(sql, schemaList),
2823
+ queryPolicies(sql, schemaList),
2824
+ queryIndexes(sql, schemaList),
2825
+ queryTriggers(sql, schemaList)
2826
+ ]
2827
+ );
2828
+ return {
2829
+ available: true,
2830
+ objects: [
2831
+ ...buildTableObjects(columns, flags, constraints),
2832
+ ...rowsToObjects("function", functions),
2833
+ ...rowsToObjects("policy", policies),
2834
+ ...rowsToObjects("index", indexes),
2835
+ ...rowsToObjects("trigger", triggers)
2836
+ ].sort((a, b) => a.key.localeCompare(b.key))
2837
+ };
2838
+ } catch (error) {
2839
+ const message = error instanceof Error ? error.message : String(error);
2840
+ console.warn(`[schema-canonical-diff] Could not query canonical snapshot: ${message}`);
2841
+ return createUnavailableCanonicalSnapshot(message);
2842
+ } finally {
2843
+ if (sql) {
2844
+ await sql.end();
2845
+ }
2846
+ }
2847
+ }
2848
+ function buildObjectMap(snapshot) {
2849
+ return new Map(snapshot.objects.map((object) => [object.key, object]));
2850
+ }
2851
+ function buildSignatureBuckets(objects) {
2852
+ const buckets = /* @__PURE__ */ new Map();
2853
+ for (const object of objects) {
2854
+ const bucketKey = `${object.kind}:${object.schema}:${object.signature}`;
2855
+ const current = buckets.get(bucketKey) ?? [];
2856
+ current.push(object);
2857
+ buckets.set(bucketKey, current);
2858
+ }
2859
+ return buckets;
2860
+ }
2861
+ function compareCanonicalSnapshots(reference, target) {
2862
+ const missing = [];
2863
+ const extra = [];
2864
+ const changed = [];
2865
+ const renamed = [];
2866
+ const targetByKey = buildObjectMap(target);
2867
+ const targetSignatureBuckets = buildSignatureBuckets(target.objects);
2868
+ const matchedTargetKeys = /* @__PURE__ */ new Set();
2869
+ for (const referenceObject of reference.objects) {
2870
+ const matchedTarget = targetByKey.get(referenceObject.key);
2871
+ if (matchedTarget) {
2872
+ matchedTargetKeys.add(matchedTarget.key);
2873
+ if (matchedTarget.signature !== referenceObject.signature) {
2874
+ changed.push({ reference: referenceObject, target: matchedTarget });
2875
+ }
2876
+ continue;
2877
+ }
2878
+ if (referenceObject.kind === "index") {
2879
+ const renameBucket = targetSignatureBuckets.get(
2880
+ `${referenceObject.kind}:${referenceObject.schema}:${referenceObject.signature}`
2881
+ ) ?? [];
2882
+ const renamedTarget = renameBucket.find((candidate) => !matchedTargetKeys.has(candidate.key));
2883
+ if (renamedTarget) {
2884
+ matchedTargetKeys.add(renamedTarget.key);
2885
+ renamed.push({ reference: referenceObject, target: renamedTarget });
2886
+ continue;
2887
+ }
2888
+ }
2889
+ missing.push(referenceObject);
2890
+ }
2891
+ for (const targetObject of target.objects) {
2892
+ if (!matchedTargetKeys.has(targetObject.key) && !reference.objects.some((candidate) => candidate.key === targetObject.key)) {
2893
+ extra.push(targetObject);
2894
+ }
2895
+ }
2896
+ return { missing, extra, changed, renamed };
2897
+ }
2898
+ function addSummaryKind(summary, kind) {
2899
+ if (!summary.changedKinds.includes(kind)) {
2900
+ summary.changedKinds.push(kind);
2901
+ summary.changedKinds.sort();
2902
+ }
2903
+ summary.hasChanges = true;
2904
+ }
2905
+ function ensureSchemaSummary(summaries, schemaName) {
2906
+ const existing = summaries[schemaName];
2907
+ if (existing) return existing;
2908
+ const created = {
2909
+ hasChanges: false,
2910
+ changedKinds: [],
2911
+ counts: { missing: 0, extra: 0, changed: 0, renamed: 0 }
2912
+ };
2913
+ summaries[schemaName] = created;
2914
+ return created;
2915
+ }
2916
+ function summarizeCanonicalDiffBySchema(diff) {
2917
+ const summaries = {};
2918
+ for (const object of diff.missing) {
2919
+ const summary = ensureSchemaSummary(summaries, object.schema);
2920
+ summary.counts.missing += 1;
2921
+ addSummaryKind(summary, object.kind);
2922
+ }
2923
+ for (const object of diff.extra) {
2924
+ const summary = ensureSchemaSummary(summaries, object.schema);
2925
+ summary.counts.extra += 1;
2926
+ addSummaryKind(summary, object.kind);
2927
+ }
2928
+ for (const pair of diff.changed) {
2929
+ const summary = ensureSchemaSummary(summaries, pair.reference.schema);
2930
+ summary.counts.changed += 1;
2931
+ addSummaryKind(summary, pair.reference.kind);
2932
+ }
2933
+ for (const pair of diff.renamed) {
2934
+ const summary = ensureSchemaSummary(summaries, pair.reference.schema);
2935
+ summary.counts.renamed += 1;
2936
+ addSummaryKind(summary, pair.reference.kind);
2937
+ }
2938
+ return summaries;
2939
+ }
2940
+ function hasCanonicalChanges(diff) {
2941
+ return diff.missing.length > 0 || diff.extra.length > 0 || diff.changed.length > 0 || diff.renamed.length > 0;
2942
+ }
2943
+
2944
+ // src/commands/ci/machine/actors/db/schema-stats.ts
2945
+ init_esm_shims();
2946
+ var EXCLUDED_SCHEMAS2 = [
2504
2947
  // PostgreSQL system schemas
2505
2948
  "pg_catalog",
2506
2949
  "pg_toast",
@@ -2604,15 +3047,18 @@ async function getDetailedIndexList(sql, schemaList) {
2604
3047
  t.relname as table,
2605
3048
  idx.indisunique as is_unique,
2606
3049
  idx.indpred IS NOT NULL as is_partial,
3050
+ am.amname as access_method,
2607
3051
  CASE
2608
3052
  WHEN idx.indpred IS NOT NULL
2609
3053
  THEN pg_get_expr(idx.indpred, idx.indrelid, true)
2610
3054
  ELSE NULL
2611
- END as where_clause
3055
+ END as where_clause,
3056
+ pg_get_indexdef(i.oid) as definition
2612
3057
  FROM pg_index idx
2613
3058
  JOIN pg_class i ON i.oid = idx.indexrelid
2614
3059
  JOIN pg_class t ON t.oid = idx.indrelid
2615
3060
  JOIN pg_namespace n ON n.oid = i.relnamespace
3061
+ JOIN pg_am am ON am.oid = i.relam
2616
3062
  WHERE n.nspname IN (${sql.unsafe(schemaList)})
2617
3063
  AND NOT EXISTS (
2618
3064
  SELECT 1 FROM pg_constraint c
@@ -2626,11 +3072,13 @@ async function getDetailedIndexList(sql, schemaList) {
2626
3072
  table: row.table,
2627
3073
  isUnique: row.is_unique,
2628
3074
  isPartial: row.is_partial,
2629
- whereClause: row.where_clause || void 0
3075
+ whereClause: row.where_clause || void 0,
3076
+ accessMethod: row.access_method || void 0,
3077
+ definition: normalizeIndexDefinition(row.definition || void 0)
2630
3078
  }));
2631
3079
  }
2632
- async function getUserSchemas(sql) {
2633
- const excludeList = EXCLUDED_SCHEMAS.map((s) => `'${s}'`).join(", ");
3080
+ async function getUserSchemas2(sql) {
3081
+ const excludeList = EXCLUDED_SCHEMAS2.map((s) => `'${s}'`).join(", ");
2634
3082
  const result = await sql`
2635
3083
  SELECT nspname
2636
3084
  FROM pg_namespace
@@ -2653,7 +3101,7 @@ async function getDbSchemaStats(databaseUrl) {
2653
3101
  // Supabase production requires SSL; local Docker does not
2654
3102
  ...isRemoteSupabase && { ssl: "require" }
2655
3103
  });
2656
- const userSchemas = await getUserSchemas(sql);
3104
+ const userSchemas = await getUserSchemas2(sql);
2657
3105
  if (userSchemas.length === 0) {
2658
3106
  return { schemas, total };
2659
3107
  }
@@ -2783,12 +3231,30 @@ function collectExpectedDriftReasons(schemaName, fieldDiffs, expectedDrift) {
2783
3231
  function compareIndexLists(reference, target) {
2784
3232
  const refIndexes = reference.indexList || [];
2785
3233
  const targetIndexes = target.indexList || [];
2786
- const refKeys = new Set(refIndexes.map((i) => `${i.schema}.${i.name}`));
2787
- const targetKeys = new Set(targetIndexes.map((i) => `${i.schema}.${i.name}`));
2788
- const missing = refIndexes.filter((i) => !targetKeys.has(`${i.schema}.${i.name}`));
2789
- const extra = targetIndexes.filter((i) => !refKeys.has(`${i.schema}.${i.name}`));
3234
+ const buildIndexKey = (index) => [
3235
+ index.schema,
3236
+ index.table,
3237
+ index.isUnique ? "unique" : "nonunique",
3238
+ index.isPartial ? "partial" : "full",
3239
+ normalizeDiffValue(index.whereClause),
3240
+ normalizeDiffValue(index.accessMethod),
3241
+ normalizeIndexDefinition(index.definition)
3242
+ ].join(":");
3243
+ const refKeys = new Set(refIndexes.map(buildIndexKey));
3244
+ const targetKeys = new Set(targetIndexes.map(buildIndexKey));
3245
+ const missing = refIndexes.filter((index) => !targetKeys.has(buildIndexKey(index)));
3246
+ const extra = targetIndexes.filter((index) => !refKeys.has(buildIndexKey(index)));
2790
3247
  return { missing, extra };
2791
3248
  }
3249
+ function normalizeDiffValue(value) {
3250
+ return (value ?? "").replace(/\s+/g, " ").trim();
3251
+ }
3252
+ function normalizeIndexDefinition(value) {
3253
+ return normalizeDiffValue(value).replace(
3254
+ /^create\s+(unique\s+)?index\s+\S+\s+on\s+/i,
3255
+ (_match, unique = "") => `create ${unique || ""}index on `
3256
+ );
3257
+ }
2792
3258
  function formatIndexInfo(index) {
2793
3259
  const qualifiedName = `${index.schema}.${index.name}`;
2794
3260
  const tags = [];
@@ -2805,7 +3271,27 @@ function hasIndexDiff(diff) {
2805
3271
  function emptyStats() {
2806
3272
  return {
2807
3273
  schemas: {},
2808
- total: { tables: 0, functions: 0, policies: 0, indexes: 0, triggers: 0 }
3274
+ total: { tables: 0, functions: 0, policies: 0, indexes: 0, triggers: 0 },
3275
+ available: true
3276
+ };
3277
+ }
3278
+ function unavailableStats(error) {
3279
+ return {
3280
+ ...emptyStats(),
3281
+ available: false,
3282
+ error,
3283
+ canonicalSnapshot: createUnavailableCanonicalSnapshot(error)
3284
+ };
3285
+ }
3286
+ function cloneEnvironmentStats(stats) {
3287
+ return {
3288
+ ...stats,
3289
+ schemas: Object.fromEntries(
3290
+ Object.entries(stats.schemas).map(([schema, values]) => [schema, { ...values }])
3291
+ ),
3292
+ total: { ...stats.total },
3293
+ indexList: stats.indexList?.map((index) => ({ ...index })),
3294
+ canonicalSnapshot: stats.canonicalSnapshot
2809
3295
  };
2810
3296
  }
2811
3297
  function logSchemaStats(label, stats, includeSchemas = false) {
@@ -2817,43 +3303,158 @@ function logSchemaStats(label, stats, includeSchemas = false) {
2817
3303
  const schemas = Object.keys(stats.schemas);
2818
3304
  console.log(`${base} (schemas: ${schemas.join(", ")})`);
2819
3305
  }
2820
- function createStatsTask(dbUrl, label, includeSchemas = false) {
3306
+ async function collectEnvironmentStats(dbUrl, label, includeSchemas = false) {
2821
3307
  if (!dbUrl) {
2822
- return Promise.resolve(null);
3308
+ return null;
2823
3309
  }
2824
- return getDbSchemaStats(dbUrl).then((stats) => {
2825
- logSchemaStats(label, stats, includeSchemas);
2826
- return stats;
2827
- });
3310
+ const stats = await getDbSchemaStats(dbUrl);
3311
+ stats.canonicalSnapshot = await getCanonicalSchemaSnapshot(dbUrl);
3312
+ stats.available = true;
3313
+ logSchemaStats(label, stats, includeSchemas);
3314
+ return stats;
2828
3315
  }
2829
3316
  function resolveSettledStats(result, failureLabel) {
2830
3317
  if (result.status === "fulfilled" && result.value) {
2831
3318
  return result.value;
2832
3319
  }
3320
+ const reason = result.status === "rejected" ? String(result.reason) : `${failureLabel} database URL is not available for schema comparison`;
2833
3321
  if (result.status === "rejected") {
2834
3322
  console.warn(`[schema-stats] Failed to query ${failureLabel} database: ${result.reason}`);
2835
3323
  }
2836
- return emptyStats();
3324
+ return unavailableStats(reason);
2837
3325
  }
2838
3326
  function resolveSettledProductionStats(result) {
2839
3327
  if (result.status === "fulfilled") {
2840
3328
  return result.value;
2841
3329
  }
2842
3330
  console.warn(`[schema-stats] Failed to query production database: ${result.reason}`);
2843
- return null;
3331
+ return unavailableStats(String(result.reason));
3332
+ }
3333
+ function buildAdminDsn(sourceDbUrl) {
3334
+ const conn = parsePostgresUrl(sourceDbUrl);
3335
+ const password = conn.password ? `:${encodeURIComponent(conn.password)}` : "";
3336
+ return `postgresql://${conn.user}${password}@${conn.host}:${conn.port}/postgres`;
3337
+ }
3338
+ function buildShadowDsn(sourceDbUrl, dbName) {
3339
+ const conn = parsePostgresUrl(sourceDbUrl);
3340
+ const password = conn.password ? `:${encodeURIComponent(conn.password)}` : "";
3341
+ return `postgresql://${conn.user}${password}@${conn.host}:${conn.port}/${dbName}`;
3342
+ }
3343
+ function createReferenceDb(sourceDbUrl) {
3344
+ const dbName = `ci_schema_ref_${randomUUID().replace(/-/g, "").slice(0, 12)}`;
3345
+ const adminDsn = buildAdminDsn(sourceDbUrl);
3346
+ const createResult = psqlSyncQuery({
3347
+ databaseUrl: adminDsn,
3348
+ sql: `CREATE DATABASE ${dbName}`,
3349
+ timeout: 3e4
3350
+ });
3351
+ if (createResult.status !== 0) {
3352
+ throw new Error(
3353
+ `Failed to create reference database: ${createResult.stderr || createResult.stdout}`
3354
+ );
3355
+ }
3356
+ return { dsn: buildShadowDsn(sourceDbUrl, dbName), dbName };
3357
+ }
3358
+ function dropReferenceDb(sourceDbUrl, dbName) {
3359
+ const adminDsn = buildAdminDsn(sourceDbUrl);
3360
+ psqlSyncQuery({
3361
+ databaseUrl: adminDsn,
3362
+ sql: `
3363
+ SELECT pg_terminate_backend(pid)
3364
+ FROM pg_stat_activity
3365
+ WHERE datname = '${dbName}' AND pid <> pg_backend_pid()
3366
+ `,
3367
+ timeout: 1e4
3368
+ });
3369
+ psqlSyncQuery({
3370
+ databaseUrl: adminDsn,
3371
+ sql: `DROP DATABASE IF EXISTS ${dbName}`,
3372
+ timeout: 3e4
3373
+ });
3374
+ }
3375
+ async function buildReferenceStats(repoRoot, tmpDir, sourceDbUrl) {
3376
+ if (!sourceDbUrl) return null;
3377
+ const referenceDb = createReferenceDb(sourceDbUrl);
3378
+ const logFile = path4.join(tmpDir, "ci-reference-db-apply.log");
3379
+ try {
3380
+ const result = await execa(
3381
+ "pnpm",
3382
+ ["exec", "runa", "db", "apply", "preview", "--auto-approve", "--no-seed", "--verbose"],
3383
+ {
3384
+ cwd: repoRoot,
3385
+ env: {
3386
+ ...process.env,
3387
+ DATABASE_URL_ADMIN: referenceDb.dsn,
3388
+ DATABASE_URL: referenceDb.dsn
3389
+ },
3390
+ reject: false
3391
+ }
3392
+ );
3393
+ const fullOutput = `${result.stdout || ""}
3394
+ ${result.stderr || ""}`;
3395
+ if (result.exitCode !== 0) {
3396
+ throw new Error(`Failed to build reference DB: ${fullOutput.substring(0, 1e3)}`);
3397
+ }
3398
+ await (await import('fs/promises')).writeFile(logFile, fullOutput);
3399
+ return collectEnvironmentStats(referenceDb.dsn, "Reference", true);
3400
+ } finally {
3401
+ dropReferenceDb(sourceDbUrl, referenceDb.dbName);
3402
+ }
3403
+ }
3404
+ async function resolveReferenceAndCiStats(params) {
3405
+ const { repoRoot, tmpDir, referenceDbUrl, ciDbUrl, referenceStrategy } = params;
3406
+ if (referenceStrategy === "reuse-ci" && ciDbUrl) {
3407
+ console.log(
3408
+ "[schema-stats] Reusing synced CI database as reference schema (skip temporary rebuild)"
3409
+ );
3410
+ const [ciResult2] = await Promise.allSettled([collectEnvironmentStats(ciDbUrl, "CI")]);
3411
+ const ci = resolveSettledStats(ciResult2, "CI");
3412
+ const reference = ci.available === false ? unavailableStats(ci.error ?? "CI database is unavailable for reference schema reuse") : cloneEnvironmentStats(ci);
3413
+ return [reference, ci];
3414
+ }
3415
+ const [referenceResult, ciResult] = await Promise.allSettled([
3416
+ buildReferenceStats(repoRoot, tmpDir, referenceDbUrl),
3417
+ collectEnvironmentStats(ciDbUrl, "CI")
3418
+ ]);
3419
+ return [resolveSettledStats(referenceResult, "reference"), resolveSettledStats(ciResult, "CI")];
2844
3420
  }
2845
3421
  var collectSchemaStatsActor = fromPromise(
2846
3422
  async ({ input }) => {
2847
- const { localDbUrl, ciDbUrl, queryProduction } = input;
2848
- console.log("[schema-stats] Collecting schema statistics (DB query mode, parallel)...");
3423
+ const {
3424
+ repoRoot,
3425
+ referenceDbUrl,
3426
+ ciDbUrl,
3427
+ referenceStrategy = "rebuild",
3428
+ queryProduction,
3429
+ tmpDir
3430
+ } = input;
3431
+ console.log(
3432
+ "[schema-stats] Collecting schema statistics (Reference/CI/Production, parallel)..."
3433
+ );
2849
3434
  const productionUrl = process.env.GH_DATABASE_URL_ADMIN || process.env.GH_DATABASE_URL;
2850
- const [localResult, ciResult, productionResult] = await Promise.allSettled([
2851
- createStatsTask(localDbUrl, "Local", true),
2852
- createStatsTask(ciDbUrl, "CI"),
2853
- createStatsTask(queryProduction ? productionUrl ?? null : null, "Production")
3435
+ const [referenceResult, productionResult] = await Promise.allSettled([
3436
+ resolveReferenceAndCiStats({
3437
+ repoRoot,
3438
+ tmpDir,
3439
+ referenceDbUrl,
3440
+ ciDbUrl,
3441
+ referenceStrategy
3442
+ }),
3443
+ collectEnvironmentStats(queryProduction ? productionUrl ?? null : null, "Production")
2854
3444
  ]);
2855
- const local = resolveSettledStats(localResult, "local");
2856
- const ci = resolveSettledStats(ciResult, "CI");
3445
+ let local;
3446
+ let ci;
3447
+ if (referenceResult.status === "fulfilled") {
3448
+ local = referenceResult.value[0];
3449
+ ci = referenceResult.value[1];
3450
+ } else {
3451
+ console.warn(
3452
+ `[schema-stats] Failed to collect reference/CI schema statistics: ${referenceResult.reason}`
3453
+ );
3454
+ const reason = String(referenceResult.reason);
3455
+ local = unavailableStats(reason);
3456
+ ci = unavailableStats(reason);
3457
+ }
2857
3458
  const production = resolveSettledProductionStats(productionResult);
2858
3459
  return {
2859
3460
  schemaStats: {
@@ -3185,24 +3786,27 @@ function extractHazardsWithContext(fullOutput, repoRoot) {
3185
3786
  appendEmojiHazards(fullOutput, idempotentRoles, hazards);
3186
3787
  return hazards;
3187
3788
  }
3188
- function detectNoChanges(fullOutput) {
3789
+ function detectSchemaChangeState(fullOutput) {
3189
3790
  const lowerOutput = fullOutput.toLowerCase();
3791
+ const changesIndicators = [
3792
+ "check mode: schema changes detected (not applied)",
3793
+ "schema changes: detected"
3794
+ ];
3795
+ if (changesIndicators.some((indicator) => lowerOutput.includes(indicator))) {
3796
+ return "changes";
3797
+ }
3190
3798
  const noChangesIndicators = [
3799
+ "check mode: all changes are for idempotent-managed objects (nothing to apply)",
3800
+ "schema changes: none",
3191
3801
  "no schema changes detected",
3192
- // From actors.ts line 969
3193
- "no schema changes",
3194
- // Shorter variant
3195
3802
  "already in sync",
3196
- "nothing to apply",
3197
3803
  "schemas are up to date",
3198
- "no changes were applied",
3199
- // From db-apply.ts line 150
3200
- "no changes",
3201
- // From actors.ts line 578, 968
3202
- "in sync"
3203
- // From db-sync.ts line 80
3804
+ "nothing to apply"
3204
3805
  ];
3205
- return noChangesIndicators.some((indicator) => lowerOutput.includes(indicator));
3806
+ if (noChangesIndicators.some((indicator) => lowerOutput.includes(indicator))) {
3807
+ return "no-changes";
3808
+ }
3809
+ return "unknown";
3206
3810
  }
3207
3811
  function isCommandSuccess(exitCode, fullOutput) {
3208
3812
  return exitCode === 0 || fullOutput.includes("check complete") || fullOutput.includes("Check Summary");
@@ -3258,6 +3862,39 @@ function buildErrorResult(error, databaseUrl) {
3258
3862
  }
3259
3863
  };
3260
3864
  }
3865
+ function buildPreviewFromOutput(fullOutput, repoRoot, productionUrl) {
3866
+ const lines = fullOutput.split("\n");
3867
+ const planSql = extractSqlFromLines(lines) || extractSqlFromSchemaChanges(fullOutput);
3868
+ const hazardDetails = extractHazardsWithContext(fullOutput, repoRoot);
3869
+ const hazards = hazardDetails.map((h) => `${h.type}: ${h.message}`);
3870
+ const schemaChangeState = detectSchemaChangeState(fullOutput);
3871
+ const hasSqlPlan = planSql !== null && planSql.trim().length > 0;
3872
+ const hasHazards = hazards.length > 0;
3873
+ const hasChanges = schemaChangeState === "changes" ? true : schemaChangeState === "no-changes" ? false : hasSqlPlan || hasHazards;
3874
+ const effectivePlanSql = hasChanges ? planSql?.substring(0, 5e3) || null : null;
3875
+ const rawError = extractErrorMessage(fullOutput);
3876
+ return {
3877
+ planSql: effectivePlanSql,
3878
+ hazards,
3879
+ hazardDetails: hazardDetails.length > 0 ? hazardDetails : void 0,
3880
+ hasChanges,
3881
+ error: enhanceConnectionError(rawError, productionUrl)
3882
+ };
3883
+ }
3884
+ function buildSuccessPreviewResult(exitCode, fullOutput, repoRoot, productionUrl) {
3885
+ const isSuccess = isCommandSuccess(exitCode, fullOutput);
3886
+ const preview = buildPreviewFromOutput(fullOutput, repoRoot, productionUrl);
3887
+ return {
3888
+ preview: {
3889
+ executed: true,
3890
+ planSql: preview.planSql,
3891
+ hazards: preview.hazards,
3892
+ hazardDetails: preview.hazardDetails,
3893
+ hasChanges: preview.hasChanges,
3894
+ error: isSuccess ? null : preview.error
3895
+ }
3896
+ };
3897
+ }
3261
3898
  var productionPreviewActor = fromPromise(
3262
3899
  async ({ input }) => {
3263
3900
  const { repoRoot, tmpDir, shouldExecute } = input;
@@ -3282,28 +3919,7 @@ var productionPreviewActor = fromPromise(
3282
3919
  const fullOutput = `${stdout}
3283
3920
  ${stderr}`;
3284
3921
  await writeLogFile(logFile, fullOutput);
3285
- const lines = fullOutput.split("\n");
3286
- const planSql = extractSqlFromLines(lines) || extractSqlFromSchemaChanges(fullOutput);
3287
- const hazardDetails = extractHazardsWithContext(fullOutput, repoRoot);
3288
- const hazards = hazardDetails.map((h) => `${h.type}: ${h.message}`);
3289
- const hasNoChangesIndicator = detectNoChanges(fullOutput);
3290
- const hasSqlPlan = planSql !== null && planSql.trim().length > 0;
3291
- const hasHazards = hazards.length > 0;
3292
- const hasChanges = hasNoChangesIndicator ? false : hasSqlPlan || hasHazards;
3293
- const effectivePlanSql = hasChanges ? planSql?.substring(0, 5e3) || null : null;
3294
- const isSuccess = isCommandSuccess(result.exitCode, fullOutput);
3295
- const rawError = !isSuccess ? extractErrorMessage(fullOutput) : null;
3296
- const errorMessage = rawError ? enhanceConnectionError(rawError, productionUrl) : null;
3297
- return {
3298
- preview: {
3299
- executed: true,
3300
- planSql: effectivePlanSql,
3301
- hazards,
3302
- hazardDetails: hazardDetails.length > 0 ? hazardDetails : void 0,
3303
- hasChanges,
3304
- error: errorMessage
3305
- }
3306
- };
3922
+ return buildSuccessPreviewResult(result.exitCode, fullOutput, repoRoot, productionUrl);
3307
3923
  } catch (error) {
3308
3924
  return buildErrorResult(error, productionUrl);
3309
3925
  }
@@ -3611,11 +4227,30 @@ function parseSchemaChangeStats(sqlInput, logPath, repoRoot) {
3611
4227
  stats.hazards = parseHazards(sql, idempotentRoles);
3612
4228
  return stats;
3613
4229
  }
3614
- var syncSchemaActor = fromPromise(
3615
- async ({ input }) => {
3616
- const { repoRoot, tmpDir, databaseUrl, mode, skipCodegen } = input;
3617
- const envArg = mode === "ci-local" ? "local" : "preview";
3618
- const useDbApply = mode !== "ci-local";
4230
+ function isCheckSummaryOutput(output) {
4231
+ return output.includes("check complete") || output.includes("Check Summary");
4232
+ }
4233
+ function isSchemaOutOfSyncOutput(output) {
4234
+ const lowerOutput = output.toLowerCase();
4235
+ return lowerOutput.includes("database schema is out of sync") || lowerOutput.includes("db_schema_out_of_sync");
4236
+ }
4237
+ function countResidualStatements(stats) {
4238
+ return stats.creates.tables + stats.creates.indexes + stats.creates.policies + stats.creates.functions + stats.creates.other + stats.alters + stats.drops;
4239
+ }
4240
+ function analyzePostCheckResult(params) {
4241
+ const hasSchemaOutOfSync = isSchemaOutOfSyncOutput(params.output);
4242
+ const hasDrift = hasSchemaOutOfSync || countResidualStatements(params.residualStats) > 0;
4243
+ const checkSucceeded = params.exitCode === 0 || isCheckSummaryOutput(params.output) || hasSchemaOutOfSync;
4244
+ return {
4245
+ hasDrift: hasDrift || params.residualStats.hazards.length > 0,
4246
+ commandFailed: !checkSucceeded
4247
+ };
4248
+ }
4249
+ var syncSchemaActor = fromPromise(
4250
+ async ({ input }) => {
4251
+ const { repoRoot, tmpDir, databaseUrl, mode, skipCodegen } = input;
4252
+ const envArg = mode === "ci-local" ? "local" : "preview";
4253
+ const useDbApply = mode !== "ci-local";
3619
4254
  const gitDiff = getSchemaGitDiff(repoRoot);
3620
4255
  try {
3621
4256
  const databaseUrlForRuntime = normalizeDatabaseUrlForDdl(databaseUrl);
@@ -3649,7 +4284,18 @@ var syncSchemaActor = fromPromise(
3649
4284
  // Always verbose for full traceability
3650
4285
  ...skipCodegen ? ["--skip-codegen"] : []
3651
4286
  ];
4287
+ const checkArgs = useDbApply ? ["exec", "runa", "db", "apply", envArg, "--check", "--no-seed", "--verbose"] : [
4288
+ "exec",
4289
+ "runa",
4290
+ "db",
4291
+ "sync",
4292
+ envArg,
4293
+ "--check",
4294
+ "--verbose",
4295
+ ...skipCodegen ? ["--skip-codegen"] : []
4296
+ ];
3652
4297
  const logFile = path4.join(tmpDir, `ci-db-${useDbApply ? "apply" : "sync"}-${envArg}.log`);
4298
+ const afterCheckLogFile = path4.join(tmpDir, `ci-db-check-${envArg}.log`);
3653
4299
  await runLogged({
3654
4300
  cwd: repoRoot,
3655
4301
  env: baseEnv,
@@ -3658,23 +4304,54 @@ var syncSchemaActor = fromPromise(
3658
4304
  args: syncArgs,
3659
4305
  logFile
3660
4306
  });
4307
+ const postCheckResult = await runLogged({
4308
+ cwd: repoRoot,
4309
+ env: baseEnv,
4310
+ label: `db ${useDbApply ? "apply" : "sync"} check (${envArg})`,
4311
+ command: "pnpm",
4312
+ args: checkArgs,
4313
+ logFile: afterCheckLogFile,
4314
+ reject: false
4315
+ });
3661
4316
  let logContent = "";
4317
+ let afterCheckLogContent = "";
3662
4318
  try {
3663
4319
  logContent = readFileSync(logFile, "utf-8");
3664
4320
  } catch {
3665
4321
  }
4322
+ try {
4323
+ afterCheckLogContent = readFileSync(afterCheckLogFile, "utf-8");
4324
+ } catch {
4325
+ }
3666
4326
  const changeStats = parseSchemaChangeStats(logContent, null, repoRoot);
4327
+ const residualStats = parseSchemaChangeStats(afterCheckLogContent, null, repoRoot);
4328
+ const fullPostCheckOutput = `${postCheckResult.stdout || ""}
4329
+ ${postCheckResult.stderr || ""}`;
4330
+ const postCheckAnalysis = analyzePostCheckResult({
4331
+ exitCode: postCheckResult.exitCode ?? 1,
4332
+ output: fullPostCheckOutput,
4333
+ residualStats
4334
+ });
4335
+ if (postCheckAnalysis.commandFailed) {
4336
+ throw new Error(
4337
+ `Schema post-check failed (${postCheckResult.exitCode}): ${fullPostCheckOutput.substring(
4338
+ 0,
4339
+ 1e3
4340
+ )}`
4341
+ );
4342
+ }
3667
4343
  const schemaDrift = {
3668
4344
  beforeSql: logContent.substring(0, 5e3),
3669
4345
  // Truncate for comment
3670
- afterSql: null,
3671
- hasDrift: false,
4346
+ afterSql: postCheckAnalysis.hasDrift ? afterCheckLogContent.substring(0, 5e3) : "",
4347
+ checkExecuted: true,
4348
+ hasDrift: postCheckAnalysis.hasDrift,
3672
4349
  changeStats,
3673
4350
  gitDiff,
3674
4351
  logs: {
3675
4352
  beforeCheckLogPath: logFile,
3676
4353
  applyLogPath: logFile,
3677
- afterCheckLogPath: logFile
4354
+ afterCheckLogPath: afterCheckLogFile
3678
4355
  }
3679
4356
  };
3680
4357
  return { applied: true, schemaDrift };
@@ -3682,6 +4359,7 @@ var syncSchemaActor = fromPromise(
3682
4359
  const schemaDrift = {
3683
4360
  beforeSql: null,
3684
4361
  afterSql: null,
4362
+ checkExecuted: false,
3685
4363
  hasDrift: false,
3686
4364
  changeStats: null,
3687
4365
  gitDiff,
@@ -4345,6 +5023,213 @@ function safeReadFile(filePath) {
4345
5023
  return null;
4346
5024
  }
4347
5025
  }
5026
+ function buildSanitizeState() {
5027
+ return {
5028
+ inSingle: false,
5029
+ inDouble: false,
5030
+ inTemplate: false,
5031
+ inLineComment: false,
5032
+ inBlockComment: false
5033
+ };
5034
+ }
5035
+ function applySanitizeStep(state, stateUpdate) {
5036
+ return { ...state, ...stateUpdate };
5037
+ }
5038
+ function processLineCommentState(char) {
5039
+ return {
5040
+ output: char === "\n" ? "\n" : " ",
5041
+ nextIndex: 1,
5042
+ state: {
5043
+ inLineComment: char !== "\n"
5044
+ }
5045
+ };
5046
+ }
5047
+ function processBlockCommentState(char, next) {
5048
+ if (char === "*" && next === "/") {
5049
+ return {
5050
+ output: " ",
5051
+ nextIndex: 2,
5052
+ state: { inBlockComment: false }
5053
+ };
5054
+ }
5055
+ return {
5056
+ output: char === "\n" ? "\n" : " ",
5057
+ nextIndex: 1,
5058
+ state: {}
5059
+ };
5060
+ }
5061
+ function processSingleQuoteState(content, i, stripStrings) {
5062
+ const char = content[i] ?? "";
5063
+ if (char === "\\") {
5064
+ return {
5065
+ output: stripStrings ? " " : `${char}${content[i + 1] ?? ""}`,
5066
+ nextIndex: 2,
5067
+ state: {}
5068
+ };
5069
+ }
5070
+ if (char === "'") {
5071
+ return {
5072
+ output: char,
5073
+ nextIndex: 1,
5074
+ state: { inSingle: false }
5075
+ };
5076
+ }
5077
+ return {
5078
+ output: stripStrings ? " " : char,
5079
+ nextIndex: 1,
5080
+ state: {}
5081
+ };
5082
+ }
5083
+ function processDoubleQuoteState(content, i, stripStrings) {
5084
+ const char = content[i] ?? "";
5085
+ if (char === "\\") {
5086
+ return {
5087
+ output: stripStrings ? " " : `${char}${content[i + 1] ?? ""}`,
5088
+ nextIndex: 2,
5089
+ state: {}
5090
+ };
5091
+ }
5092
+ if (char === '"') {
5093
+ return {
5094
+ output: char,
5095
+ nextIndex: 1,
5096
+ state: { inDouble: false }
5097
+ };
5098
+ }
5099
+ return {
5100
+ output: stripStrings ? " " : char,
5101
+ nextIndex: 1,
5102
+ state: {}
5103
+ };
5104
+ }
5105
+ function processTemplateQuoteState(content, i, stripStrings) {
5106
+ const char = content[i] ?? "";
5107
+ if (char === "\\") {
5108
+ return {
5109
+ output: stripStrings ? " " : `${char}${content[i + 1] ?? ""}`,
5110
+ nextIndex: 2,
5111
+ state: {}
5112
+ };
5113
+ }
5114
+ if (char === "`") {
5115
+ return {
5116
+ output: "`",
5117
+ nextIndex: 1,
5118
+ state: { inTemplate: false }
5119
+ };
5120
+ }
5121
+ return {
5122
+ output: stripStrings ? char === "\n" ? "\n" : " " : char,
5123
+ nextIndex: 1,
5124
+ state: {}
5125
+ };
5126
+ }
5127
+ function processQuoteState(content, i, stripStrings, quote) {
5128
+ if (quote === "'") return processSingleQuoteState(content, i, stripStrings);
5129
+ if (quote === '"') return processDoubleQuoteState(content, i, stripStrings);
5130
+ return processTemplateQuoteState(content, i, stripStrings);
5131
+ }
5132
+ function processDefaultState(char, next) {
5133
+ if (char === "/" && next === "/") {
5134
+ return {
5135
+ output: " ",
5136
+ nextIndex: 2,
5137
+ state: { inLineComment: true }
5138
+ };
5139
+ }
5140
+ if (char === "/" && next === "*") {
5141
+ return {
5142
+ output: " ",
5143
+ nextIndex: 2,
5144
+ state: { inBlockComment: true }
5145
+ };
5146
+ }
5147
+ if (char === "'") {
5148
+ return {
5149
+ output: char,
5150
+ nextIndex: 1,
5151
+ state: { inSingle: true }
5152
+ };
5153
+ }
5154
+ if (char === '"') {
5155
+ return {
5156
+ output: char,
5157
+ nextIndex: 1,
5158
+ state: { inDouble: true }
5159
+ };
5160
+ }
5161
+ if (char === "`") {
5162
+ return {
5163
+ output: char,
5164
+ nextIndex: 1,
5165
+ state: { inTemplate: true }
5166
+ };
5167
+ }
5168
+ return { output: char, nextIndex: 1, state: {} };
5169
+ }
5170
+ function runSanitizeStep(step, state, result, currentIndex) {
5171
+ return {
5172
+ i: currentIndex + step.nextIndex,
5173
+ result: `${result}${step.output}`,
5174
+ state: applySanitizeStep(state, step.state)
5175
+ };
5176
+ }
5177
+ function sanitizeTsContent(content, options = {}) {
5178
+ const { stripStrings = false } = options;
5179
+ let result = "";
5180
+ let i = 0;
5181
+ let state = buildSanitizeState();
5182
+ while (i < content.length) {
5183
+ const char = content[i] ?? "";
5184
+ const next = content[i + 1] ?? "";
5185
+ if (state.inLineComment) {
5186
+ const step2 = processLineCommentState(char);
5187
+ const nextState2 = runSanitizeStep(step2, state, result, i);
5188
+ i = nextState2.i;
5189
+ result = nextState2.result;
5190
+ state = nextState2.state;
5191
+ continue;
5192
+ }
5193
+ if (state.inBlockComment) {
5194
+ const step2 = processBlockCommentState(char, next);
5195
+ const nextState2 = runSanitizeStep(step2, state, result, i);
5196
+ i = nextState2.i;
5197
+ result = nextState2.result;
5198
+ state = nextState2.state;
5199
+ continue;
5200
+ }
5201
+ if (state.inSingle) {
5202
+ const step2 = processQuoteState(content, i, stripStrings, "'");
5203
+ const nextState2 = runSanitizeStep(step2, state, result, i);
5204
+ i = nextState2.i;
5205
+ result = nextState2.result;
5206
+ state = nextState2.state;
5207
+ continue;
5208
+ }
5209
+ if (state.inDouble) {
5210
+ const step2 = processQuoteState(content, i, stripStrings, '"');
5211
+ const nextState2 = runSanitizeStep(step2, state, result, i);
5212
+ i = nextState2.i;
5213
+ result = nextState2.result;
5214
+ state = nextState2.state;
5215
+ continue;
5216
+ }
5217
+ if (state.inTemplate) {
5218
+ const step2 = processQuoteState(content, i, stripStrings, "`");
5219
+ const nextState2 = runSanitizeStep(step2, state, result, i);
5220
+ i = nextState2.i;
5221
+ result = nextState2.result;
5222
+ state = nextState2.state;
5223
+ continue;
5224
+ }
5225
+ const step = processDefaultState(char, next);
5226
+ const nextState = runSanitizeStep(step, state, result, i);
5227
+ i = nextState.i;
5228
+ result = nextState.result;
5229
+ state = nextState.state;
5230
+ }
5231
+ return result;
5232
+ }
4348
5233
  function readManifest(manifestPath) {
4349
5234
  try {
4350
5235
  if (!existsSync(manifestPath)) {
@@ -4389,11 +5274,12 @@ async function detectLayer1Content(repoRoot, manifest) {
4389
5274
  const filePath = join(repoRoot, file);
4390
5275
  const content = safeReadFile(filePath);
4391
5276
  if (!content) continue;
5277
+ const codeOnly = sanitizeTsContent(content, { stripStrings: true });
4392
5278
  const xstatePatterns = [
4393
5279
  /(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*createMachine\s*\(/,
4394
5280
  /(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*setup\s*\([^)]*\)\s*\.createMachine\s*\(/
4395
5281
  ];
4396
- const hasXStateMachine = xstatePatterns.some((p) => p.test(content));
5282
+ const hasXStateMachine = xstatePatterns.some((p) => p.test(codeOnly));
4397
5283
  if (hasXStateMachine) {
4398
5284
  machineCount++;
4399
5285
  }
@@ -4523,6 +5409,41 @@ async function detectLayer4Content(repoRoot, manifest) {
4523
5409
  }
4524
5410
  return createSkipStatus(base, "No user-facing pages found");
4525
5411
  }
5412
+ var detectStaticLayer = async (_repoRoot, _manifest) => ({
5413
+ layer: 0,
5414
+ hasContent: true,
5415
+ contentType: "machines",
5416
+ // Not really, but L0 always runs
5417
+ count: 1,
5418
+ source: "filesystem"
5419
+ });
5420
+ var layerDetectors = {
5421
+ 0: detectStaticLayer,
5422
+ 1: detectLayer1Content,
5423
+ 2: detectLayer2Content,
5424
+ 3: detectLayer3Content,
5425
+ 4: detectLayer4Content
5426
+ };
5427
+ function getUnknownLayerStatus(layer) {
5428
+ return {
5429
+ layer,
5430
+ hasContent: false,
5431
+ contentType: "machines",
5432
+ count: 0,
5433
+ source: "filesystem",
5434
+ skipReason: `Unknown layer ${layer}`
5435
+ };
5436
+ }
5437
+ async function detectLayerStatus(layer, repoRoot, manifest) {
5438
+ const detector = layerDetectors[layer];
5439
+ if (!detector) {
5440
+ return getUnknownLayerStatus(layer);
5441
+ }
5442
+ if (layer === 0) {
5443
+ return detectStaticLayer();
5444
+ }
5445
+ return detector(repoRoot, manifest);
5446
+ }
4526
5447
  async function detectLayerContent(repoRoot, selectedLayers, manifestPath) {
4527
5448
  const effectiveManifestPath = join(repoRoot, ".runa/manifests/manifest.json");
4528
5449
  const manifest = readManifest(effectiveManifestPath);
@@ -4530,40 +5451,7 @@ async function detectLayerContent(repoRoot, selectedLayers, manifestPath) {
4530
5451
  const skippedLayers = [];
4531
5452
  const runnableLayers = [];
4532
5453
  for (const layer of selectedLayers) {
4533
- let status;
4534
- switch (layer) {
4535
- case 0:
4536
- status = {
4537
- layer: 0,
4538
- hasContent: true,
4539
- contentType: "machines",
4540
- // Not really, but L0 always runs
4541
- count: 1,
4542
- source: "filesystem"
4543
- };
4544
- break;
4545
- case 1:
4546
- status = await detectLayer1Content(repoRoot, manifest);
4547
- break;
4548
- case 2:
4549
- status = await detectLayer2Content(repoRoot);
4550
- break;
4551
- case 3:
4552
- status = await detectLayer3Content(repoRoot, manifest);
4553
- break;
4554
- case 4:
4555
- status = await detectLayer4Content(repoRoot, manifest);
4556
- break;
4557
- default:
4558
- status = {
4559
- layer,
4560
- hasContent: false,
4561
- contentType: "machines",
4562
- count: 0,
4563
- source: "filesystem",
4564
- skipReason: `Unknown layer ${layer}`
4565
- };
4566
- }
5454
+ const status = await detectLayerStatus(layer, repoRoot, manifest);
4567
5455
  layers[layer] = status;
4568
5456
  if (status.hasContent) {
4569
5457
  runnableLayers.push(layer);
@@ -4983,6 +5871,9 @@ function getLayersForCorePhase(selectedLayers, mode) {
4983
5871
  function hasE2ELayer(selectedLayers) {
4984
5872
  return selectedLayers.includes(E2E_LAYER);
4985
5873
  }
5874
+ function shouldReuseCiReferenceStats(context) {
5875
+ return context.mode !== "ci-local" && context.schemaApplied && context.schemaDrift !== null && context.schemaDrift?.hasDrift === false;
5876
+ }
4986
5877
  function mergeLayerResults(coreResults, e2eResults) {
4987
5878
  return { ...coreResults, ...e2eResults };
4988
5879
  }
@@ -5079,7 +5970,7 @@ init_esm_shims();
5079
5970
  var CI_STEPS = [
5080
5971
  { step: "setup", label: "\u74B0\u5883\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7", detail: "\u30EA\u30DD\u30B8\u30C8\u30EA\u691C\u51FA\u3001Supabase\u8D77\u52D5" },
5081
5972
  { step: "syncSchema", label: "\u30B9\u30AD\u30FC\u30DE\u540C\u671F", detail: "pg-schema-diff \u2192 CI DB" },
5082
- { step: "applySeeds", label: "\u30B7\u30FC\u30C9\u9069\u7528", detail: "ci.sql + pgTAP + roles" },
5973
+ { step: "applySeeds", label: "\u30B7\u30FC\u30C9\u9069\u7528", detail: "ci.sql + prerequisite seeds" },
5083
5974
  { step: "staticChecks", label: "\u9759\u7684\u30C1\u30A7\u30C3\u30AF", detail: "\u578B\u30C1\u30A7\u30C3\u30AF \u2225 lint" },
5084
5975
  { step: "build", label: "\u30D3\u30EB\u30C9", detail: "build \u2225 playwright install" },
5085
5976
  { step: "runTests", label: "\u30C6\u30B9\u30C8\u5B9F\u884C", detail: "L0-L3 (\u30D6\u30ED\u30C3\u30AD\u30F3\u30B0) \u2192 L4 (E2E)" },
@@ -5243,43 +6134,187 @@ function formatSkippedLayers(layerSkipReasons, originalSelectedLayers) {
5243
6134
  return lines;
5244
6135
  }
5245
6136
 
5246
- // src/commands/ci/machine/formatters/sections/schema-matrix.ts
6137
+ // src/commands/ci/machine/formatters/sections/production-schema-status.ts
5247
6138
  init_esm_shims();
6139
+ var COUNT_FIELDS = [
6140
+ "tables",
6141
+ "functions",
6142
+ "policies",
6143
+ "indexes",
6144
+ "triggers"
6145
+ ];
5248
6146
  function emptyStats2() {
5249
6147
  return { tables: 0, functions: 0, policies: 0, indexes: 0, triggers: 0 };
5250
6148
  }
5251
- function formatLocalCell(stats) {
6149
+ function findExpectedCountDrift(schemaName, field, expectedDrift) {
6150
+ return expectedDrift.find(
6151
+ (entry) => entry.field === field && (entry.schemas === void 0 || entry.schemas.includes(schemaName))
6152
+ ) ?? null;
6153
+ }
6154
+ function buildCountDiffs(schemaStats, expectedDrift) {
6155
+ if (!schemaStats?.local || !schemaStats.production) return [];
6156
+ if (schemaStats.local.available === false || schemaStats.production.available === false) {
6157
+ return [];
6158
+ }
6159
+ const schemaNames = /* @__PURE__ */ new Set([
6160
+ ...Object.keys(schemaStats.local.schemas),
6161
+ ...Object.keys(schemaStats.production.schemas)
6162
+ ]);
6163
+ const diffs = [];
6164
+ for (const schemaName of Array.from(schemaNames).sort()) {
6165
+ const reference = schemaStats.local.schemas[schemaName] ?? emptyStats2();
6166
+ const production = schemaStats.production.schemas[schemaName] ?? emptyStats2();
6167
+ for (const field of COUNT_FIELDS) {
6168
+ if (reference[field] === production[field]) continue;
6169
+ const expected = findExpectedCountDrift(schemaName, field, expectedDrift);
6170
+ diffs.push({
6171
+ schema: schemaName,
6172
+ field,
6173
+ value: production[field],
6174
+ reference: reference[field],
6175
+ delta: production[field] - reference[field],
6176
+ expected: expected !== null,
6177
+ reason: expected?.reason
6178
+ });
6179
+ }
6180
+ }
6181
+ return diffs;
6182
+ }
6183
+ function areIndexDiffsCoveredByExpectedDrift(indexDiff, countDiffs) {
6184
+ if (!indexDiff || !hasIndexDiff(indexDiff)) return false;
6185
+ const touchedSchemas = /* @__PURE__ */ new Set([
6186
+ ...indexDiff.missing.map((index) => index.schema),
6187
+ ...indexDiff.extra.map((index) => index.schema)
6188
+ ]);
6189
+ if (touchedSchemas.size === 0) return false;
6190
+ const expectedIndexSchemas = new Set(
6191
+ countDiffs.filter((diff) => diff.field === "indexes" && diff.expected).map((diff) => diff.schema)
6192
+ );
6193
+ const unexpectedIndexSchemas = new Set(
6194
+ countDiffs.filter((diff) => diff.field === "indexes" && !diff.expected).map((diff) => diff.schema)
6195
+ );
6196
+ for (const schema of touchedSchemas) {
6197
+ if (unexpectedIndexSchemas.has(schema)) return false;
6198
+ if (!expectedIndexSchemas.has(schema)) return false;
6199
+ }
6200
+ return true;
6201
+ }
6202
+ function buildIndexDiff(schemaStats) {
6203
+ if (!schemaStats?.local || !schemaStats.production) return null;
6204
+ if (schemaStats.local.available === false || schemaStats.production.available === false) {
6205
+ return null;
6206
+ }
6207
+ return compareIndexLists(schemaStats.local, schemaStats.production);
6208
+ }
6209
+ function buildSemanticDiff(schemaStats) {
6210
+ const referenceSnapshot = schemaStats?.local?.canonicalSnapshot;
6211
+ const productionSnapshot = schemaStats?.production?.canonicalSnapshot;
6212
+ if (!referenceSnapshot || !productionSnapshot) return null;
6213
+ if (referenceSnapshot.available !== true || productionSnapshot.available !== true) return null;
6214
+ return compareCanonicalSnapshots(referenceSnapshot, productionSnapshot);
6215
+ }
6216
+ function getProductionSchemaSignals(productionPreview, schemaStats, expectedDrift = []) {
6217
+ const previewHasChanges = productionPreview?.hasChanges === true;
6218
+ const indexDiff = buildIndexDiff(schemaStats);
6219
+ const semanticDiff = buildSemanticDiff(schemaStats);
6220
+ const countDiffs = buildCountDiffs(schemaStats, expectedDrift);
6221
+ const hasIndexChanges = indexDiff ? hasIndexDiff(indexDiff) : false;
6222
+ const hasUnexpectedIndexChanges = hasIndexChanges && !areIndexDiffsCoveredByExpectedDrift(indexDiff, countDiffs);
6223
+ const hasSemanticChanges = semanticDiff ? hasCanonicalChanges(semanticDiff) : false;
6224
+ const hasCountChanges = countDiffs.length > 0;
6225
+ const hasUnexpectedCountChanges = countDiffs.some((diff) => !diff.expected);
6226
+ const knownDriftReasons = Array.from(
6227
+ new Set(countDiffs.filter((diff) => diff.expected).flatMap((diff) => diff.reason ?? []))
6228
+ );
6229
+ const hasKnownDriftOnly = hasCountChanges && !previewHasChanges && !hasUnexpectedCountChanges && !hasUnexpectedIndexChanges && !hasSemanticChanges;
6230
+ const requiresDeploy = previewHasChanges || hasUnexpectedIndexChanges || hasSemanticChanges || hasUnexpectedCountChanges;
6231
+ return {
6232
+ previewHasChanges,
6233
+ indexDiff,
6234
+ semanticDiff,
6235
+ countDiffs,
6236
+ hasIndexChanges,
6237
+ hasUnexpectedIndexChanges,
6238
+ hasSemanticChanges,
6239
+ hasCountChanges,
6240
+ hasUnexpectedCountChanges,
6241
+ hasKnownDriftOnly,
6242
+ knownDriftReasons,
6243
+ requiresDeploy,
6244
+ previewMissedChanges: !previewHasChanges && (hasUnexpectedIndexChanges || hasSemanticChanges || hasUnexpectedCountChanges),
6245
+ previewOnlyChanges: previewHasChanges && !hasUnexpectedIndexChanges && !hasSemanticChanges && !hasUnexpectedCountChanges
6246
+ };
6247
+ }
6248
+
6249
+ // src/commands/ci/machine/formatters/sections/schema-matrix.ts
6250
+ init_esm_shims();
6251
+ function emptyStats3() {
6252
+ return { tables: 0, functions: 0, policies: 0, indexes: 0, triggers: 0 };
6253
+ }
6254
+ function isStatsAvailable(stats) {
6255
+ return stats != null && stats.available !== false;
6256
+ }
6257
+ function isSemanticSnapshotAvailable(stats) {
6258
+ return stats != null && stats.available !== false && stats.canonicalSnapshot?.available === true;
6259
+ }
6260
+ function formatUnavailableCell(stats) {
6261
+ if (stats == null) return "n/a";
6262
+ return stats.error ? `\u26AA unavailable` : "n/a";
6263
+ }
6264
+ function formatReferenceCell(stats, isAvailable) {
6265
+ if (!isAvailable) return "\u26AA unavailable";
5252
6266
  return formatSchemaStatsCompact(stats, true);
5253
6267
  }
5254
- function formatCiCellWithDiff(stats, reference) {
5255
- if (hasDisplayedStatsDiff(stats, reference)) {
6268
+ function formatCiCellWithDiff(stats, reference, semanticSummary, isAvailable, referenceAvailable) {
6269
+ if (!isAvailable) return "\u26AA unavailable";
6270
+ if (!referenceAvailable) return formatSchemaStatsCompact(stats, true);
6271
+ if (hasDisplayedStatsDiff(stats, reference) || semanticSummary?.hasChanges) {
5256
6272
  return `\u26A0\uFE0F ${formatSchemaStatsWithDiff(stats, reference)}`;
5257
6273
  }
5258
6274
  return formatSchemaStatsCompact(stats, true);
5259
6275
  }
5260
- function formatProdCellWithDiff(stats, reference, schemaName, expectedDrift) {
5261
- if (!hasDisplayedStatsDiff(stats, reference)) {
6276
+ function formatProdCellWithDiff(stats, reference, schemaName, expectedDrift, semanticSummary, isAvailable, referenceAvailable) {
6277
+ if (!isAvailable) return "\u26AA unavailable";
6278
+ if (!referenceAvailable) return formatSchemaStatsCompact(stats, true);
6279
+ if (!hasDisplayedStatsDiff(stats, reference) && !semanticSummary?.hasChanges) {
5262
6280
  return formatSchemaStatsCompact(stats, true);
5263
6281
  }
5264
6282
  const fieldDiffs = getFieldDiffs(stats, reference);
5265
- const allExpected = expectedDrift.length > 0 && isAllDriftExpected(schemaName, fieldDiffs, expectedDrift);
6283
+ const allExpected = !semanticSummary?.hasChanges && expectedDrift.length > 0 && isAllDriftExpected(schemaName, fieldDiffs, expectedDrift);
5266
6284
  const marker = allExpected ? "\u{1F4CB}" : "\u{1F4DD}";
5267
6285
  return `${marker} ${formatSchemaStatsWithDiff(stats, reference)}`;
5268
6286
  }
5269
- function formatCiTotalCellWithDiff(stats, reference) {
6287
+ function formatCiTotalCellWithDiff(stats, reference, hasSemanticChanges, isAvailable, referenceAvailable) {
6288
+ if (!isAvailable) return "**\u26AA unavailable**";
6289
+ if (!referenceAvailable) return `**${formatSchemaStatsCompact(stats, true)}**`;
5270
6290
  const formatted = formatSchemaStatsCompact(stats, true);
5271
- if (hasDisplayedStatsDiff(stats, reference)) {
6291
+ if (hasDisplayedStatsDiff(stats, reference) || hasSemanticChanges) {
5272
6292
  return `\u26A0\uFE0F **${formatted}**`;
5273
6293
  }
5274
6294
  return `**${formatted}**`;
5275
6295
  }
5276
- function formatProdTotalCellWithDiff(stats, reference) {
6296
+ function formatProdTotalCellWithDiff(stats, reference, hasSemanticChanges, isAvailable, referenceAvailable) {
6297
+ if (!isAvailable) return "**\u26AA unavailable**";
6298
+ if (!referenceAvailable) return `**${formatSchemaStatsCompact(stats, true)}**`;
5277
6299
  const formatted = formatSchemaStatsCompact(stats, true);
5278
- if (hasDisplayedStatsDiff(stats, reference)) {
6300
+ if (hasDisplayedStatsDiff(stats, reference) || hasSemanticChanges) {
5279
6301
  return `\u{1F4DD} **${formatted}**`;
5280
6302
  }
5281
6303
  return `**${formatted}**`;
5282
6304
  }
6305
+ function buildCanonicalDiffs(schemaStats) {
6306
+ const referenceSnapshot = schemaStats.local.canonicalSnapshot;
6307
+ const ciSnapshot = schemaStats.ci.canonicalSnapshot;
6308
+ const productionSnapshot = schemaStats.production?.canonicalSnapshot;
6309
+ const ciDiff = isSemanticSnapshotAvailable(schemaStats.local) && isSemanticSnapshotAvailable(schemaStats.ci) ? compareCanonicalSnapshots(referenceSnapshot, ciSnapshot) : null;
6310
+ const productionDiff = isSemanticSnapshotAvailable(schemaStats.local) && isSemanticSnapshotAvailable(schemaStats.production) ? compareCanonicalSnapshots(referenceSnapshot, productionSnapshot) : null;
6311
+ return {
6312
+ ci: ciDiff,
6313
+ production: productionDiff,
6314
+ ciBySchema: ciDiff ? summarizeCanonicalDiffBySchema(ciDiff) : {},
6315
+ productionBySchema: productionDiff ? summarizeCanonicalDiffBySchema(productionDiff) : {}
6316
+ };
6317
+ }
5283
6318
  function getSortedActiveSchemas(schemaStats) {
5284
6319
  const allSchemas = /* @__PURE__ */ new Set([
5285
6320
  ...getActiveSchemas(schemaStats.local),
@@ -5292,37 +6327,67 @@ function getSortedActiveSchemas(schemaStats) {
5292
6327
  return a.localeCompare(b);
5293
6328
  });
5294
6329
  }
5295
- function generateSchemaTableRows(schemaStats, sortedSchemas, hasProduction, expectedDrift) {
6330
+ function generateSchemaTableRows(schemaStats, sortedSchemas, hasProduction, expectedDrift, ciDiffBySchema, productionDiffBySchema) {
5296
6331
  const lines = [];
5297
6332
  for (const schemaName of sortedSchemas) {
5298
- const localStats = schemaStats.local.schemas[schemaName] ?? emptyStats2();
5299
- const ciStats = schemaStats.ci.schemas[schemaName] ?? emptyStats2();
6333
+ const localStats = schemaStats.local.schemas[schemaName] ?? emptyStats3();
6334
+ const ciStats = schemaStats.ci.schemas[schemaName] ?? emptyStats3();
5300
6335
  const prodStats = schemaStats.production?.schemas[schemaName] ?? null;
5301
- const localCell = formatLocalCell(localStats);
5302
- const ciCell = formatCiCellWithDiff(ciStats, localStats);
5303
- const prodCell = prodStats !== null ? formatProdCellWithDiff(prodStats, localStats, schemaName, expectedDrift) : null;
6336
+ const localCell = formatReferenceCell(localStats, isStatsAvailable(schemaStats.local));
6337
+ const ciCell = formatCiCellWithDiff(
6338
+ ciStats,
6339
+ localStats,
6340
+ ciDiffBySchema[schemaName],
6341
+ isStatsAvailable(schemaStats.ci),
6342
+ isStatsAvailable(schemaStats.local)
6343
+ );
6344
+ const prodCell = prodStats !== null ? formatProdCellWithDiff(
6345
+ prodStats,
6346
+ localStats,
6347
+ schemaName,
6348
+ expectedDrift,
6349
+ productionDiffBySchema[schemaName],
6350
+ isStatsAvailable(schemaStats.production),
6351
+ isStatsAvailable(schemaStats.local)
6352
+ ) : formatUnavailableCell(schemaStats.production);
5304
6353
  lines.push(
5305
6354
  hasProduction ? `| ${schemaName} | ${localCell} | ${ciCell} | ${prodCell ?? "n/a"} |` : `| ${schemaName} | ${localCell} | ${ciCell} |`
5306
6355
  );
5307
6356
  }
5308
6357
  return lines;
5309
6358
  }
5310
- function generateSchemaMatrixSummary(localTotal, ciTotal, prodTotal) {
5311
- const ciSynced = !hasDisplayedStatsDiff(localTotal, ciTotal);
5312
- const prodSynced = prodTotal === null || !hasDisplayedStatsDiff(localTotal, prodTotal);
5313
- if (ciSynced && prodSynced) {
5314
- return `\u{1F4CA} **Local:** ${formatTotalStatsCompact(localTotal)}`;
6359
+ function generateSchemaMatrixSummary(localTotal, ciTotal, prodTotal, hasCiSemanticDiff, hasProdSemanticDiff, localAvailable, ciAvailable, prodAvailable) {
6360
+ if (!localAvailable) {
6361
+ const parts2 = ["\u{1F4CA} **Reference:** unavailable"];
6362
+ if (ciAvailable) parts2.push(`| **CI:** ${formatTotalStatsCompact(ciTotal)}`);
6363
+ else parts2.push("| **CI:** unavailable");
6364
+ if (prodTotal !== null) {
6365
+ parts2.push(
6366
+ prodAvailable ? `| **Prod:** ${formatTotalStatsCompact(prodTotal)}` : "| **Prod:** unavailable"
6367
+ );
6368
+ }
6369
+ return parts2.join(" ");
6370
+ }
6371
+ const ciSynced = ciAvailable && !hasDisplayedStatsDiff(localTotal, ciTotal) && !hasCiSemanticDiff;
6372
+ const prodSynced = prodTotal === null || prodAvailable && !hasDisplayedStatsDiff(localTotal, prodTotal) && !hasProdSemanticDiff;
6373
+ if (ciSynced && prodSynced && ciAvailable && (prodTotal === null || prodAvailable)) {
6374
+ return `\u{1F4CA} **Reference:** ${formatTotalStatsCompact(localTotal)}`;
5315
6375
  }
5316
- const parts = [`\u{1F4CA} **Local:** ${formatTotalStatsCompact(localTotal)}`];
5317
- if (!ciSynced) parts.push(`| **CI:** ${formatTotalStatsCompact(ciTotal)}`);
6376
+ const parts = [`\u{1F4CA} **Reference:** ${formatTotalStatsCompact(localTotal)}`];
6377
+ if (!ciAvailable) parts.push("| **CI:** unavailable");
6378
+ else if (!ciSynced) parts.push(`| **CI:** ${formatTotalStatsCompact(ciTotal)}`);
5318
6379
  if (prodTotal !== null && !prodSynced) {
5319
- parts.push(`| **Prod:** \u{1F4DD} ${formatTotalStatsCompact(prodTotal)}`);
6380
+ parts.push(
6381
+ prodAvailable ? `| **Prod:** \u{1F4DD} ${formatTotalStatsCompact(prodTotal)}` : "| **Prod:** unavailable"
6382
+ );
5320
6383
  }
5321
6384
  return parts.join(" ");
5322
6385
  }
5323
6386
  function generateIndexDiffSection(schemaStats) {
5324
- if (!schemaStats.production) return [];
5325
- const diff = compareIndexLists(schemaStats.local, schemaStats.production);
6387
+ if (!isStatsAvailable(schemaStats.local) || !isStatsAvailable(schemaStats.production)) return [];
6388
+ const productionStats = schemaStats.production;
6389
+ if (!productionStats) return [];
6390
+ const diff = compareIndexLists(schemaStats.local, productionStats);
5326
6391
  if (!hasIndexDiff(diff)) return [];
5327
6392
  const lines = [];
5328
6393
  if (diff.missing.length > 0) {
@@ -5342,11 +6407,88 @@ function generateIndexDiffSection(schemaStats) {
5342
6407
  }
5343
6408
  return lines;
5344
6409
  }
6410
+ function appendAvailabilityNote(lines, label, error, kind) {
6411
+ const scope = kind === "stats" ? "schema stats" : "semantic diff";
6412
+ lines.push(`> \u26AA **${label} ${scope} unavailable:** ${error ?? "unknown error"}`);
6413
+ }
6414
+ function generateAvailabilityNotes(schemaStats) {
6415
+ const lines = [];
6416
+ const appendNotesForEnv = (label, stats) => {
6417
+ if (stats == null) return;
6418
+ if (!isStatsAvailable(stats)) {
6419
+ appendAvailabilityNote(lines, label, stats.error, "stats");
6420
+ return;
6421
+ }
6422
+ if (stats.canonicalSnapshot?.available === false) {
6423
+ appendAvailabilityNote(lines, label, stats.canonicalSnapshot.error, "semantic");
6424
+ }
6425
+ };
6426
+ appendNotesForEnv("Reference", schemaStats.local);
6427
+ appendNotesForEnv("CI", schemaStats.ci);
6428
+ appendNotesForEnv("Prod", schemaStats.production);
6429
+ if (lines.length === 0) return [];
6430
+ return ["", ...lines, ">"];
6431
+ }
6432
+ function describeObject(object) {
6433
+ switch (object.kind) {
6434
+ case "index":
6435
+ return `index \`${object.label}\``;
6436
+ case "policy":
6437
+ return `policy \`${object.label}\``;
6438
+ case "trigger":
6439
+ return `trigger \`${object.label}\``;
6440
+ case "function":
6441
+ return `function \`${object.label}\``;
6442
+ case "table":
6443
+ return `table \`${object.label}\``;
6444
+ }
6445
+ }
6446
+ function appendObjectLines(lines, prefix, objects, label) {
6447
+ if (objects.length === 0) return;
6448
+ lines.push(`> ${prefix} **${label}:**`);
6449
+ for (const object of objects.slice(0, 6)) {
6450
+ lines.push(`> - ${describeObject(object)}`);
6451
+ }
6452
+ if (objects.length > 6) {
6453
+ lines.push(`> - ... \u4ED6${objects.length - 6}\u4EF6`);
6454
+ }
6455
+ lines.push(">");
6456
+ }
6457
+ function appendChangedObjectLines(lines, prefix, pairs, label) {
6458
+ if (pairs.length === 0) return;
6459
+ lines.push(`> ${prefix} **${label}:**`);
6460
+ for (const pair of pairs.slice(0, 6)) {
6461
+ lines.push(`> - ${describeObject(pair.reference)}`);
6462
+ }
6463
+ if (pairs.length > 6) {
6464
+ lines.push(`> - ... \u4ED6${pairs.length - 6}\u4EF6`);
6465
+ }
6466
+ lines.push(">");
6467
+ }
6468
+ function generateSemanticDiffSection(title, prefix, diff) {
6469
+ if (!diff || !hasCanonicalChanges(diff)) return [];
6470
+ const lines = ["", `> ${prefix} **${title} semantic diff:**`, ">"];
6471
+ appendObjectLines(lines, prefix, diff.missing, "missing objects");
6472
+ appendObjectLines(lines, prefix, diff.extra, "extra objects");
6473
+ appendChangedObjectLines(lines, prefix, diff.changed, "changed objects");
6474
+ if (diff.renamed.length > 0) {
6475
+ lines.push(`> ${prefix} **renamed indexes:**`);
6476
+ for (const pair of diff.renamed.slice(0, 6)) {
6477
+ lines.push(`> - \`${pair.reference.label}\` -> \`${pair.target.label}\``);
6478
+ }
6479
+ if (diff.renamed.length > 6) {
6480
+ lines.push(`> - ... \u4ED6${diff.renamed.length - 6}\u4EF6`);
6481
+ }
6482
+ lines.push(">");
6483
+ }
6484
+ return lines;
6485
+ }
5345
6486
  function generateExpectedDriftFootnotes(schemaStats, sortedSchemas, expectedDrift) {
5346
6487
  if (expectedDrift.length === 0 || !schemaStats.production) return [];
6488
+ if (!isStatsAvailable(schemaStats.local) || !isStatsAvailable(schemaStats.production)) return [];
5347
6489
  const allReasons = /* @__PURE__ */ new Set();
5348
6490
  for (const schemaName of sortedSchemas) {
5349
- const localStats = schemaStats.local.schemas[schemaName] ?? emptyStats2();
6491
+ const localStats = schemaStats.local.schemas[schemaName] ?? emptyStats3();
5350
6492
  const prodStats = schemaStats.production.schemas[schemaName];
5351
6493
  if (!prodStats || !hasDisplayedStatsDiff(prodStats, localStats)) continue;
5352
6494
  const fieldDiffs = getFieldDiffs(prodStats, localStats);
@@ -5370,33 +6512,69 @@ function formatSchemaMatrix(schemaStats, gitDiff, expectedDrift = []) {
5370
6512
  const hasGitChanges = gitDiff && gitDiff.filesChanged.length > 0;
5371
6513
  const header = hasGitChanges ? `### \u30B9\u30AD\u30FC\u30DE\u72B6\u6CC1 (+${gitDiff.linesAdded}/-${gitDiff.linesDeleted} PR\u5185)` : "### \u30B9\u30AD\u30FC\u30DE\u72B6\u6CC1";
5372
6514
  const sortedSchemas = getSortedActiveSchemas(schemaStats);
6515
+ const availabilityNotes = generateAvailabilityNotes(schemaStats);
5373
6516
  if (sortedSchemas.length === 0) {
5374
- return [header, "", "_\u30B9\u30AD\u30FC\u30DE\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u304C\u3042\u308A\u307E\u305B\u3093_", ""];
6517
+ return [header, "", ...availabilityNotes, "_\u30B9\u30AD\u30FC\u30DE\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u304C\u3042\u308A\u307E\u305B\u3093_", ""];
5375
6518
  }
5376
6519
  const hasProduction = schemaStats.production !== null;
5377
- const tableHeader = hasProduction ? ["| Schema | \u{1F4C4} Local | \u{1F52C} CI | \u{1F3ED} Prod |", "|--------|:--------:|:-----:|:-------:|"] : ["| Schema | \u{1F4C4} Local | \u{1F52C} CI |", "|--------|:--------:|:-----:|"];
5378
- const rows = generateSchemaTableRows(schemaStats, sortedSchemas, hasProduction, expectedDrift);
6520
+ const canonicalDiffs = buildCanonicalDiffs(schemaStats);
6521
+ const tableHeader = hasProduction ? ["| Schema | \u{1F4C4} Reference | \u{1F52C} CI | \u{1F3ED} Prod |", "|--------|:------------:|:-----:|:-------:|"] : ["| Schema | \u{1F4C4} Reference | \u{1F52C} CI |", "|--------|:------------:|:-----:|"];
6522
+ const rows = generateSchemaTableRows(
6523
+ schemaStats,
6524
+ sortedSchemas,
6525
+ hasProduction,
6526
+ expectedDrift,
6527
+ canonicalDiffs.ciBySchema,
6528
+ canonicalDiffs.productionBySchema
6529
+ );
5379
6530
  const { local, ci, production } = schemaStats;
5380
- const localTotalCell = `**${formatSchemaStatsCompact(local.total, true)}**`;
5381
- const ciTotalCell = formatCiTotalCellWithDiff(ci.total, local.total);
5382
- const prodTotalCell = production ? formatProdTotalCellWithDiff(production.total, local.total) : null;
6531
+ const localTotalCell = isStatsAvailable(local) ? `**${formatSchemaStatsCompact(local.total, true)}**` : "**\u26AA unavailable**";
6532
+ const ciTotalCell = formatCiTotalCellWithDiff(
6533
+ ci.total,
6534
+ local.total,
6535
+ canonicalDiffs.ci ? hasCanonicalChanges(canonicalDiffs.ci) : false,
6536
+ isStatsAvailable(ci),
6537
+ isStatsAvailable(local)
6538
+ );
6539
+ const prodTotalCell = production ? formatProdTotalCellWithDiff(
6540
+ production.total,
6541
+ local.total,
6542
+ canonicalDiffs.production ? hasCanonicalChanges(canonicalDiffs.production) : false,
6543
+ isStatsAvailable(production),
6544
+ isStatsAvailable(local)
6545
+ ) : null;
5383
6546
  const totalRow = hasProduction ? `| **Total** | ${localTotalCell} | ${ciTotalCell} | ${prodTotalCell ?? "n/a"} |` : `| **Total** | ${localTotalCell} | ${ciTotalCell} |`;
5384
- const summary = generateSchemaMatrixSummary(local.total, ci.total, production?.total ?? null);
6547
+ const summary = generateSchemaMatrixSummary(
6548
+ local.total,
6549
+ ci.total,
6550
+ production?.total ?? null,
6551
+ canonicalDiffs.ci ? hasCanonicalChanges(canonicalDiffs.ci) : false,
6552
+ canonicalDiffs.production ? hasCanonicalChanges(canonicalDiffs.production) : false,
6553
+ isStatsAvailable(local),
6554
+ isStatsAvailable(ci),
6555
+ isStatsAvailable(production)
6556
+ );
5385
6557
  const indexDiffSection = generateIndexDiffSection(schemaStats);
5386
6558
  const expectedDriftNotes = generateExpectedDriftFootnotes(
5387
6559
  schemaStats,
5388
6560
  sortedSchemas,
5389
6561
  expectedDrift
5390
6562
  );
6563
+ const semanticDiffSection = [
6564
+ ...generateSemanticDiffSection("CI", "\u26A0\uFE0F", canonicalDiffs.ci),
6565
+ ...generateSemanticDiffSection("Prod", "\u{1F4DD}", canonicalDiffs.production)
6566
+ ];
5391
6567
  return [
5392
6568
  header,
5393
6569
  "",
6570
+ ...availabilityNotes,
5394
6571
  ...tableHeader,
5395
6572
  ...rows,
5396
6573
  totalRow,
5397
6574
  "",
5398
6575
  summary,
5399
6576
  ...indexDiffSection,
6577
+ ...semanticDiffSection,
5400
6578
  ...expectedDriftNotes,
5401
6579
  "",
5402
6580
  "<sub>T=\u30C6\u30FC\u30D6\u30EB F=\u95A2\u6570 P=\u30DD\u30EA\u30B7\u30FC I=\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9 Tr=\u30C8\u30EA\u30AC\u30FC</sub>",
@@ -5406,8 +6584,8 @@ function formatSchemaMatrix(schemaStats, gitDiff, expectedDrift = []) {
5406
6584
 
5407
6585
  // src/commands/ci/machine/formatters/sections/final-comment.ts
5408
6586
  function formatDriftStatus(drift) {
5409
- if (drift == null) {
5410
- return "_n/a_";
6587
+ if (drift == null || !drift.checkExecuted) {
6588
+ return "_\u672A\u691C\u8A3C_";
5411
6589
  }
5412
6590
  return drift.hasDrift ? "**\u274C \u30C9\u30EA\u30D5\u30C8\u691C\u51FA (schemas \u2194 CI DB)**" : "**\u2705 \u30C9\u30EA\u30D5\u30C8\u306A\u3057 (schemas \u2194 CI DB)**";
5413
6591
  }
@@ -5532,21 +6710,6 @@ function classifyPreviewError(error) {
5532
6710
  }
5533
6711
  return { label: "\u672C\u756A\u30D7\u30EC\u30D3\u30E5\u30FC\u5931\u6557", guidance: "" };
5534
6712
  }
5535
- function generateIndexDiffWarning(schemaStats) {
5536
- if (!schemaStats?.local || !schemaStats?.production) return [];
5537
- const indexDiff = compareIndexLists(schemaStats.local, schemaStats.production);
5538
- if (!hasIndexDiff(indexDiff)) return [];
5539
- const diffParts = [];
5540
- if (indexDiff.missing.length > 0) diffParts.push(`${indexDiff.missing.length}\u4EF6\u4E0D\u8DB3`);
5541
- if (indexDiff.extra.length > 0) diffParts.push(`${indexDiff.extra.length}\u4EF6\u4F59\u5206`);
5542
- return [
5543
- `> \u26A0\uFE0F **\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u5DEE\u5206\u691C\u51FA** (${diffParts.join(", ")})`,
5544
- ">",
5545
- "> pg-schema-diff \u3067\u306F\u691C\u51FA\u3055\u308C\u307E\u305B\u3093\u3067\u3057\u305F\u304C\u3001\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u306E\u5DEE\u5206\u304C\u3042\u308A\u307E\u3059\u3002",
5546
- "> \u30B9\u30AD\u30FC\u30DE\u30DE\u30C8\u30EA\u30C3\u30AF\u30B9\u306E\u8A73\u7D30\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
5547
- ""
5548
- ];
5549
- }
5550
6713
  function generatePreviewError(error) {
5551
6714
  const { label, guidance } = classifyPreviewError(error);
5552
6715
  const lines = [`> \u26A0\uFE0F **${label}**`];
@@ -5567,7 +6730,82 @@ function generatePreviewError(error) {
5567
6730
  );
5568
6731
  return lines;
5569
6732
  }
5570
- function generateProductionPreviewSection(prodPreview, schemaDrift, schemaStats) {
6733
+ function generatePreviewChangesDetectedSection(prodPreview, previewOnlyChanges) {
6734
+ const lines = [
6735
+ "> \u{1F536} **\u30B9\u30AD\u30FC\u30DE\u5909\u66F4\u691C\u51FA - \u672C\u756A\u30C7\u30D7\u30ED\u30A4\u304C\u5FC5\u8981\u3067\u3059**",
6736
+ ">",
6737
+ "> \u3053\u306EPR\u3092\u30DE\u30FC\u30B8\u3057\u305F\u5F8C\u3001\u672C\u756A\u30C7\u30D7\u30ED\u30A4\u30EF\u30FC\u30AF\u30D5\u30ED\u30FC\u3092\u5B9F\u884C\u3057\u3066\u30B9\u30AD\u30FC\u30DE\u5909\u66F4\u3092\u9069\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
6738
+ ""
6739
+ ];
6740
+ if (previewOnlyChanges) {
6741
+ lines.push(
6742
+ "> \u88DC\u8DB3: `Prod semantic diff` / count diff / index diff \u3067\u306F\u5DEE\u5206\u304C\u306A\u304F\u3001`production --check` \u306E\u307F\u304C\u5909\u66F4\u3092\u691C\u51FA\u3057\u307E\u3057\u305F\u3002",
6743
+ "> \u3053\u308C\u306F grants / ACL / ownership \u306A\u3069 canonical semantic diff \u306E\u5BFE\u8C61\u5916\u5DEE\u5206\u3067\u3042\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002",
6744
+ ""
6745
+ );
6746
+ }
6747
+ if (prodPreview.planSql) {
6748
+ lines.push(...generatePreviewDetails(prodPreview));
6749
+ }
6750
+ return lines;
6751
+ }
6752
+ function buildComparisonMismatchReasons(schemaStats, expectedDrift) {
6753
+ const signals = getProductionSchemaSignals(null, schemaStats, expectedDrift);
6754
+ const reasons = [];
6755
+ if (signals.hasSemanticChanges && signals.semanticDiff) {
6756
+ const diff = signals.semanticDiff;
6757
+ reasons.push(
6758
+ `semantic diff: changed ${diff.changed.length}, missing ${diff.missing.length}, extra ${diff.extra.length}, renamed ${diff.renamed.length}`
6759
+ );
6760
+ }
6761
+ if (signals.hasIndexChanges && signals.indexDiff) {
6762
+ reasons.push(
6763
+ `index diff: missing ${signals.indexDiff.missing.length}, extra ${signals.indexDiff.extra.length}`
6764
+ );
6765
+ }
6766
+ if (signals.hasUnexpectedCountChanges) {
6767
+ const unexpectedDiffs = signals.countDiffs.filter((diff) => !diff.expected);
6768
+ const formatted = unexpectedDiffs.slice(0, 6).map(
6769
+ (diff) => `${diff.schema}.${diff.field} ${diff.reference} -> ${diff.value} (${diff.delta >= 0 ? "+" : ""}${diff.delta})`
6770
+ );
6771
+ reasons.push(`count diff: ${formatted.join(", ")}${unexpectedDiffs.length > 6 ? ", ..." : ""}`);
6772
+ }
6773
+ return reasons;
6774
+ }
6775
+ function generateComparisonMismatchSection(schemaStats, expectedDrift) {
6776
+ const reasons = buildComparisonMismatchReasons(schemaStats, expectedDrift);
6777
+ const lines = [
6778
+ "> \u{1F536} **\u672C\u756ADB\u3068\u306E\u5DEE\u5206\u691C\u51FA - \u672C\u756A\u30C7\u30D7\u30ED\u30A4\u304C\u5FC5\u8981\u3067\u3059**",
6779
+ ">",
6780
+ "> `runa db apply production --check` \u306F\u5909\u66F4\u306A\u3057\u5224\u5B9A\u3067\u3057\u305F\u304C\u3001Reference \u2194 Prod \u306E\u6BD4\u8F03\u3067\u5DEE\u5206\u3092\u691C\u51FA\u3057\u307E\u3057\u305F\u3002",
6781
+ "> \u30B9\u30AD\u30FC\u30DE\u30DE\u30C8\u30EA\u30C3\u30AF\u30B9\u306E `Prod semantic diff` \u3068\u8A73\u7D30\u30ED\u30B0\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
6782
+ ""
6783
+ ];
6784
+ for (const reason of reasons) {
6785
+ lines.push(`> - ${reason}`);
6786
+ }
6787
+ if (reasons.length > 0) {
6788
+ lines.push("");
6789
+ }
6790
+ return lines;
6791
+ }
6792
+ function generateKnownDriftOnlySection(schemaStats, expectedDrift) {
6793
+ const signals = getProductionSchemaSignals(null, schemaStats, expectedDrift);
6794
+ const lines = [
6795
+ "**\u672C\u756A\u30D7\u30EC\u30D3\u30E5\u30FC**: \u2705 \u672C\u756A\u30C7\u30D7\u30ED\u30A4\u304C\u5FC5\u8981\u306A\u30B9\u30AD\u30FC\u30DE\u5909\u66F4\u306F\u3042\u308A\u307E\u305B\u3093",
6796
+ "",
6797
+ "> \u{1F4CB} **\u65E2\u77E5\u306E\u5DEE\u5206\u306E\u307F\u691C\u51FA**",
6798
+ ">",
6799
+ "> Reference \u2194 Prod \u306B\u306F `expectedDrift` \u3067\u8A31\u5BB9\u6E08\u307F\u306E\u5DEE\u5206\u306E\u307F\u304C\u3042\u308A\u307E\u3059\u3002\u8FFD\u52A0\u306E db apply/deploy \u306F\u4E0D\u8981\u3067\u3059\u3002"
6800
+ ];
6801
+ for (const reason of signals.knownDriftReasons) {
6802
+ lines.push(`> - ${reason}`);
6803
+ }
6804
+ lines.push("");
6805
+ return lines;
6806
+ }
6807
+ function generateProductionPreviewSection(prodPreview, schemaDrift, schemaStats, expectedDrift) {
6808
+ const signals = getProductionSchemaSignals(prodPreview, schemaStats, expectedDrift);
5571
6809
  if (!prodPreview?.executed) {
5572
6810
  if (schemaDrift?.gitDiff?.filesChanged?.length) {
5573
6811
  return [
@@ -5580,20 +6818,15 @@ function generateProductionPreviewSection(prodPreview, schemaDrift, schemaStats)
5580
6818
  return [];
5581
6819
  }
5582
6820
  if (prodPreview.error) return generatePreviewError(prodPreview.error);
5583
- if (prodPreview.hasChanges) {
5584
- const lines = [
5585
- "> \u{1F536} **\u30B9\u30AD\u30FC\u30DE\u5909\u66F4\u691C\u51FA - \u672C\u756A\u30C7\u30D7\u30ED\u30A4\u304C\u5FC5\u8981\u3067\u3059**",
5586
- ">",
5587
- "> \u3053\u306EPR\u3092\u30DE\u30FC\u30B8\u3057\u305F\u5F8C\u3001\u672C\u756A\u30C7\u30D7\u30ED\u30A4\u30EF\u30FC\u30AF\u30D5\u30ED\u30FC\u3092\u5B9F\u884C\u3057\u3066\u30B9\u30AD\u30FC\u30DE\u5909\u66F4\u3092\u9069\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
5588
- ""
5589
- ];
5590
- if (prodPreview.planSql) {
5591
- lines.push(...generatePreviewDetails(prodPreview));
5592
- }
5593
- return lines;
6821
+ if (signals.previewHasChanges) {
6822
+ return generatePreviewChangesDetectedSection(prodPreview, signals.previewOnlyChanges);
6823
+ }
6824
+ if (signals.previewMissedChanges) {
6825
+ return generateComparisonMismatchSection(schemaStats, expectedDrift);
6826
+ }
6827
+ if (signals.hasKnownDriftOnly) {
6828
+ return generateKnownDriftOnlySection(schemaStats, expectedDrift);
5594
6829
  }
5595
- const indexWarning = generateIndexDiffWarning(schemaStats);
5596
- if (indexWarning.length > 0) return indexWarning;
5597
6830
  return ["**\u672C\u756A\u30D7\u30EC\u30D3\u30E5\u30FC**: \u2705 \u672C\u756A\u306B\u9069\u7528\u3059\u308B\u30B9\u30AD\u30FC\u30DE\u5909\u66F4\u306F\u3042\u308A\u307E\u305B\u3093", ""];
5598
6831
  }
5599
6832
  function shouldShowDeployButton(productionPreview, schemaDrift, hasSchemaChanges2) {
@@ -5602,18 +6835,19 @@ function shouldShowDeployButton(productionPreview, schemaDrift, hasSchemaChanges
5602
6835
  if (!productionPreview?.executed && schemaDrift?.gitDiff?.filesChanged?.length) return true;
5603
6836
  return false;
5604
6837
  }
5605
- function generateDeploySection(exitCode, layerResults, gitBranchName, productionPreview, schemaDrift, schemaStats, env) {
6838
+ function generateDeploySection(exitCode, layerResults, gitBranchName, productionPreview, schemaDrift, schemaStats, expectedDrift, env) {
5606
6839
  if (!checkIfDeployable(exitCode, layerResults)) return [];
5607
6840
  const deployWorkflowUrl = env.repository ? `${env.serverUrl}/${env.repository}/actions/workflows/deploy-db.yml` : null;
5608
- const hasPgSchemaDiffChanges = productionPreview?.hasChanges === true;
5609
- const hasIndexChanges = schemaStats?.local && schemaStats?.production ? hasIndexDiff(compareIndexLists(schemaStats.local, schemaStats.production)) : false;
5610
- const hasSchemaChanges2 = hasPgSchemaDiffChanges || hasIndexChanges;
6841
+ const signals = getProductionSchemaSignals(productionPreview, schemaStats, expectedDrift);
6842
+ const hasSchemaChanges2 = signals.requiresDeploy;
5611
6843
  const headerEmoji = hasSchemaChanges2 ? "\u{1F6A8}" : "\u{1F680}";
5612
6844
  const headerText = hasSchemaChanges2 ? "\u672C\u756A\u30C7\u30D7\u30ED\u30A4 (\u30B9\u30AD\u30FC\u30DE\u5909\u66F4\u3042\u308A!)" : "\u672C\u756A\u30C7\u30D7\u30ED\u30A4";
5613
6845
  const lines = ["---", "", `### ${headerEmoji} ${headerText}`, ""];
5614
6846
  if (exitCode !== 0)
5615
6847
  lines.push("> **\u6CE8**: Layer 4 (E2E) \u306F\u5931\u6557\u3057\u307E\u3057\u305F\u304C\u3001\u30B9\u30AD\u30FC\u30DE\u5909\u66F4\u306F\u30C7\u30D7\u30ED\u30A4\u53EF\u80FD\u3067\u3059\u3002", "");
5616
- lines.push(...generateProductionPreviewSection(productionPreview, schemaDrift, schemaStats));
6848
+ lines.push(
6849
+ ...generateProductionPreviewSection(productionPreview, schemaDrift, schemaStats, expectedDrift)
6850
+ );
5617
6851
  const showButton = shouldShowDeployButton(productionPreview, schemaDrift, hasSchemaChanges2);
5618
6852
  if (deployWorkflowUrl && showButton) {
5619
6853
  const buttonText = hasSchemaChanges2 ? "\u26A1 \u30B9\u30AD\u30FC\u30DE\u5909\u66F4\u3092\u672C\u756A\u306B\u30C7\u30D7\u30ED\u30A4" : "\u25B6\uFE0F \u672C\u756A\u306B\u30C7\u30D7\u30ED\u30A4";
@@ -5653,6 +6887,7 @@ function generateIntermediateCommentBody(input) {
5653
6887
  productionPreview,
5654
6888
  schemaDrift,
5655
6889
  input.schemaStats ?? null,
6890
+ input.expectedDrift ?? [],
5656
6891
  env
5657
6892
  ),
5658
6893
  "<sub>\u{1F916} RUNA CI \u751F\u6210 (\u4E2D\u9593\u66F4\u65B0)</sub>"
@@ -5688,6 +6923,7 @@ function generateCommentBody(input) {
5688
6923
  input.productionPreview,
5689
6924
  schemaDrift,
5690
6925
  input.schemaStats ?? null,
6926
+ input.expectedDrift ?? [],
5691
6927
  env
5692
6928
  ),
5693
6929
  "<sub>\u{1F916} RUNA CI \u751F\u6210</sub>"
@@ -5737,7 +6973,7 @@ function formatSyncSchemaDetail(schemaDrift) {
5737
6973
  if (!hasGitChanges) {
5738
6974
  const hasSqlContent = schemaDrift.beforeSql && schemaDrift.beforeSql.trim().length > 0;
5739
6975
  if (hasSqlContent) {
5740
- return "ci.sql + pgTAP + roles";
6976
+ return "ci.sql + prerequisite seeds";
5741
6977
  }
5742
6978
  return "SQL\u30D5\u30A1\u30A4\u30EB\u5909\u66F4\u306A\u3057";
5743
6979
  }
@@ -5777,7 +7013,7 @@ function formatRunTestsDetail(layerResults) {
5777
7013
  }
5778
7014
  var stepDetailHandlers = {
5779
7015
  syncSchema: (_status, schemaDrift) => formatSyncSchemaDetail(schemaDrift),
5780
- applySeeds: () => "ci.sql + pgTAP + roles",
7016
+ applySeeds: () => "ci.sql + prerequisite seeds",
5781
7017
  staticChecks: (status) => status === "passed" ? "\u578B\u30C1\u30A7\u30C3\u30AF \u2713 lint \u2713" : "\u578B\u30C1\u30A7\u30C3\u30AF \u2717 \u307E\u305F\u306F lint \u2717",
5782
7018
  build: (status) => status === "passed" ? "\u30A2\u30D7\u30EA\u30D3\u30EB\u30C9\u5B8C\u4E86, Playwright\u6E96\u5099\u5B8C\u4E86" : "\u30D3\u30EB\u30C9\u5931\u6557",
5783
7019
  runTests: (_status, _schemaDrift, layerResults) => formatRunTestsDetail(layerResults)
@@ -6205,7 +7441,7 @@ function createInitialContext(input) {
6205
7441
  function createOutput(context) {
6206
7442
  return {
6207
7443
  mode: context.mode,
6208
- status: context.error ? "failure" : "success",
7444
+ status: context.exitCode === 0 && !context.error ? "success" : "failure",
6209
7445
  repoKind: context.repoKind,
6210
7446
  steps: {},
6211
7447
  // Populated from summary
@@ -6410,7 +7646,7 @@ function createBuildAndPlaywrightInput(context) {
6410
7646
  enablePublicE2EFlag: true
6411
7647
  }),
6412
7648
  isCI: context.input.isCI ?? false,
6413
- skipPlaywright: context.selectedLayers.includes(4) ? false : true
7649
+ skipPlaywright: !context.selectedLayers.includes(4)
6414
7650
  };
6415
7651
  }
6416
7652
  function createAppStartInput(context) {
@@ -6774,10 +8010,17 @@ var ciMachine = setup({
6774
8010
  databaseUrl: context.supabase?.databaseUrlRaw ?? "",
6775
8011
  mode: context.mode
6776
8012
  }),
6777
- onDone: {
6778
- actions: assign({ seedsApplied: ({ event }) => event.output.applied }),
6779
- target: "productionPreview"
6780
- },
8013
+ onDone: [
8014
+ {
8015
+ guard: "isCiLocalMode",
8016
+ actions: assign({ seedsApplied: ({ event }) => event.output.applied }),
8017
+ target: "productionPreview"
8018
+ },
8019
+ {
8020
+ actions: assign({ seedsApplied: ({ event }) => event.output.applied }),
8021
+ target: "postSeedPr"
8022
+ }
8023
+ ],
6781
8024
  onError: {
6782
8025
  // Seeds failure is CRITICAL - blocks CI (Layer 2/3 tests depend on seeds)
6783
8026
  target: "failed",
@@ -6789,6 +8032,371 @@ var ciMachine = setup({
6789
8032
  }
6790
8033
  },
6791
8034
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8035
+ // Post-Seed PR Phase
8036
+ // CI PR path only: run observability work in parallel with the critical build/test path
8037
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8038
+ postSeedPr: {
8039
+ type: "parallel",
8040
+ meta: {
8041
+ e2e: {
8042
+ observable: "log",
8043
+ assertions: [{ type: "log", contains: "Database roles configured" }]
8044
+ }
8045
+ },
8046
+ states: {
8047
+ execution: {
8048
+ initial: "setupRoles",
8049
+ states: {
8050
+ setupRoles: {
8051
+ invoke: {
8052
+ src: "setupRoles",
8053
+ input: ({ context }) => createSetupRolesInput(context),
8054
+ onDone: {
8055
+ target: "staticChecks",
8056
+ actions: assign({
8057
+ rolesSetup: true,
8058
+ supabase: ({ context, event }) => mergeSetupRolesSupabase(context, event.output.appDatabaseUrl)
8059
+ })
8060
+ },
8061
+ onError: {
8062
+ target: "staticChecks",
8063
+ actions: assign({ rolesSetup: false })
8064
+ }
8065
+ }
8066
+ },
8067
+ staticChecks: {
8068
+ always: [{ guard: "shouldSkipStaticChecks", target: "buildAndPlaywright" }],
8069
+ invoke: {
8070
+ src: "staticChecks",
8071
+ input: ({ context }) => createStaticChecksInput(context),
8072
+ onDone: [
8073
+ {
8074
+ guard: ({ event }) => !event.output.passed,
8075
+ target: "failed",
8076
+ actions: assign({
8077
+ staticChecksPassed: false,
8078
+ error: ({ event }) => event.output.error ?? "Static checks failed"
8079
+ })
8080
+ },
8081
+ {
8082
+ target: "buildAndPlaywright",
8083
+ actions: assign({ staticChecksPassed: true })
8084
+ }
8085
+ ],
8086
+ onError: {
8087
+ target: "failed",
8088
+ actions: assign({
8089
+ staticChecksPassed: false,
8090
+ error: ({ event }) => extractErrorMessage2(event, "Static checks failed")
8091
+ })
8092
+ }
8093
+ }
8094
+ },
8095
+ buildAndPlaywright: {
8096
+ always: [{ guard: "shouldSkipBuild", target: "appStart" }],
8097
+ invoke: {
8098
+ src: "buildAndPlaywright",
8099
+ input: ({ context }) => ({
8100
+ ...createBuildAndPlaywrightInput(context),
8101
+ skipPlaywright: shouldSkipPlaywrightInstall(context)
8102
+ }),
8103
+ onDone: [
8104
+ {
8105
+ guard: ({ event }) => !event.output.buildPassed,
8106
+ target: "failed",
8107
+ actions: assign({
8108
+ appBuildPassed: false,
8109
+ playwrightInstalled: ({ event }) => event.output.playwrightInstalled,
8110
+ manifestGenerated: ({ event }) => event.output.manifestGenerated,
8111
+ error: ({ event }) => event.output.buildError ?? "App build failed"
8112
+ })
8113
+ },
8114
+ {
8115
+ target: "appStart",
8116
+ actions: assign({
8117
+ appBuildPassed: true,
8118
+ playwrightInstalled: ({ event }) => event.output.playwrightInstalled,
8119
+ manifestGenerated: ({ event }) => event.output.manifestGenerated
8120
+ })
8121
+ }
8122
+ ],
8123
+ onError: {
8124
+ target: "failed",
8125
+ actions: assign({
8126
+ appBuildPassed: false,
8127
+ error: ({ event }) => extractErrorMessage2(event, "App build failed")
8128
+ })
8129
+ }
8130
+ }
8131
+ },
8132
+ appStart: {
8133
+ always: [{ guard: "shouldSkipAppStart", target: "capabilities" }],
8134
+ invoke: {
8135
+ src: "appStart",
8136
+ input: ({ context }) => createAppStartInput(context),
8137
+ onDone: [
8138
+ {
8139
+ guard: ({ event }) => !event.output.started,
8140
+ target: "failed",
8141
+ actions: assign({
8142
+ appStarted: false,
8143
+ error: ({ event }) => event.output.error ?? "App start failed"
8144
+ })
8145
+ },
8146
+ {
8147
+ target: "capabilities",
8148
+ actions: assign({
8149
+ appStarted: true,
8150
+ appPid: ({ event }) => event.output.pid ?? null,
8151
+ baseUrl: ({ event }) => event.output.baseUrl ?? null
8152
+ })
8153
+ }
8154
+ ],
8155
+ onError: {
8156
+ target: "failed",
8157
+ actions: assign({
8158
+ appStarted: false,
8159
+ error: ({ event }) => extractErrorMessage2(event, "App start failed")
8160
+ })
8161
+ }
8162
+ }
8163
+ },
8164
+ capabilities: {
8165
+ invoke: {
8166
+ src: "capabilities",
8167
+ input: ({ context }) => createCapabilitiesInput(context),
8168
+ onDone: {
8169
+ target: "runCoreTests",
8170
+ actions: assign({
8171
+ capabilities: ({ event }) => event.output.detected?.capabilities ?? [],
8172
+ selectedLayers: ({ context, event }) => filterRunnableLayers(context.selectedLayers, event.output.layerContent),
8173
+ layerSkipReasons: ({ event }) => extractLayerSkipReasons(event.output.layerContent)
8174
+ })
8175
+ },
8176
+ onError: {
8177
+ target: "runCoreTests"
8178
+ }
8179
+ }
8180
+ },
8181
+ runCoreTests: {
8182
+ invoke: {
8183
+ src: "runLayers",
8184
+ input: ({ context }) => createRunCoreTestsInput(context),
8185
+ onDone: [
8186
+ {
8187
+ guard: ({ event }) => {
8188
+ const failedLayers = event.output.failedLayers ?? [];
8189
+ return failedLayers.length > 0;
8190
+ },
8191
+ target: "coreTestsFailed",
8192
+ actions: assign({
8193
+ testsRun: true,
8194
+ layerResults: ({ event }) => deriveCoreLayerResults(event.output)
8195
+ })
8196
+ },
8197
+ {
8198
+ target: "coreTestsComplete",
8199
+ actions: assign({
8200
+ testsRun: true,
8201
+ layerResults: ({ context, event }) => {
8202
+ const nextResults = convertLayerResults(event.output.results);
8203
+ return mergeLayerResults(context.layerResults, nextResults);
8204
+ }
8205
+ })
8206
+ }
8207
+ ],
8208
+ onError: {
8209
+ target: "coreTestsFailed",
8210
+ actions: assign({
8211
+ testsRun: true,
8212
+ error: ({ event }) => extractErrorMessage2(event, "Core tests failed")
8213
+ })
8214
+ }
8215
+ }
8216
+ },
8217
+ coreTestsComplete: {
8218
+ always: [
8219
+ { guard: ({ context }) => hasE2ELayer(context.selectedLayers), target: "e2ePhase" },
8220
+ {
8221
+ target: "done",
8222
+ actions: assign({
8223
+ exitCode: ({ context }) => computeExitCodeFromLayerResults(context.layerResults)
8224
+ })
8225
+ }
8226
+ ]
8227
+ },
8228
+ coreTestsFailed: {
8229
+ entry: assign({
8230
+ exitCode: ({ context }) => {
8231
+ if (Object.keys(context.layerResults).length === 0) {
8232
+ return 1;
8233
+ }
8234
+ return computeExitCodeFromLayerResults(context.layerResults);
8235
+ }
8236
+ }),
8237
+ always: [{ target: "#ci.finalize" }]
8238
+ },
8239
+ e2ePhase: {
8240
+ type: "parallel",
8241
+ states: {
8242
+ intermediateComment: {
8243
+ initial: "checking",
8244
+ states: {
8245
+ checking: {
8246
+ always: [
8247
+ {
8248
+ guard: ({ context }) => !context.prContext?.prNumber,
8249
+ target: "done"
8250
+ },
8251
+ {
8252
+ guard: ({ context }) => !shouldPostGitHubComment(context),
8253
+ target: "done"
8254
+ },
8255
+ { target: "posting" }
8256
+ ]
8257
+ },
8258
+ posting: {
8259
+ invoke: {
8260
+ src: "upsertComment",
8261
+ input: ({ context }) => createIntermediateCommentRequest(context),
8262
+ onDone: "done",
8263
+ onError: "done"
8264
+ }
8265
+ },
8266
+ done: { type: "final" }
8267
+ }
8268
+ },
8269
+ e2eTests: {
8270
+ initial: "running",
8271
+ states: {
8272
+ running: {
8273
+ invoke: {
8274
+ src: "runLayers",
8275
+ input: ({ context }) => createE2ERunLayersInput(context),
8276
+ onDone: {
8277
+ target: "done",
8278
+ actions: assign({
8279
+ layerResults: ({ context, event }) => {
8280
+ const e2eResults = convertLayerResults(event.output.results);
8281
+ return mergeLayerResults(context.layerResults, e2eResults);
8282
+ }
8283
+ })
8284
+ },
8285
+ onError: {
8286
+ target: "done",
8287
+ actions: assign({
8288
+ layerResults: ({ context }) => ({
8289
+ ...context.layerResults,
8290
+ [E2E_LAYER]: { status: "failed", exitCode: 1 }
8291
+ })
8292
+ })
8293
+ }
8294
+ }
8295
+ },
8296
+ done: { type: "final" }
8297
+ }
8298
+ }
8299
+ },
8300
+ onDone: [
8301
+ {
8302
+ guard: ({ context }) => Object.values(context.layerResults).some(
8303
+ (result) => result.status === "failed"
8304
+ ),
8305
+ target: "failed",
8306
+ actions: assign({
8307
+ exitCode: ({ context }) => computeExitCodeFromLayerResults(context.layerResults)
8308
+ })
8309
+ },
8310
+ {
8311
+ target: "done",
8312
+ actions: assign({
8313
+ exitCode: ({ context }) => computeExitCodeFromLayerResults(context.layerResults)
8314
+ })
8315
+ }
8316
+ ]
8317
+ },
8318
+ failed: {
8319
+ always: [
8320
+ {
8321
+ target: "#ci.finalize",
8322
+ actions: assign({
8323
+ exitCode: ({ context }) => context.exitCode !== 0 ? context.exitCode : 1
8324
+ })
8325
+ }
8326
+ ]
8327
+ },
8328
+ done: {
8329
+ type: "final"
8330
+ }
8331
+ }
8332
+ },
8333
+ observability: {
8334
+ initial: "productionPreview",
8335
+ states: {
8336
+ productionPreview: {
8337
+ invoke: {
8338
+ src: "productionPreview",
8339
+ input: ({ context }) => ({
8340
+ repoRoot: assertRepoRoot(context),
8341
+ tmpDir: assertTmpDir(context),
8342
+ shouldExecute: Boolean(context.input.productionDatabaseUrl?.trim())
8343
+ }),
8344
+ onDone: {
8345
+ target: "collectSchemaStats",
8346
+ actions: assign({
8347
+ productionPreview: ({ event }) => event.output.preview
8348
+ })
8349
+ },
8350
+ onError: {
8351
+ target: "collectSchemaStats",
8352
+ actions: assign({
8353
+ productionPreview: ({ event }) => ({
8354
+ executed: true,
8355
+ planSql: null,
8356
+ hazards: [],
8357
+ hasChanges: false,
8358
+ error: event.error instanceof Error ? event.error.message : "Production preview failed"
8359
+ })
8360
+ })
8361
+ }
8362
+ }
8363
+ },
8364
+ collectSchemaStats: {
8365
+ invoke: {
8366
+ src: "collectSchemaStats",
8367
+ input: ({ context }) => ({
8368
+ repoRoot: context.repoRoot ?? process.cwd(),
8369
+ referenceDbUrl: context.supabase?.databaseUrlRaw ?? null,
8370
+ ciDbUrl: context.supabase?.databaseUrlRaw ?? null,
8371
+ referenceStrategy: shouldReuseCiReferenceStats(context) ? "reuse-ci" : "rebuild",
8372
+ queryProduction: true,
8373
+ tmpDir: context.tmpDir ?? process.cwd()
8374
+ }),
8375
+ onDone: {
8376
+ target: "done",
8377
+ actions: assign({
8378
+ schemaStats: ({ event }) => event.output.schemaStats
8379
+ })
8380
+ },
8381
+ onError: {
8382
+ target: "done",
8383
+ actions: assign({
8384
+ schemaStats: () => null
8385
+ })
8386
+ }
8387
+ }
8388
+ },
8389
+ done: {
8390
+ type: "final"
8391
+ }
8392
+ }
8393
+ }
8394
+ },
8395
+ onDone: {
8396
+ target: "finalize"
8397
+ }
8398
+ },
8399
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6792
8400
  // Production Preview (ci-pr modes only, when schema changes exist)
6793
8401
  // Runs dry-run against production to show what SQL would be applied
6794
8402
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -6845,11 +8453,12 @@ var ciMachine = setup({
6845
8453
  invoke: {
6846
8454
  src: "collectSchemaStats",
6847
8455
  input: ({ context }) => ({
6848
- // In CI mode, local and CI both use the Docker Supabase
6849
- localDbUrl: context.supabase?.databaseUrlRaw ?? null,
8456
+ repoRoot: context.repoRoot ?? process.cwd(),
8457
+ referenceDbUrl: context.supabase?.databaseUrlRaw ?? null,
6850
8458
  ciDbUrl: context.supabase?.databaseUrlRaw ?? null,
6851
- // Query production only in ci-pr modes
6852
- queryProduction: context.mode !== "ci-local"
8459
+ referenceStrategy: shouldReuseCiReferenceStats(context) ? "reuse-ci" : "rebuild",
8460
+ queryProduction: context.mode !== "ci-local",
8461
+ tmpDir: context.tmpDir ?? process.cwd()
6853
8462
  }),
6854
8463
  onDone: {
6855
8464
  target: "decidePath",
@@ -7383,6 +8992,19 @@ var STATE_TO_STEP = {
7383
8992
  pullProduction: "syncSchema",
7384
8993
  syncSchema: "syncSchema",
7385
8994
  applySeeds: "applySeeds",
8995
+ postSeedPr: "applySeeds",
8996
+ "postSeedPr.execution.setupRoles": "applySeeds",
8997
+ "postSeedPr.execution.staticChecks": "staticChecks",
8998
+ "postSeedPr.execution.buildAndPlaywright": "build",
8999
+ "postSeedPr.execution.appStart": "build",
9000
+ "postSeedPr.execution.capabilities": "build",
9001
+ "postSeedPr.execution.runCoreTests": "runTests",
9002
+ "postSeedPr.execution.coreTestsComplete": "runTests",
9003
+ "postSeedPr.execution.coreTestsFailed": "runTests",
9004
+ "postSeedPr.execution.e2ePhase": "runTests",
9005
+ "postSeedPr.execution.done": "runTests",
9006
+ "postSeedPr.observability.productionPreview": "applySeeds",
9007
+ "postSeedPr.observability.collectSchemaStats": "applySeeds",
7386
9008
  decidePath: "applySeeds",
7387
9009
  installPgTap: "applySeeds",
7388
9010
  setupRoles: "applySeeds",
@@ -7420,6 +9042,17 @@ var STEP_ORDER = [
7420
9042
  "runTests",
7421
9043
  "finalize"
7422
9044
  ];
9045
+ function resolveStepForState(state) {
9046
+ let cursor = state;
9047
+ while (cursor.length > 0) {
9048
+ const step = STATE_TO_STEP[cursor];
9049
+ if (step) return step;
9050
+ const nextDot = cursor.lastIndexOf(".");
9051
+ if (nextDot === -1) break;
9052
+ cursor = cursor.slice(0, nextDot);
9053
+ }
9054
+ return void 0;
9055
+ }
7423
9056
  function getCompletedSteps(currentStep) {
7424
9057
  const currentIndex = STEP_ORDER.indexOf(currentStep);
7425
9058
  if (currentIndex <= 0) return [];
@@ -7508,8 +9141,8 @@ function determineFailedStep(context) {
7508
9141
  function handleProgressCommentUpdate(snapshot, prevState) {
7509
9142
  const currentState = getStateName(snapshot);
7510
9143
  const context = snapshot.context;
7511
- const currentStep = STATE_TO_STEP[currentState];
7512
- const prevStep = STATE_TO_STEP[prevState];
9144
+ const currentStep = resolveStepForState(currentState);
9145
+ const prevStep = resolveStepForState(prevState);
7513
9146
  if (!currentStep || currentStep === prevStep) return;
7514
9147
  recordStepTiming(currentStep);
7515
9148
  if (currentStep === "finalize") {
@@ -7704,16 +9337,13 @@ var stateLogHandlers = {
7704
9337
  logger.info(`Running: ${ctx.selectedLayers.map((l) => `test layer${l}`).join(", ")}`);
7705
9338
  }
7706
9339
  };
7707
- stateLogHandlers["setup.resolving"] = stateLogHandlers.setup;
7708
- stateLogHandlers["setup.local"] = stateLogHandlers.setup;
7709
- function handleStateChange(snapshot, prevState, logger) {
7710
- const state = getStateName(snapshot);
7711
- if (state === prevState) return;
7712
- const handler = stateLogHandlers[state];
7713
- if (handler) {
7714
- handler(snapshot.context, logger);
7715
- }
7716
- }
9340
+ var handleStateChange = createMachineStateChangeLogger({
9341
+ getState: getStateName,
9342
+ getContext: (snapshot) => snapshot.context,
9343
+ handlers: stateLogHandlers,
9344
+ useParentState: true,
9345
+ handlerArgumentOrder: "context-first"
9346
+ });
7717
9347
  function printSummary(logger, output) {
7718
9348
  endCurrentGroup();
7719
9349
  logSection2("Summary");
@@ -8052,6 +9682,7 @@ function logPlan2(steps) {
8052
9682
  console.log("");
8053
9683
  }
8054
9684
  }
9685
+ var CI_PR_UNKNOWN_STATE_LOG_SKIP = /* @__PURE__ */ new Set(["decidePath", "done", "failed"]);
8055
9686
  var stateLogHandlers2 = {
8056
9687
  // ─── Setup Phase ───────────────────────────────────────────
8057
9688
  setup: (_ctx, _logger) => {
@@ -8078,6 +9709,75 @@ var stateLogHandlers2 = {
8078
9709
  logSection3("Database: Apply Seeds (db seed)");
8079
9710
  logger.info("Running: pnpm exec runa db seed ci --auto-approve");
8080
9711
  },
9712
+ "postSeedPr.execution.setupRoles": (ctx, logger) => {
9713
+ if (ctx.seedsApplied) {
9714
+ logger.success("Seeds applied successfully");
9715
+ }
9716
+ logSection3("Database: Setup Roles (db:setup-roles)");
9717
+ logger.info("Running: pnpm db:setup-roles");
9718
+ },
9719
+ "postSeedPr.observability.productionPreview": (ctx, logger) => {
9720
+ if (ctx.seedsApplied) {
9721
+ logger.success("Seeds applied successfully");
9722
+ }
9723
+ logSection3("Database: Production Preview (dry-run)");
9724
+ logger.info("Running: pnpm exec runa db apply production --check");
9725
+ },
9726
+ "postSeedPr.observability.collectSchemaStats": (ctx, logger) => {
9727
+ if (ctx.productionPreview?.executed) {
9728
+ if (ctx.productionPreview.hasChanges) {
9729
+ logger.info("Production preview: schema changes detected");
9730
+ } else {
9731
+ logger.success("Production preview: no schema changes");
9732
+ }
9733
+ }
9734
+ logSection3("Database: Collect Schema Statistics");
9735
+ logger.info("Comparing Local/CI/Production schemas...");
9736
+ },
9737
+ "postSeedPr.execution.staticChecks": (ctx, logger) => {
9738
+ if (ctx.rolesSetup) {
9739
+ logger.success("Database roles configured");
9740
+ }
9741
+ logSection3("Static Checks: type-check + lint (parallel)");
9742
+ logger.info("Running: pnpm type-check || pnpm lint");
9743
+ },
9744
+ "postSeedPr.execution.buildAndPlaywright": (ctx, logger) => {
9745
+ if (ctx.staticChecksPassed) {
9746
+ logger.success("Static checks passed");
9747
+ }
9748
+ logSection3("Build: App Build + Playwright Install (parallel)");
9749
+ logger.info("Running: pnpm build || pnpm exec playwright install chromium");
9750
+ },
9751
+ "postSeedPr.execution.appStart": (ctx, logger) => {
9752
+ if (ctx.appBuildPassed) {
9753
+ logger.success("App build completed");
9754
+ }
9755
+ if (ctx.manifestGenerated) {
9756
+ logger.success("Manifest generated (Layer 3 ready)");
9757
+ } else if (ctx.manifestGenerated === false) {
9758
+ logger.warn("Manifest generation failed (Layer 3 may be skipped)");
9759
+ }
9760
+ if (ctx.playwrightInstalled) {
9761
+ logger.success("Playwright browsers installed");
9762
+ }
9763
+ logSection3("Start Application");
9764
+ logger.info(`Starting app on port ${ctx.app?.port ?? 3e3}...`);
9765
+ },
9766
+ "postSeedPr.execution.capabilities": (ctx, logger) => {
9767
+ if (ctx.appStarted) {
9768
+ logger.success(`App started at ${ctx.baseUrl ?? "http://localhost:3000"}`);
9769
+ }
9770
+ logSection3("Detect Test Capabilities");
9771
+ },
9772
+ "postSeedPr.execution.runCoreTests": (ctx, logger) => {
9773
+ const coreLayers = ctx.selectedLayers.filter((l) => l !== 4);
9774
+ logSection3(`Core Tests: Layer ${coreLayers.join(", ")}`);
9775
+ logger.info(`Running core layers: ${coreLayers.map((l) => `test layer${l}`).join(", ")}`);
9776
+ },
9777
+ "postSeedPr.execution.e2ePhase": (_ctx, logger) => {
9778
+ logSection3("E2E Phase: Layer 4 + Intermediate Comment");
9779
+ logger.info("Running Layer 4 E2E tests (parallel with intermediate PR comment)");
9780
+ },
8081
9781
  productionPreview: (ctx, logger) => {
8082
9782
  if (ctx.seedsApplied) {
8083
9783
  logger.success("Seeds applied successfully");
@@ -8186,18 +9886,18 @@ var stateLogHandlers2 = {
8186
9886
  logSection3("Finalize: Post GitHub Comment");
8187
9887
  }
8188
9888
  };
8189
- stateLogHandlers2["setup.resolving"] = stateLogHandlers2.setup;
8190
- stateLogHandlers2["finalize.writeSummary"] = stateLogHandlers2.finalize;
8191
- function handleStateChange2(snapshot, prevState, logger) {
8192
- const state = getStateName(snapshot);
8193
- if (state === prevState) return;
8194
- const handler = stateLogHandlers2[state];
8195
- if (handler) {
8196
- handler(snapshot.context, logger);
8197
- } else if (cliOutputFormat !== "json" && state !== "decidePath" && state !== "done" && state !== "failed") {
8198
- logger.info(`State: ${state}`);
9889
+ var handleStateChange2 = createMachineStateChangeLogger({
9890
+ getState: getStateName,
9891
+ getContext: (snapshot) => snapshot.context,
9892
+ handlers: stateLogHandlers2,
9893
+ useParentState: true,
9894
+ handlerArgumentOrder: "context-first",
9895
+ onUnknownState: (state, logger) => {
9896
+ if (cliOutputFormat !== "json" && !CI_PR_UNKNOWN_STATE_LOG_SKIP.has(state)) {
9897
+ logger.info(`State: ${state}`);
9898
+ }
8199
9899
  }
8200
- }
9900
+ });
8201
9901
  function printSummary2(logger, output) {
8202
9902
  logSection3("Summary");
8203
9903
  logger.info(`Mode: ${output.mode}`);
@@ -8284,7 +9984,7 @@ async function runCiPrCommand(options) {
8284
9984
  { id: "setup", description: "Start local Supabase instance" },
8285
9985
  {
8286
9986
  id: "db",
8287
- description: "db apply \u2192 db seed \u2192 production preview \u2192 schema stats \u2192 db:setup-roles"
9987
+ description: "db apply \u2192 db seed \u2192 (production preview \u2225 schema stats \u2225 db:setup-roles)"
8288
9988
  },
8289
9989
  ...skipStaticChecks ? [] : [{ id: "static", description: "pnpm type-check + pnpm lint (parallel)" }],
8290
9990
  ...skipBuild ? [] : [{ id: "build", description: "pnpm build \u2192 manifest:generate + playwright install" }],