hecks 3.0.5 → 3.1.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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +24 -12
  3. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda.rb +1 -1
  5. data/lib/hecks/adapters/driven/tenant_provisioner.rb +2 -2
  6. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +59 -32
  7. data/lib/hecks/bluebook/hexagon.rb +45 -25
  8. data/lib/hecks/bluebook/model_check.rb +7 -7
  9. data/lib/hecks/chapters.rb +32 -3
  10. data/lib/hecks/cli/check_era.rb +1 -1
  11. data/lib/hecks/cli/deploy_template_diff.rb +1 -1
  12. data/lib/hecks/cli/doc_coverage.rb +1 -1
  13. data/lib/hecks/cli/domain_stub.rb +128 -0
  14. data/lib/hecks/cli/domain_writer.rb +39 -0
  15. data/lib/hecks/cli/interview_agent.rb +59 -0
  16. data/lib/hecks/cli/interview_draft.rb +246 -0
  17. data/lib/hecks/cli/interview_run.rb +87 -0
  18. data/lib/hecks/cli/interview_session.rb +299 -0
  19. data/lib/hecks/cli/model_check.rb +1 -1
  20. data/lib/hecks/cli/project_cli.rb +51 -29
  21. data/lib/hecks/cli/reference.rb +2 -1
  22. data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +1 -1
  23. data/lib/hecks/cli/seed_semantics_corpus.rb +3 -3
  24. data/lib/hecks/cli/spec_example.rb +1 -1
  25. data/lib/hecks/cli/stress_concurrency_specs.rb +14 -5
  26. data/lib/hecks/corpus.rb +12 -8
  27. data/lib/hecks/deploy/bluebook/deploy.bluebook +85 -12
  28. data/lib/hecks/deploy/oidc.json +5 -0
  29. data/lib/hecks/doc/reference.rb +4 -3
  30. data/lib/hecks/doors/cli_door.rb +13 -13
  31. data/lib/hecks/doors/cli_runner.rb +53 -33
  32. data/lib/hecks/doors/launcher_options.rb +5 -5
  33. data/lib/hecks/doors/mcp_door.rb +40 -18
  34. data/lib/hecks/doors/mcp_door_scope.rb +244 -24
  35. data/lib/hecks/doors/usage_cache.rb +123 -0
  36. data/lib/hecks/doors.rb +1 -1
  37. data/lib/hecks/embryonaut_bluebook.rb +2 -2
  38. data/lib/hecks/forms/banking_presentation.rb +1 -1
  39. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -1
  40. data/lib/hecks/framework/bluebook/console_settings.bluebook +1 -1
  41. data/lib/hecks/framework/bluebook/framework.hecksagon +4 -4
  42. data/lib/hecks/framework.rb +1 -1
  43. data/lib/hecks/fuzzing/combination_miner.rb +1 -1
  44. data/lib/hecks/fuzzing/concurrent_dispatch.rb +1 -1
  45. data/lib/hecks/fuzzing/differential.rb +4 -2
  46. data/lib/hecks/fuzzing/era_boundary.rb +1 -1
  47. data/lib/hecks/fuzzing/form_census.rb +2 -2
  48. data/lib/hecks/fuzzing/generated_domain_check.rb +1 -1
  49. data/lib/hecks/fuzzing/isolated_boot.rb +10 -3
  50. data/lib/hecks/fuzzing/persistence_parity.rb +2 -2
  51. data/lib/hecks/fuzzing/target_capabilities.rb +3 -3
  52. data/lib/hecks/gate/stages.yml +5 -5
  53. data/lib/hecks/grammar/grammar.hecksagon +2 -2
  54. data/lib/hecks/hecks/adapters/deploy_toolchain.rb +4 -4
  55. data/lib/hecks/hecks/adapters/finding_github.adapter +7 -0
  56. data/lib/hecks/hecks/adapters/finding_github.rb +114 -0
  57. data/lib/hecks/hecks/adapters/host_http.rb +4 -3
  58. data/lib/hecks/hecks/adapters/local_files.rb +59 -0
  59. data/lib/hecks/hecks/adapters/site_toolchain.adapter +7 -0
  60. data/lib/hecks/hecks/adapters/site_toolchain.rb +54 -0
  61. data/lib/hecks/hecks/adapters/terminal.rb +15 -0
  62. data/lib/hecks/hecks/codebase.bluebook +4 -3
  63. data/lib/hecks/hecks/context_map.hecksagon +2 -2
  64. data/lib/hecks/hecks/custodian.bluebook +81 -3
  65. data/lib/hecks/hecks/hecks.bluebook +18 -0
  66. data/lib/hecks/hecks/hecks.hecksagon +16 -4
  67. data/lib/hecks/hecks/hecks.world +7 -6
  68. data/lib/hecks/hecks/site.behaviors +17 -0
  69. data/lib/hecks/hecks/site.world +10 -0
  70. data/lib/hecks/language/bluebook/bluebook.hecksagon +1 -1
  71. data/lib/hecks/language/hecksagon/hecksagon.bluebook +24 -47
  72. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +4 -4
  73. data/lib/hecks/projections/deploy/box/settings.rb +324 -0
  74. data/lib/hecks/projections/deploy/box/templates/box.yaml.tmpl +268 -0
  75. data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +68 -0
  76. data/lib/hecks/projections/deploy/box/templates/fetch-secrets.sh +21 -0
  77. data/lib/hecks/projections/deploy/box/templates/rds.yaml.tmpl +181 -0
  78. data/lib/hecks/projections/deploy/box/templates/render-compose-taskdef.sh.tmpl +49 -0
  79. data/lib/hecks/projections/deploy/box/templates/render-compose.sh.tmpl +43 -0
  80. data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +91 -0
  81. data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +58 -0
  82. data/lib/hecks/projections/deploy/box.rb +553 -0
  83. data/lib/hecks/projections/deploy/fargate/cdn.rb +13 -1
  84. data/lib/hecks/projections/deploy/fargate.rb +1 -1
  85. data/lib/hecks/projections/deploy/lambda.rb +11 -10
  86. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +1 -1
  87. data/lib/hecks/projections/diagrams.rb +2 -2
  88. data/lib/hecks/projections/site/edge/behaviours.rb +107 -0
  89. data/lib/hecks/projections/site/edge/checks.rb +137 -0
  90. data/lib/hecks/projections/site/edge/pattern.rb +50 -0
  91. data/lib/hecks/projections/site/edge/rules.rb +128 -0
  92. data/lib/hecks/projections/site/edge.rb +136 -0
  93. data/lib/hecks/projections/site/regions.rb +44 -0
  94. data/lib/hecks/projections/site/routes_ts.rb +284 -0
  95. data/lib/hecks/projections/site/site_cdn.rb +78 -0
  96. data/lib/hecks/projections/site/table/checks.rb +124 -0
  97. data/lib/hecks/projections/site/table/row_builder.rb +155 -0
  98. data/lib/hecks/projections/site/table.rb +98 -0
  99. data/lib/hecks/projections.rb +2 -0
  100. data/lib/hecks/projector/cli_projector.rb +153 -111
  101. data/lib/hecks/quality_control/adapters/agent.adapter +1 -1
  102. data/lib/hecks/quality_control/adapters/agent.rb +2 -2
  103. data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +3 -3
  104. data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +8 -8
  105. data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +3 -3
  106. data/lib/hecks/quality_control/cli/qa_generated_domains.rb +4 -4
  107. data/lib/hecks/quality_control/cli/qa_log_bug.rb +2 -2
  108. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +6 -5
  109. data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +2 -2
  110. data/lib/hecks/quality_control/cli/qa_postgres_role.rb +5 -5
  111. data/lib/hecks/quality_control/cli/qa_pr_check.rb +3 -3
  112. data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +3 -3
  113. data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +2 -2
  114. data/lib/hecks/quality_control/cli/qa_sweep.rb +10 -9
  115. data/lib/hecks/quality_control/cli/qa_tick.rb +3 -3
  116. data/lib/hecks/quality_control/quality_control.bluebook +18 -18
  117. data/lib/hecks/quality_control/quality_control.hecksagon +1 -1
  118. data/lib/hecks/runtime/loader.rb +31 -8
  119. data/lib/hecks/runtime/registry/verification.rb +15 -29
  120. data/lib/hecks/runtime/registry.rb +14 -20
  121. data/lib/hecks/rust_build/append_optionals.rb +79 -0
  122. data/lib/hecks/rust_build/conformance_fuzz.rb +2 -1
  123. data/lib/hecks/rust_build/domain_name.rb +26 -0
  124. data/lib/hecks/rust_build/kernel_input.rb +58 -0
  125. data/lib/hecks/rust_build/project_rust/codegen_run.rb +143 -0
  126. data/lib/hecks/rust_build/project_rust/root_mod.rb +1 -1
  127. data/lib/hecks/rust_build/project_rust.rb +57 -24
  128. data/lib/hecks/rust_build/rust_literal.rb +33 -0
  129. data/lib/hecks/rust_build/write_if_changed.rb +86 -0
  130. data/lib/hecks/site/bluebook/site.bluebook +205 -0
  131. data/lib/hecks/site/bluebook/site.hecksagon +5 -0
  132. data/lib/hecks/site/bluebook/site.ports.hecksagon +15 -0
  133. data/lib/hecks/sme/bluebook/sme.bluebook +336 -0
  134. data/lib/hecks/sme/bluebook/sme.hecksagon +5 -0
  135. data/lib/hecks/storehouse.rb +37 -7
  136. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +1 -1
  137. data/lib/hecks/three_zero/forms.yml +88 -88
  138. data/lib/hecks/tickets/bluebook/tickets.bluebook +331 -0
  139. data/lib/hecks/tickets/bluebook/tickets.hecksagon +11 -0
  140. data/lib/hecks/tickets/bluebook/tickets.ports.hecksagon +26 -0
  141. data/lib/hecks/tools/ci_gates.rb +1 -1
  142. data/lib/hecks/tools/deploy_recipe.rb +14 -6
  143. data/lib/hecks/tools/oidc_manifests.rb +2 -1
  144. data/lib/hecks/tools/sequence_script.rb +2 -2
  145. data/lib/hecks/tools/site_routes.rb +152 -0
  146. data/lib/hecks/tools/tenant_provisioning.rb +1 -1
  147. data/lib/hecks/tools.rb +1 -0
  148. data/lib/hecks/version.rb +1 -1
  149. data/lib/hecks.rb +1 -0
  150. data/rust/build/src/cargo_sync.rs +1 -1
  151. data/rust/build/src/pipeline.rs +23 -16
  152. data/rust/build/src/resolve.rs +39 -6
  153. data/rust/codegen/src/json_codec.rs +3 -0
  154. data/rust/codegen/src/naming.rs +4 -0
  155. data/rust/codegen/src/types.rs +45 -0
  156. data/rust/host/HECKS_RELEASE +1 -1
  157. data/rust/host/src/api.rs +1 -1
  158. data/rust/host/src/dispatch.rs +87 -7
  159. data/rust/host/src/needs.rs +111 -0
  160. data/rust/parser/src/keywords.rs +7 -6
  161. data/rust/parser/src/main.rs +4 -3
  162. data/rust/parser/src/parse/chapter.rs +1 -1
  163. data/rust/parser/src/parse/hecksagon.rs +25 -10
  164. data/rust/project/mutations.rb +4 -30
  165. data/rust/project/naming.rb +8 -28
  166. data/rust/project/write_if_changed.rb +4 -80
  167. data/rust/project_rust_pipeline.rb +16 -12
  168. data/rust/src/kernel/cli.rs +2 -0
  169. data/rust/src/kernel/needs.rs +74 -1
  170. metadata +51 -2
@@ -0,0 +1,91 @@
1
+ #!/bin/bash
2
+ # Copy @@STACK@@'s schemas from the old database into the new RDS instance through a bastion that can
3
+ # reach both, then verify the copy. Piped dump | restore, so nothing is written to disk. Never writes to
4
+ # the source.
5
+ #
6
+ # restore-to-rds.sh <bastion-instance-id> <source-host> <source-secret-arn> <rds-host> <rds-secret-arn>
7
+ #
8
+ # FORCE=1 drop the target schemas first: a re-load before cutover, or a rollback copy in the other
9
+ # direction (swap the hosts and secrets, and SRC_DB and DST_DB)
10
+ # SRC_DB the source database (default @@SOURCE_DATABASE@@)
11
+ # DST_DB the target database (default @@DATABASE@@)
12
+ #
13
+ # Schemas copied: @@SCHEMAS@@
14
+ #
15
+ # Why not a plain pg_restore: a Hecks schema's materialized views call hecks_tr_extract() unqualified, and
16
+ # pg_restore runs with an empty search_path, so the restore's REFRESH step errors on them. This restores
17
+ # tolerating exactly that error, then refreshes the views with the schema on the search_path, and fails on
18
+ # any other error.
19
+ #
20
+ # Needs pg_dump, pg_restore and psql 16 or newer, the SSM plugin, jq and AWS credentials that can read both
21
+ # secrets and start SSM sessions.
22
+ set -euo pipefail
23
+ HERE=$(cd "$(dirname "$0")" && pwd)
24
+
25
+ BASTION=${1:?bastion instance id}; SRC_HOST=${2:?source host}; SRC_SECRET=${3:?source secret arn}
26
+ DST_HOST=${4:?target host}; DST_SECRET=${5:?target secret arn}
27
+ SCHEMAS="@@SCHEMAS@@"
28
+ SRC_DB=${SRC_DB:-@@SOURCE_DATABASE@@}; DST_DB=${DST_DB:-@@DATABASE@@}
29
+ SRC_PORT=15432; DST_PORT=15433
30
+
31
+ pg_major() { "$1" --version 2>/dev/null | sed -E 's/.* ([0-9]+)[.].*/\1/'; }
32
+ # The default client on the PATH is often older than the server (Homebrew's is 14); prefer postgresql@16.
33
+ if [ "$(pg_major pg_dump || echo 0)" -lt 16 ] && [ -x /opt/homebrew/opt/postgresql@16/bin/pg_dump ]; then
34
+ export PATH=/opt/homebrew/opt/postgresql@16/bin:$PATH
35
+ fi
36
+ for t in pg_dump pg_restore psql; do
37
+ [ "$(pg_major $t || echo 0)" -ge 16 ] || { echo "need $t 16 or newer on the PATH" >&2; exit 1; }
38
+ done
39
+
40
+ WORK=$(mktemp -d)
41
+ PIDS=()
42
+ cleanup() { for p in "${PIDS[@]:-}"; do [ -n "$p" ] && kill "$p" 2>/dev/null || true; done; rm -rf "$WORK"; }
43
+ trap cleanup EXIT
44
+
45
+ secret_field() { aws secretsmanager get-secret-value --secret-id "$1" --query SecretString --output text | jq -r "$2"; }
46
+ SRC_PW=$(secret_field "$SRC_SECRET" .password); DST_PW=$(secret_field "$DST_SECRET" .password)
47
+ SRC_USER=$(secret_field "$SRC_SECRET" '.username // "postgres"'); DST_USER=$(secret_field "$DST_SECRET" '.username // "postgres"')
48
+
49
+ tunnel() { # host local-port
50
+ aws ssm start-session --target "$BASTION" --document-name AWS-StartPortForwardingSessionToRemoteHost \
51
+ --parameters "{\"host\":[\"$1\"],\"portNumber\":[\"5432\"],\"localPortNumber\":[\"$2\"]}" >"$WORK/tunnel_$2.log" 2>&1 &
52
+ PIDS+=($!)
53
+ disown # so shutting the tunnel down at exit does not print a "Terminated" line
54
+ }
55
+ tunnel "$SRC_HOST" $SRC_PORT; tunnel "$DST_HOST" $DST_PORT
56
+ for i in $(seq 1 20); do
57
+ grep -q "Waiting for connections" "$WORK/tunnel_$SRC_PORT.log" 2>/dev/null && grep -q "Waiting for connections" "$WORK/tunnel_$DST_PORT.log" 2>/dev/null && break
58
+ sleep 1
59
+ done
60
+
61
+ SRC() { PGPASSWORD=$SRC_PW psql -h localhost -p $SRC_PORT -U "$SRC_USER" -d "$SRC_DB" -Atc "$1"; }
62
+ DST() { PGPASSWORD=$DST_PW psql -h localhost -p $DST_PORT -U "$DST_USER" -d "$DST_DB" -Atc "$1"; }
63
+ echo "source $(SRC 'show server_version'), target $(DST 'show server_version')"
64
+
65
+ # Refuse to clobber a target that already holds the schemas.
66
+ for S in $SCHEMAS; do
67
+ if [ "$(DST "select count(*) from pg_namespace where nspname='$S'")" != 0 ]; then
68
+ if [ "${FORCE:-}" = 1 ]; then DST "drop schema \"$S\" cascade" >/dev/null; echo "dropped target schema $S"
69
+ else echo "target already has schema $S; re-run with FORCE=1 to replace it" >&2; exit 1; fi
70
+ fi
71
+ done
72
+
73
+ for S in $SCHEMAS; do
74
+ echo "== copying schema $S"
75
+ PGPASSWORD=$SRC_PW pg_dump -h localhost -p $SRC_PORT -U "$SRC_USER" -d "$SRC_DB" --schema="$S" -Fc --no-owner --no-privileges \
76
+ | PGPASSWORD=$DST_PW pg_restore -h localhost -p $DST_PORT -U "$DST_USER" -d "$DST_DB" --no-owner --no-privileges 2>"$WORK/err_$S.txt" || true
77
+ TOTAL=$(grep -c 'error:' "$WORK/err_$S.txt" || true)
78
+ UNEXPECTED=$(grep 'error:' "$WORK/err_$S.txt" | grep -vc 'hecks_tr_extract' || true)
79
+ echo " restore errors: $TOTAL (unexpected: $UNEXPECTED)"
80
+ if [ "$UNEXPECTED" != 0 ]; then grep 'error:' "$WORK/err_$S.txt" | grep -v 'hecks_tr_extract' | head -5 >&2; exit 1; fi
81
+ done
82
+
83
+ # Refresh what pg_restore could not (with the schema on the search_path).
84
+ for S in $SCHEMAS; do
85
+ for MV in $(DST "select matviewname from pg_matviews where schemaname='$S' and not ispopulated"); do
86
+ PGPASSWORD=$DST_PW psql -h localhost -p $DST_PORT -U "$DST_USER" -d "$DST_DB" -qAtc "set search_path=\"$S\",public; refresh materialized view \"$S\".\"$MV\""
87
+ done
88
+ done
89
+
90
+ # Same structure, same exact row counts in every table, nothing unpopulated.
91
+ A_DB=$SRC_DB B_DB=$DST_DB bash "$HERE/verify-copy.sh" "$BASTION" "$SRC_HOST" "$SRC_SECRET" "$DST_HOST" "$DST_SECRET"
@@ -0,0 +1,58 @@
1
+ #!/bin/bash
2
+ # Compare two databases holding @@STACK@@'s schemas (structure and exact row counts of every table)
3
+ # through a bastion that can reach both. Read-only on both. Used at the end of restore-to-rds.sh, for a
4
+ # backup restore drill (production vs a database restored from its backup) and for checking a rollback copy.
5
+ #
6
+ # verify-copy.sh <bastion-instance-id> <host-a> <secret-arn-a> <host-b> <secret-arn-b>
7
+ #
8
+ # A_DB, B_DB the database on each side (default @@DATABASE@@)
9
+ #
10
+ # Schemas compared: @@SCHEMAS@@
11
+ # Exit 0 only if both are identical and no materialized view is unpopulated in either.
12
+ set -euo pipefail
13
+
14
+ BASTION=${1:?bastion instance id}; A_HOST=${2:?}; A_SECRET=${3:?}; B_HOST=${4:?}; B_SECRET=${5:?}
15
+ SCHEMAS="@@SCHEMAS_CSV@@"
16
+ A_DB=${A_DB:-@@DATABASE@@}; B_DB=${B_DB:-@@DATABASE@@}
17
+ A_PORT=15434; B_PORT=15435
18
+
19
+ pg_major() { "$1" --version 2>/dev/null | sed -E 's/.* ([0-9]+)[.].*/\1/'; }
20
+ if [ "$(pg_major psql || echo 0)" -lt 16 ] && [ -x /opt/homebrew/opt/postgresql@16/bin/psql ]; then
21
+ export PATH=/opt/homebrew/opt/postgresql@16/bin:$PATH
22
+ fi
23
+
24
+ WORK=$(mktemp -d); PIDS=()
25
+ cleanup() { for p in "${PIDS[@]:-}"; do [ -n "$p" ] && kill "$p" 2>/dev/null || true; done; rm -rf "$WORK"; }
26
+ trap cleanup EXIT
27
+
28
+ secret_field() { aws secretsmanager get-secret-value --secret-id "$1" --query SecretString --output text | jq -r "$2"; }
29
+ A_PW=$(secret_field "$A_SECRET" .password); B_PW=$(secret_field "$B_SECRET" .password)
30
+ A_USER=$(secret_field "$A_SECRET" '.username // "postgres"'); B_USER=$(secret_field "$B_SECRET" '.username // "postgres"')
31
+ tunnel() {
32
+ aws ssm start-session --target "$BASTION" --document-name AWS-StartPortForwardingSessionToRemoteHost \
33
+ --parameters "{\"host\":[\"$1\"],\"portNumber\":[\"5432\"],\"localPortNumber\":[\"$2\"]}" >"$WORK/t_$2.log" 2>&1 &
34
+ PIDS+=($!); disown
35
+ }
36
+ tunnel "$A_HOST" $A_PORT; tunnel "$B_HOST" $B_PORT
37
+ for i in $(seq 1 20); do
38
+ grep -q "Waiting for connections" "$WORK/t_$A_PORT.log" 2>/dev/null && grep -q "Waiting for connections" "$WORK/t_$B_PORT.log" 2>/dev/null && break
39
+ sleep 1
40
+ done
41
+ A() { PGPASSWORD=$A_PW psql -h localhost -p $A_PORT -U "$A_USER" -d "$A_DB" -Atc "$1"; }
42
+ B() { PGPASSWORD=$B_PW psql -h localhost -p $B_PORT -U "$B_USER" -d "$B_DB" -Atc "$1"; }
43
+
44
+ SHAPE="select n.nspname||' '||c.relkind::text, count(*) from pg_class c join pg_namespace n on n.oid=c.relnamespace where n.nspname = any(string_to_array('$SCHEMAS', ',')) group by 1
45
+ union all select 'policies', count(*) from pg_policies where schemaname = any(string_to_array('$SCHEMAS', ','))
46
+ union all select 'functions', count(*) from pg_proc p join pg_namespace n on n.oid=p.pronamespace where n.nspname = any(string_to_array('$SCHEMAS', ',')) order by 1"
47
+ COUNTS="select table_schema||'.'||table_name, (xpath('/row/c/text()', query_to_xml(format('select count(*) as c from %I.%I', table_schema, table_name), false, true, '')))[1]::text from information_schema.tables where table_type='BASE TABLE' and table_schema = any(string_to_array('$SCHEMAS', ',')) order by 1"
48
+ UNPOP="select count(*) from pg_matviews where schemaname = any(string_to_array('$SCHEMAS', ',')) and not ispopulated"
49
+
50
+ BAD=0
51
+ A "$SHAPE" >"$WORK/shape_a"; B "$SHAPE" >"$WORK/shape_b"
52
+ A "$COUNTS" >"$WORK/counts_a"; B "$COUNTS" >"$WORK/counts_b"
53
+ [ -s "$WORK/counts_a" ] || { echo "FAIL: first database has no tables to compare" >&2; BAD=1; }
54
+ [ "$(A "$UNPOP")" = 0 ] && [ "$(B "$UNPOP")" = 0 ] || { echo "FAIL: unpopulated materialized views" >&2; BAD=1; }
55
+ diff "$WORK/shape_a" "$WORK/shape_b" >/dev/null || { echo "FAIL: structure differs" >&2; diff "$WORK/shape_a" "$WORK/shape_b" >&2 || true; BAD=1; }
56
+ diff "$WORK/counts_a" "$WORK/counts_b" >/dev/null || { echo "FAIL: row counts differ" >&2; diff "$WORK/counts_a" "$WORK/counts_b" >&2 || true; BAD=1; }
57
+ [ "$BAD" = 0 ] || exit 1
58
+ echo "OK: $(wc -l <"$WORK/counts_a" | tr -d ' ') tables, identical row counts and structure"