hecks 2.9.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/exe/hecks +51 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
- data/lib/hecks/adapters/driven/lambda.rb +1 -1
- data/lib/hecks/adapters/driven/memory.rb +16 -3
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +6 -2
- data/lib/hecks/adapters/driven/postgres/shared_connection.rb +180 -0
- data/lib/hecks/adapters/driven/postgres.rb +4 -2
- data/lib/hecks/adapters/driven/prism.rb +41 -8
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +73 -16
- data/lib/hecks/behaviors/expectations.rb +1 -1
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bench/cli.rb +62 -0
- data/lib/hecks/bench/environment.rb +71 -0
- data/lib/hecks/bench/postgres_probe.rb +30 -0
- data/lib/hecks/bench/report.rb +76 -0
- data/lib/hecks/bench/ruby_runner.rb +93 -0
- data/lib/hecks/bench/run.rb +55 -0
- data/lib/hecks/bench/rust_runner.rb +101 -0
- data/lib/hecks/bench/stats.rb +46 -0
- data/lib/hecks/bench/suite.rb +98 -0
- data/lib/hecks/bench/workload.rb +89 -0
- data/lib/hecks/bench.rb +17 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +2 -0
- data/lib/hecks/bluebook/attribute.rb +1 -1
- data/lib/hecks/bluebook/behaviour/aggregate.rb +30 -1
- data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -0
- data/lib/hecks/bluebook/chapter.rb +6 -2
- data/lib/hecks/bluebook/domain_port.rb +36 -5
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +10 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +8 -0
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -1
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +31 -3
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +122 -3
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/query_builder.rb +27 -1
- data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +29 -2
- data/lib/hecks/bluebook/expression/canonical_form.rb +1 -1
- data/lib/hecks/bluebook/expression/resolver.rb +3 -2
- data/lib/hecks/bluebook/hexagon.rb +13 -5
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +4 -1
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +246 -0
- data/lib/hecks/bluebook/meta_validator.rb +9 -4
- data/lib/hecks/bluebook/model_check.rb +23 -0
- data/lib/hecks/bluebook/pattern_subset.rb +38 -0
- data/lib/hecks/bluebook/query.rb +20 -5
- data/lib/hecks/bluebook/smoke_test.rb +1 -1
- data/lib/hecks/canonical_json.rb +30 -0
- data/lib/hecks/chapters.rb +102 -0
- data/lib/hecks/cli/behaviors.rb +61 -0
- data/lib/hecks/cli/canonicalise.rb +25 -0
- data/lib/hecks/cli/check_engine_agreement.rb +32 -0
- data/lib/hecks/cli/check_era.rb +88 -0
- data/lib/hecks/cli/console.rb +66 -0
- data/lib/hecks/cli/deploy_template_diff.rb +40 -0
- data/lib/hecks/cli/doc_coverage.rb +32 -0
- data/lib/hecks/cli/document.rb +2 -2
- data/lib/hecks/cli/expression_projection.rb +41 -0
- data/lib/hecks/cli/follow.rb +88 -0
- data/lib/hecks/cli/history.rb +44 -0
- data/lib/hecks/cli/ir.rb +1 -1
- data/lib/hecks/cli/mcp.rb +3 -3
- data/lib/hecks/cli/model_check.rb +7 -4
- data/lib/hecks/cli/pattern_cases.rb +47 -0
- data/lib/hecks/cli/present.rb +39 -0
- data/lib/hecks/cli/project_cli.rb +224 -32
- data/lib/hecks/cli/project_diagrams.rb +4 -3
- data/lib/hecks/cli/project_glossary.rb +44 -0
- data/lib/hecks/cli/reference.rb +46 -0
- data/lib/hecks/cli/refresh_projections.rb +44 -0
- data/lib/hecks/cli/refresh_rspec_runtime_baseline.rb +150 -0
- data/lib/hecks/cli/release.rb +70 -0
- data/lib/hecks/cli/release_gem.rb +59 -0
- data/lib/hecks/cli/rspec_io_parallel_files.rb +137 -0
- data/lib/hecks/cli/rspec_shard_files.rb +66 -0
- data/lib/hecks/cli/run.rb +3 -3
- data/lib/hecks/cli/seed_semantics_corpus.rb +71 -0
- data/lib/hecks/cli/serve_query_ir_mcp.rb +30 -0
- data/lib/hecks/cli/shape.rb +70 -0
- data/lib/hecks/cli/smoke_http.rb +230 -0
- data/lib/hecks/cli/smoke_test.rb +5 -3
- data/lib/hecks/cli/spec_example.rb +27 -0
- data/lib/hecks/cli/statements.rb +32 -0
- data/lib/hecks/cli/stores.rb +4 -5
- data/lib/hecks/cli/stress_concurrency_specs.rb +177 -0
- data/lib/hecks/cli.rb +201 -52
- data/lib/hecks/codemod.rb +324 -0
- data/lib/hecks/corpus.rb +423 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +591 -23
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +41 -4
- data/lib/hecks/deploy/oidc.json +82 -2
- data/lib/hecks/doc/reference.rb +351 -0
- data/lib/hecks/doors/cli_door.rb +196 -0
- data/lib/hecks/doors/cli_runner.rb +297 -0
- data/lib/hecks/{facade → doors}/command_request.rb +1 -1
- data/lib/hecks/{facade → doors}/handle.rb +2 -2
- data/lib/hecks/{facade → doors}/json_door.rb +4 -4
- data/lib/hecks/doors/launcher_options.rb +129 -0
- data/lib/hecks/{cli → doors}/mcp_door.rb +7 -7
- data/lib/hecks/doors/mcp_door_scope.rb +97 -0
- data/lib/hecks/{facade/surface → doors/ruby_door}/aggregate_door.rb +8 -3
- data/lib/hecks/{facade/surface → doors/ruby_door}/chapter.rb +3 -3
- data/lib/hecks/{facade/surface.rb → doors/ruby_door.rb} +36 -5
- data/lib/hecks/doors.rb +61 -0
- data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +1 -1
- data/lib/hecks/embryonaut_bluebook.rb +1 -1
- data/lib/hecks/engine_agreement.rb +156 -0
- data/lib/hecks/facade.rb +24 -37
- data/lib/hecks/forms/app.rb +6 -6
- data/lib/hecks/forms/banking_presentation.rb +50 -0
- data/lib/hecks/forms/field_shape.rb +2 -1
- data/lib/hecks/forms/port_argument.rb +1 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +393 -0
- data/lib/hecks/fuzzing/combination_miner.rb +86 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +214 -0
- data/lib/hecks/fuzzing/coverage_campaign.rb +143 -0
- data/lib/hecks/fuzzing/differential.rb +156 -0
- data/lib/hecks/fuzzing/domain_generator.rb +615 -0
- data/lib/hecks/fuzzing/era_boundary.rb +71 -0
- data/lib/hecks/fuzzing/form_census.rb +136 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +93 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +64 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +301 -0
- data/lib/hecks/fuzzing/nondeterministic.rb +51 -0
- data/lib/hecks/fuzzing/outside_world.rb +45 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +102 -0
- data/lib/hecks/fuzzing/properties/corrections.rb +88 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +314 -0
- data/lib/hecks/fuzzing/properties/guards.rb +190 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +300 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +93 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +89 -0
- data/lib/hecks/fuzzing/properties/querying.rb +175 -0
- data/lib/hecks/fuzzing/properties.rb +162 -0
- data/lib/hecks/fuzzing/qa_settings.rb +123 -0
- data/lib/hecks/fuzzing/replay.rb +505 -0
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -0
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +108 -0
- data/lib/hecks/fuzzing/self_consistency.rb +339 -0
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +385 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +132 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +95 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +77 -0
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +109 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +237 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +189 -0
- data/lib/hecks/fuzzing/shrinker.rb +166 -0
- data/lib/hecks/fuzzing/structural_skips.rb +33 -0
- data/lib/hecks/fuzzing/sweep_depth.rb +43 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +163 -0
- data/lib/hecks/fuzzing/value_generator.rb +216 -0
- data/lib/hecks/fuzzing.rb +23 -0
- data/lib/hecks/grammar/evolve.rb +336 -0
- data/lib/hecks/hecks/adapters/codebase/codemods.rb +45 -0
- data/lib/hecks/hecks/adapters/codebase/conformance.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/corpus_tasks.rb +202 -0
- data/lib/hecks/hecks/adapters/codebase/gem_registry.rb +80 -0
- data/lib/hecks/hecks/adapters/codebase/kernel_tables.rb +54 -0
- data/lib/hecks/hecks/adapters/codebase/language.rb +240 -0
- data/lib/hecks/hecks/adapters/codebase/npm_registry.rb +102 -0
- data/lib/hecks/hecks/adapters/codebase/publishing.rb +142 -0
- data/lib/hecks/hecks/adapters/codebase/regeneration.rb +67 -0
- data/lib/hecks/hecks/adapters/codebase/release_facts.rb +90 -0
- data/lib/hecks/hecks/adapters/codebase/ruby_child.rb +83 -0
- data/lib/hecks/hecks/adapters/codebase/secret_vault.rb +36 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.adapter +7 -0
- data/lib/hecks/hecks/adapters/codebase/source_tree.rb +189 -0
- data/lib/hecks/hecks/adapters/codebase/sqlite_fixture.rb +71 -0
- data/lib/hecks/hecks/adapters/codebase/style.rb +171 -0
- data/lib/hecks/hecks/adapters/codebase/test_runner.rb +55 -0
- data/lib/hecks/hecks/adapters/codebase/test_suite.rb +165 -0
- data/lib/hecks/hecks/adapters/codebase/tree.rb +111 -0
- data/lib/hecks/hecks/adapters/console_capture.rb +79 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/deploy_toolchain.rb +108 -0
- data/lib/hecks/hecks/adapters/git.adapter +7 -0
- data/lib/hecks/hecks/adapters/git.rb +102 -0
- data/lib/hecks/hecks/adapters/host_http.adapter +7 -0
- data/lib/hecks/hecks/adapters/host_http.rb +65 -0
- data/lib/hecks/hecks/adapters/in_process_boot.adapter +7 -0
- data/lib/hecks/hecks/adapters/in_process_boot.rb +197 -0
- data/lib/hecks/hecks/adapters/in_process_operations.rb +202 -0
- data/lib/hecks/hecks/adapters/journal_store/changes.rb +143 -0
- data/lib/hecks/hecks/adapters/journal_store/compaction.rb +145 -0
- data/lib/hecks/hecks/adapters/journal_store/edge_audit.rb +132 -0
- data/lib/hecks/hecks/adapters/journal_store/examination.rb +135 -0
- data/lib/hecks/hecks/adapters/journal_store/held_domain.rb +163 -0
- data/lib/hecks/hecks/adapters/journal_store/readings.rb +187 -0
- data/lib/hecks/hecks/adapters/journal_store.adapter +7 -0
- data/lib/hecks/hecks/adapters/journal_store.rb +64 -0
- data/lib/hecks/hecks/adapters/local_files.adapter +7 -0
- data/lib/hecks/hecks/adapters/local_files.rb +46 -0
- data/lib/hecks/hecks/adapters/pg_admin.adapter +7 -0
- data/lib/hecks/hecks/adapters/pg_admin.rb +245 -0
- data/lib/hecks/hecks/adapters/process_pool.adapter +7 -0
- data/lib/hecks/hecks/adapters/process_pool.rb +119 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.adapter +7 -0
- data/lib/hecks/hecks/adapters/rust_toolchain.rb +179 -0
- data/lib/hecks/hecks/adapters/rust_workspace.rb +150 -0
- data/lib/hecks/hecks/adapters/shell.adapter +7 -0
- data/lib/hecks/hecks/adapters/shell.rb +49 -0
- data/lib/hecks/hecks/adapters/terminal.adapter +7 -0
- data/lib/hecks/hecks/adapters/terminal.rb +74 -0
- data/lib/hecks/hecks/codebase.behaviors +172 -0
- data/lib/hecks/hecks/codebase.bluebook +2453 -0
- data/lib/hecks/hecks/context_map.hecksagon +13 -0
- data/lib/hecks/hecks/custodian.behaviors +179 -0
- data/lib/hecks/hecks/custodian.bluebook +1847 -0
- data/lib/hecks/hecks/deploy.behaviors +74 -0
- data/lib/hecks/hecks/deploy.world +12 -0
- data/lib/hecks/hecks/environments/memory.world +10 -0
- data/lib/hecks/hecks/governance.world +7 -0
- data/lib/hecks/hecks/hecks.behaviors +62 -0
- data/lib/hecks/hecks/hecks.bluebook +124 -0
- data/lib/hecks/hecks/hecks.hecksagon +390 -0
- data/lib/hecks/hecks/hecks.world +42 -0
- data/lib/hecks/kernel_capabilities.rb +194 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +11 -0
- data/lib/hecks/language/bluebook/query.bluebook +7 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +4 -4
- data/lib/hecks/language/hecksagon/domain_port.bluebook +10 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +20 -0
- data/lib/hecks/mcp_stdio_guard.rb +2 -2
- data/lib/hecks/persistence_legacy_fixture/regenerate.rb +161 -0
- data/lib/hecks/persistence_legacy_fixture.rb +113 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +8 -5
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +4 -1
- data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +15 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +32 -15
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +6 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +227 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +43 -24
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +1 -0
- data/lib/hecks/ports/persistence/state_codec.rb +24 -5
- data/lib/hecks/projection_files.rb +285 -0
- data/lib/hecks/projections/bootstrap_table.rb +1 -1
- data/lib/hecks/projections/deploy/fargate.rb +5 -5
- data/lib/hecks/projections/deploy/lambda.rb +43 -32
- data/lib/hecks/projections/deploy/preview/preview.sh.erb +1 -1
- data/lib/hecks/projections/deploy/preview/template.rb +1 -1
- data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/scripts/expected-era.header +1 -1
- data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +3 -3
- data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/shared.rb +1 -1
- data/lib/hecks/projections/diagrams.rb +3 -3
- data/lib/hecks/projections/model/deviations.rb +1 -1
- data/lib/hecks/projections/parser_table.rb +4 -4
- data/lib/hecks/projections/reference.rb +2 -3
- data/lib/hecks/projections/rust_vocabulary.rb +1 -1
- data/lib/hecks/projector/cli_projector.rb +106 -37
- data/lib/hecks/projector.rb +1 -1
- data/lib/hecks/quality_control/adapters/agent.adapter +7 -0
- data/lib/hecks/quality_control/adapters/agent.rb +58 -0
- data/lib/hecks/quality_control/adapters/angle_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/angle_tools.rb +15 -0
- data/lib/hecks/quality_control/adapters/clearance_recorder.rb +36 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/clearance_tools.rb +17 -0
- data/lib/hecks/quality_control/adapters/git_pr.adapter +7 -0
- data/lib/hecks/quality_control/adapters/git_pr.rb +226 -0
- data/lib/hecks/quality_control/adapters/github_checks.adapter +9 -0
- data/lib/hecks/quality_control/adapters/github_checks.rb +83 -0
- data/lib/hecks/quality_control/adapters/github_ci_webhook.rb +56 -0
- data/lib/hecks/quality_control/adapters/github_issues.adapter +7 -0
- data/lib/hecks/quality_control/adapters/github_issues.rb +49 -0
- data/lib/hecks/quality_control/adapters/qa_tool.rb +59 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/sweep_tools.rb +51 -0
- data/lib/hecks/quality_control/adapters/target_tools.adapter +7 -0
- data/lib/hecks/quality_control/adapters/target_tools.rb +43 -0
- data/lib/hecks/quality_control/cli/child.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_concurrency_racer.rb +45 -0
- data/lib/hecks/quality_control/cli/qa_discover_external_domains.rb +248 -0
- data/lib/hecks/quality_control/cli/qa_domain_novelty.rb +163 -0
- data/lib/hecks/quality_control/cli/qa_generated_domains.rb +450 -0
- data/lib/hecks/quality_control/cli/qa_log_bug.rb +195 -0
- data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +220 -0
- data/lib/hecks/quality_control/cli/qa_open_pr.rb +305 -0
- data/lib/hecks/quality_control/cli/qa_postgres_migrate.rb +154 -0
- data/lib/hecks/quality_control/cli/qa_postgres_role.rb +159 -0
- data/lib/hecks/quality_control/cli/qa_pr_check.rb +232 -0
- data/lib/hecks/quality_control/cli/qa_seed_angles.rb +142 -0
- data/lib/hecks/quality_control/cli/qa_seed_targets.rb +69 -0
- data/lib/hecks/quality_control/cli/qa_sweep/all_mode.rb +280 -0
- data/lib/hecks/quality_control/cli/qa_sweep/checks.rb +329 -0
- data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +92 -0
- data/lib/hecks/quality_control/cli/qa_sweep/finding_report.rb +117 -0
- data/lib/hecks/quality_control/cli/qa_sweep/parsing.rb +175 -0
- data/lib/hecks/quality_control/cli/qa_sweep/release_mode.rb +60 -0
- data/lib/hecks/quality_control/cli/qa_sweep.rb +611 -0
- data/lib/hecks/quality_control/cli/qa_tick.rb +274 -0
- data/lib/hecks/quality_control/quality_control.bluebook +2224 -0
- data/lib/hecks/quality_control/quality_control.hecksagon +13 -0
- data/lib/hecks/quality_control/quality_control.ports.hecksagon +54 -0
- data/lib/hecks/query_ir.rb +346 -0
- data/lib/hecks/query_ir_mcp.rb +198 -0
- data/lib/hecks/release/runner/ci_publisher.rb +2 -2
- data/lib/hecks/release/runner/clean_tree.rb +42 -0
- data/lib/hecks/release/runner/console.rb +6 -0
- data/lib/hecks/release/runner/gem_publisher.rb +14 -7
- data/lib/hecks/release/runner/npm_publisher.rb +6 -36
- data/lib/hecks/release/runner/preflight.rb +26 -2
- data/lib/hecks/release/runner/published.rb +8 -26
- data/lib/hecks/release/runner/tagger.rb +30 -15
- data/lib/hecks/release/runner.rb +57 -6
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/runtime/adapter_lookup.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +4 -0
- data/lib/hecks/runtime/dependency_planning.rb +20 -2
- data/lib/hecks/runtime/dispatcher.rb +53 -3
- data/lib/hecks/runtime/interpreting.rb +3 -0
- data/lib/hecks/runtime/loader.rb +65 -12
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/policy_interpreter.rb +5 -2
- data/lib/hecks/runtime/port_operation_interpreter.rb +2 -10
- data/lib/hecks/runtime/query_interpreter.rb +77 -0
- data/lib/hecks/runtime/reaction_outcome.rb +89 -0
- data/lib/hecks/runtime/refusal_wording.rb +23 -1
- data/lib/hecks/runtime/registry/verification.rb +163 -4
- data/lib/hecks/runtime/registry.rb +57 -3
- data/lib/hecks/runtime/remote_dispatcher.rb +31 -2
- data/lib/hecks/runtime/value/coercion.rb +23 -135
- data/lib/hecks/runtime/value/field_checks.rb +172 -0
- data/lib/hecks/runtime/value.rb +4 -1
- data/lib/hecks/runtime.rb +20 -6
- data/lib/hecks/rust_build/conformance.rb +158 -0
- data/lib/hecks/rust_build/conformance_fuzz.rb +71 -0
- data/lib/hecks/rust_build/coverage.rb +218 -0
- data/lib/hecks/rust_build/native_build.rb +124 -0
- data/lib/hecks/rust_build/project_rust/root_mod.rb +120 -0
- data/lib/hecks/rust_build/project_rust.rb +332 -0
- data/lib/hecks/rust_build/wasm.rb +105 -0
- data/lib/hecks/rust_build/wasm_browser.rb +113 -0
- data/lib/hecks/rust_build.rb +144 -0
- data/lib/hecks/storehouse.rb +16 -16
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +2 -2
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +1 -1
- data/lib/hecks/three_zero/forms.yml +127 -0
- data/lib/hecks/three_zero.rb +12 -0
- data/lib/hecks/tools/argument_gate_matrix.rb +316 -0
- data/lib/hecks/tools/comment_style.rb +1070 -0
- data/lib/hecks/tools/corpus_report.rb +87 -0
- data/lib/hecks/tools/deploy_recipe.rb +255 -0
- data/lib/hecks/tools/deploy_recipe_lint.rb +282 -0
- data/lib/hecks/tools/drop_implicit_append_fields.rb +95 -0
- data/lib/hecks/tools/era_projection_backfill.rb +94 -0
- data/lib/hecks/tools/era_reattest.rb +146 -0
- data/lib/hecks/tools/evolve_run.rb +189 -0
- data/lib/hecks/tools/fuzz_sweep.rb +373 -0
- data/lib/hecks/tools/heki_compaction.rb +100 -0
- data/lib/hecks/tools/hoist_local_givens.rb +246 -0
- data/lib/hecks/tools/journal_compaction.rb +89 -0
- data/lib/hecks/tools/oidc_manifests.rb +70 -0
- data/lib/hecks/tools/query_ir_run.rb +81 -0
- data/lib/hecks/tools/regeneration_run.rb +166 -0
- data/lib/hecks/tools/rust_comment_style.rb +642 -0
- data/lib/hecks/tools/sequence_script.rb +62 -0
- data/lib/hecks/tools/tail_merge.rb +117 -0
- data/lib/hecks/tools/tenant_provisioning.rb +142 -0
- data/lib/hecks/tools/translation_audit.rb +208 -0
- data/lib/hecks/tools/translation_scaffold.rb +131 -0
- data/lib/hecks/tools.rb +86 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +32 -15
- data/qa/settings.yml +66 -0
- data/rust/.cargo/config.toml +6 -0
- data/rust/.gitignore +2 -0
- data/rust/Cargo.lock +54 -0
- data/rust/Cargo.toml +61 -0
- data/rust/build/.cargo/config.toml +3 -0
- data/rust/build/.gitignore +1 -0
- data/rust/build/Cargo.lock +7 -0
- data/rust/build/Cargo.toml +19 -0
- data/rust/build/src/build_artifact.rs +47 -0
- data/rust/build/src/cargo_sync.rs +248 -0
- data/rust/build/src/json.rs +421 -0
- data/rust/build/src/lineage_pass.rs +285 -0
- data/rust/build/src/main.rs +51 -0
- data/rust/build/src/optional_pass.rs +150 -0
- data/rust/build/src/pipeline.rs +264 -0
- data/rust/build/src/reserved_names.rs +76 -0
- data/rust/build/src/resolve.rs +124 -0
- data/rust/build/src/root.rs +25 -0
- data/rust/build/src/sidecars.rs +73 -0
- data/rust/build/src/subprocess.rs +51 -0
- data/rust/build/src/tmp.rs +31 -0
- data/rust/codegen/.cargo/config.toml +3 -0
- data/rust/codegen/.gitignore +1 -0
- data/rust/codegen/Cargo.lock +7 -0
- data/rust/codegen/Cargo.toml +20 -0
- data/rust/codegen/src/attr.rs +32 -0
- data/rust/codegen/src/bridging.rs +481 -0
- data/rust/codegen/src/commands.rs +1226 -0
- data/rust/codegen/src/constraints.rs +109 -0
- data/rust/codegen/src/dependency_planning.rs +209 -0
- data/rust/codegen/src/domain_generator.rs +1288 -0
- data/rust/codegen/src/exemplar.rs +230 -0
- data/rust/codegen/src/expr_emitter.rs +87 -0
- data/rust/codegen/src/fielded.rs +192 -0
- data/rust/codegen/src/hecks_naming.rs +33 -0
- data/rust/codegen/src/json.rs +335 -0
- data/rust/codegen/src/json_codec.rs +697 -0
- data/rust/codegen/src/literal.rs +207 -0
- data/rust/codegen/src/main.rs +290 -0
- data/rust/codegen/src/manifest.rs +227 -0
- data/rust/codegen/src/mutations.rs +784 -0
- data/rust/codegen/src/naming.rs +286 -0
- data/rust/codegen/src/ports.rs +208 -0
- data/rust/codegen/src/prelude.rs +116 -0
- data/rust/codegen/src/queries.rs +786 -0
- data/rust/codegen/src/reactions.rs +532 -0
- data/rust/codegen/src/read_models.rs +697 -0
- data/rust/codegen/src/reference_specs.rs +40 -0
- data/rust/codegen/src/registry.rs +966 -0
- data/rust/codegen/src/reserved_names.rs +76 -0
- data/rust/codegen/src/shared.rs +93 -0
- data/rust/codegen/src/skip_reason.rs +25 -0
- data/rust/codegen/src/types.rs +362 -0
- data/rust/host/.cargo/config.toml +3 -0
- data/rust/host/.gitignore +1 -0
- data/rust/host/Cargo.lock +4297 -0
- data/rust/host/Cargo.toml +61 -0
- data/rust/host/HECKS_RELEASE +1 -0
- data/rust/host/rds-ca-bundle.pem +2736 -0
- data/rust/host/src/api.rs +1596 -0
- data/rust/host/src/approval.rs +652 -0
- data/rust/host/src/auth.rs +1287 -0
- data/rust/host/src/bin/expr_harness.rs +46 -0
- data/rust/host/src/bin/lineage_harness.rs +119 -0
- data/rust/host/src/bin/mint_harness.rs +135 -0
- data/rust/host/src/checkout.rs +265 -0
- data/rust/host/src/dispatch.rs +1443 -0
- data/rust/host/src/expr_json.rs +1117 -0
- data/rust/host/src/field_hints.rs +52 -0
- data/rust/host/src/ir.rs +733 -0
- data/rust/host/src/journal.rs +874 -0
- data/rust/host/src/lambda_client.rs +378 -0
- data/rust/host/src/log.rs +147 -0
- data/rust/host/src/main.rs +474 -0
- data/rust/host/src/mint.rs +1865 -0
- data/rust/host/src/payments/keystore.rs +330 -0
- data/rust/host/src/payments/tests.rs +1400 -0
- data/rust/host/src/payments.rs +767 -0
- data/rust/host/src/presentation.rs +698 -0
- data/rust/host/src/presentation_write.rs +1431 -0
- data/rust/host/src/query_step.rs +149 -0
- data/rust/host/src/rate_limit.rs +904 -0
- data/rust/host/src/reference_transform.rs +241 -0
- data/rust/host/src/reference_validate.rs +338 -0
- data/rust/host/src/resend.rs +296 -0
- data/rust/host/src/secrets.rs +106 -0
- data/rust/host/src/server.rs +663 -0
- data/rust/host/src/storage_shape.rs +175 -0
- data/rust/host/src/test_pg.rs +40 -0
- data/rust/host/src/ui_schema.rs +1351 -0
- data/rust/host/src/wasm_runner.rs +191 -0
- data/rust/host/src/web/newsletter/cooldown.rs +126 -0
- data/rust/host/src/web/newsletter/tests.rs +104 -0
- data/rust/host/src/web/newsletter.rs +310 -0
- data/rust/host/src/web/newsletter_send.rs +357 -0
- data/rust/host/src/web/registrations.rs +1505 -0
- data/rust/host/src/web.rs +2937 -0
- data/rust/lsp/.gitignore +1 -0
- data/rust/lsp/Cargo.lock +7 -0
- data/rust/lsp/Cargo.toml +16 -0
- data/rust/lsp/README.md +133 -0
- data/rust/lsp/src/diagnostics.rs +165 -0
- data/rust/lsp/src/json.rs +385 -0
- data/rust/lsp/src/main.rs +399 -0
- data/rust/lsp/src/outline.rs +177 -0
- data/rust/lsp/src/rpc.rs +67 -0
- data/rust/parser/.cargo/config.toml +5 -0
- data/rust/parser/.gitignore +1 -0
- data/rust/parser/Cargo.lock +7 -0
- data/rust/parser/Cargo.toml +21 -0
- data/rust/parser/src/build/closed_sets.rs +27 -0
- data/rust/parser/src/build/identity.rs +276 -0
- data/rust/parser/src/build/mod.rs +12 -0
- data/rust/parser/src/build/naming.rs +141 -0
- data/rust/parser/src/build/pattern_subset.rs +161 -0
- data/rust/parser/src/build/query_derive.rs +72 -0
- data/rust/parser/src/build/query_inference.rs +269 -0
- data/rust/parser/src/build/query_options.rs +100 -0
- data/rust/parser/src/build/read_model.rs +94 -0
- data/rust/parser/src/build/references.rs +32 -0
- data/rust/parser/src/canonical.rs +114 -0
- data/rust/parser/src/diag.rs +55 -0
- data/rust/parser/src/emit.rs +953 -0
- data/rust/parser/src/expr/ast_json.rs +147 -0
- data/rust/parser/src/expr/evaluator.rs +81 -0
- data/rust/parser/src/expr/mod.rs +84 -0
- data/rust/parser/src/expr/resolver.rs +497 -0
- data/rust/parser/src/ir.rs +387 -0
- data/rust/parser/src/keywords.rs +585 -0
- data/rust/parser/src/lex.rs +662 -0
- data/rust/parser/src/main.rs +252 -0
- data/rust/parser/src/parse/aggregate.rs +612 -0
- data/rust/parser/src/parse/chapter.rs +517 -0
- data/rust/parser/src/parse/command.rs +637 -0
- data/rust/parser/src/parse/domain_port.rs +127 -0
- data/rust/parser/src/parse/entity.rs +527 -0
- data/rust/parser/src/parse/file.rs +67 -0
- data/rust/parser/src/parse/hecksagon.rs +231 -0
- data/rust/parser/src/parse/lifecycle.rs +162 -0
- data/rust/parser/src/parse/mod.rs +1262 -0
- data/rust/parser/src/parse/policy.rs +87 -0
- data/rust/parser/src/parse/process_manager.rs +305 -0
- data/rust/parser/src/parse/query.rs +100 -0
- data/rust/parser/src/parse/read_model.rs +214 -0
- data/rust/parser/src/parse/value_object.rs +250 -0
- data/rust/parser/src/ruby_value.rs +422 -0
- data/rust/project/bridging.rb +257 -0
- data/rust/project/commands.rb +877 -0
- data/rust/project/constraints.rb +86 -0
- data/rust/project/dependency_planning.rb +178 -0
- data/rust/project/domain_generator.rb +980 -0
- data/rust/project/exemplar.rb +148 -0
- data/rust/project/expr_emitter.rb +72 -0
- data/rust/project/fielded.rb +141 -0
- data/rust/project/json_codec.rb +641 -0
- data/rust/project/mutations.rb +721 -0
- data/rust/project/naming.rb +205 -0
- data/rust/project/ports.rb +65 -0
- data/rust/project/queries.rb +493 -0
- data/rust/project/reactions.rb +317 -0
- data/rust/project/read_models.rb +690 -0
- data/rust/project/reference_specs.rb +27 -0
- data/rust/project/registry.rb +396 -0
- data/rust/project/skip_reason.rb +29 -0
- data/rust/project/types.rb +225 -0
- data/rust/project/write_if_changed.rb +83 -0
- data/rust/project.rb +30 -0
- data/rust/project_rust_pipeline.rb +383 -0
- data/rust/src/exemplar/commands.rs +279 -0
- data/rust/src/exemplar/constraints.rs +18 -0
- data/rust/src/exemplar/fielded.rs +280 -0
- data/rust/src/exemplar/json.rs +277 -0
- data/rust/src/exemplar/mod.rs +17 -0
- data/rust/src/exemplar/mutations.rs +80 -0
- data/rust/src/exemplar/queries.rs +33 -0
- data/rust/src/exemplar/reactions.rs +88 -0
- data/rust/src/exemplar/read_models.rs +48 -0
- data/rust/src/exemplar/registry.rs +166 -0
- data/rust/src/exemplar/types.rs +44 -0
- data/rust/src/kernel/attribute_shapes/composite.rs +88 -0
- data/rust/src/kernel/attribute_shapes/list.rs +18 -0
- data/rust/src/kernel/attribute_shapes/mod.rs +44 -0
- data/rust/src/kernel/attribute_shapes/optional.rs +9 -0
- data/rust/src/kernel/attribute_shapes/scalar.rs +39 -0
- data/rust/src/kernel/cli.rs +585 -0
- data/rust/src/kernel/dispatch.rs +1090 -0
- data/rust/src/kernel/expr.rs +458 -0
- data/rust/src/kernel/expression_operators/arithmetic.rs +128 -0
- data/rust/src/kernel/expression_operators/comparison.rs +48 -0
- data/rust/src/kernel/expression_operators/enumeration.rs +232 -0
- data/rust/src/kernel/expression_operators/logical.rs +13 -0
- data/rust/src/kernel/expression_operators/membership.rs +18 -0
- data/rust/src/kernel/expression_operators/mod.rs +68 -0
- data/rust/src/kernel/expression_operators/pattern_match.rs +73 -0
- data/rust/src/kernel/expression_operators/positional.rs +126 -0
- data/rust/src/kernel/expression_operators/presence.rs +101 -0
- data/rust/src/kernel/expression_operators/sign_test.rs +16 -0
- data/rust/src/kernel/expression_operators/sized.rs +35 -0
- data/rust/src/kernel/expression_operators/text.rs +79 -0
- data/rust/src/kernel/expression_operators/to_string.rs +18 -0
- data/rust/src/kernel/json.rs +779 -0
- data/rust/src/kernel/mod.rs +133 -0
- data/rust/src/kernel/named_query.rs +219 -0
- data/rust/src/kernel/orchestrate.rs +1810 -0
- data/rust/src/kernel/pattern.rs +307 -0
- data/rust/src/kernel/query_comparators.rs +206 -0
- data/rust/src/kernel/query_ordering.rs +103 -0
- data/rust/src/kernel/read_model.rs +914 -0
- data/rust/src/kernel/reference_lookup.rs +149 -0
- data/rust/src/kernel/repository.rs +468 -0
- data/rust/src/kernel/routing.rs +612 -0
- data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +118 -0
- data/rust/src/kernel/vocab/entity_dispatch_order.rs +114 -0
- data/rust/src/kernel/vocab/field_hint.rs +70 -0
- data/rust/src/kernel/vocab/mod.rs +20 -0
- data/rust/src/kernel/vocab/query_comparator.rs +65 -0
- data/rust/src/kernel/vocab/refusal_template.rs +1592 -0
- data/rust/src/lib.rs +8 -0
- data/rust/src/main.rs +16 -0
- data/rust/web/.gitignore +1 -0
- data/rust/web/Cargo.lock +119 -0
- data/rust/web/Cargo.toml +18 -0
- data/rust/web/src/lib.rs +8 -0
- metadata +477 -12
- data/lib/hecks/facade/cli_door.rb +0 -96
- data/lib/hecks/facade/cli_runner.rb +0 -127
- data/lib/hecks/mcp_door_scope.rb +0 -95
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
//! Flat, domain-agnostic journal of every command this domain has ever
|
|
2
|
+
//! accepted, replayed in full on each invocation rather than filtered (ADR 0018).
|
|
3
|
+
|
|
4
|
+
use anyhow::Context;
|
|
5
|
+
use sha2::{Digest, Sha256};
|
|
6
|
+
use tokio_postgres::{Client, GenericClient};
|
|
7
|
+
|
|
8
|
+
pub async fn ensure_schema(client: &Client) -> anyhow::Result<()> {
|
|
9
|
+
client
|
|
10
|
+
.batch_execute(
|
|
11
|
+
"CREATE TABLE IF NOT EXISTS hecks_lambda_journal (
|
|
12
|
+
ordinal BIGSERIAL PRIMARY KEY,
|
|
13
|
+
verb TEXT NOT NULL,
|
|
14
|
+
args JSONB NOT NULL,
|
|
15
|
+
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
16
|
+
)",
|
|
17
|
+
)
|
|
18
|
+
.await?;
|
|
19
|
+
// Single-row cache of the kernel's last "instances" output; the
|
|
20
|
+
// `boolean PRIMARY KEY DEFAULT true CHECK (id)` trick enforces at
|
|
21
|
+
// most one row. See `load_snapshot`/`save_snapshot`.
|
|
22
|
+
client
|
|
23
|
+
.batch_execute(
|
|
24
|
+
"CREATE TABLE IF NOT EXISTS hecks_lambda_snapshot (
|
|
25
|
+
id boolean PRIMARY KEY DEFAULT true CHECK (id),
|
|
26
|
+
ordinal bigint NOT NULL,
|
|
27
|
+
seed jsonb NOT NULL
|
|
28
|
+
)",
|
|
29
|
+
)
|
|
30
|
+
.await?;
|
|
31
|
+
// Additive column; `DEFAULT false` makes every pre-existing row read
|
|
32
|
+
// as "not backfilled yet", the correct value with no separate migration.
|
|
33
|
+
client
|
|
34
|
+
.batch_execute(
|
|
35
|
+
"ALTER TABLE hecks_lambda_snapshot ADD COLUMN IF NOT EXISTS sagas_backfilled boolean NOT NULL DEFAULT false",
|
|
36
|
+
)
|
|
37
|
+
.await?;
|
|
38
|
+
// One row per in-flight saga instance (unlike the single-row snapshot
|
|
39
|
+
// above): sagas are numerous and mostly independent. No `ordinal`
|
|
40
|
+
// column — always written inside dispatch::handle's advisory-locked
|
|
41
|
+
// transaction, so plain overwrite-in-place is correct (see dispatch.rs).
|
|
42
|
+
client
|
|
43
|
+
.batch_execute(
|
|
44
|
+
"CREATE TABLE IF NOT EXISTS hecks_lambda_sagas (
|
|
45
|
+
process_manager TEXT NOT NULL,
|
|
46
|
+
correlation TEXT NOT NULL,
|
|
47
|
+
state TEXT NOT NULL,
|
|
48
|
+
memory JSONB NOT NULL,
|
|
49
|
+
PRIMARY KEY (process_manager, correlation)
|
|
50
|
+
)",
|
|
51
|
+
)
|
|
52
|
+
.await?;
|
|
53
|
+
// Additive column, same backfill idiom as `sagas_backfilled` above;
|
|
54
|
+
// `DEFAULT '[]'::jsonb` gives every pre-existing saga row an empty
|
|
55
|
+
// ledger instead of requiring a separate migration.
|
|
56
|
+
client
|
|
57
|
+
.batch_execute(
|
|
58
|
+
"ALTER TABLE hecks_lambda_sagas ADD COLUMN IF NOT EXISTS completed_compensations jsonb NOT NULL DEFAULT '[]'::jsonb",
|
|
59
|
+
)
|
|
60
|
+
.await?;
|
|
61
|
+
// Durable record of a cross-domain delivery that exhausted retries;
|
|
62
|
+
// see `record_dead_letter` below. Flat and domain-agnostic like
|
|
63
|
+
// `hecks_lambda_journal` — one deployed Lambda has only one source domain.
|
|
64
|
+
client
|
|
65
|
+
.batch_execute(
|
|
66
|
+
"CREATE TABLE IF NOT EXISTS hecks_cross_domain_dead_letters (
|
|
67
|
+
id BIGSERIAL PRIMARY KEY,
|
|
68
|
+
policy TEXT NOT NULL,
|
|
69
|
+
target_domain TEXT NOT NULL,
|
|
70
|
+
target_verb TEXT NOT NULL,
|
|
71
|
+
payload JSONB NOT NULL,
|
|
72
|
+
error TEXT NOT NULL,
|
|
73
|
+
attempts INT NOT NULL,
|
|
74
|
+
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
75
|
+
)",
|
|
76
|
+
)
|
|
77
|
+
.await?;
|
|
78
|
+
Ok(())
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Durably records a cross-domain delivery that exhausted every retry.
|
|
82
|
+
/// Written post-commit — a failure here must never roll back the local command.
|
|
83
|
+
pub async fn record_dead_letter<C: GenericClient>(
|
|
84
|
+
client: &C,
|
|
85
|
+
policy: &str,
|
|
86
|
+
target_domain: &str,
|
|
87
|
+
target_verb: &str,
|
|
88
|
+
payload: &serde_json::Value,
|
|
89
|
+
error: &str,
|
|
90
|
+
attempts: i32,
|
|
91
|
+
) -> anyhow::Result<()> {
|
|
92
|
+
client
|
|
93
|
+
.execute(
|
|
94
|
+
"INSERT INTO hecks_cross_domain_dead_letters \
|
|
95
|
+
(policy, target_domain, target_verb, payload, error, attempts) \
|
|
96
|
+
VALUES ($1, $2, $3, $4, $5, $6)",
|
|
97
|
+
&[&policy, &target_domain, &target_verb, payload, &error, &attempts],
|
|
98
|
+
)
|
|
99
|
+
.await?;
|
|
100
|
+
Ok(())
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Every command ever accepted, in dispatch order, as `{"verb","args"}` steps.
|
|
104
|
+
/// Generic over `GenericClient` so callers can run it inside their own transaction.
|
|
105
|
+
pub async fn load_steps<C: GenericClient>(client: &C) -> anyhow::Result<Vec<serde_json::Value>> {
|
|
106
|
+
let rows = client
|
|
107
|
+
.query(
|
|
108
|
+
"SELECT verb, args FROM hecks_lambda_journal ORDER BY ordinal",
|
|
109
|
+
&[],
|
|
110
|
+
)
|
|
111
|
+
.await?;
|
|
112
|
+
|
|
113
|
+
Ok(rows
|
|
114
|
+
.iter()
|
|
115
|
+
.map(|row| {
|
|
116
|
+
let verb: String = row.get(0);
|
|
117
|
+
let args: serde_json::Value = row.get(1);
|
|
118
|
+
serde_json::json!({ "verb": verb, "args": args })
|
|
119
|
+
})
|
|
120
|
+
.collect())
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Steps recorded after `ordinal` — what remains to replay on top of a
|
|
124
|
+
/// snapshot taken as of that ordinal (normally empty).
|
|
125
|
+
pub async fn load_steps_after<C: GenericClient>(client: &C, ordinal: i64) -> anyhow::Result<Vec<serde_json::Value>> {
|
|
126
|
+
let rows = client
|
|
127
|
+
.query(
|
|
128
|
+
"SELECT verb, args FROM hecks_lambda_journal WHERE ordinal > $1 ORDER BY ordinal",
|
|
129
|
+
&[&ordinal],
|
|
130
|
+
)
|
|
131
|
+
.await?;
|
|
132
|
+
|
|
133
|
+
Ok(rows
|
|
134
|
+
.iter()
|
|
135
|
+
.map(|row| {
|
|
136
|
+
let verb: String = row.get(0);
|
|
137
|
+
let args: serde_json::Value = row.get(1);
|
|
138
|
+
serde_json::json!({ "verb": verb, "args": args })
|
|
139
|
+
})
|
|
140
|
+
.collect())
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// The kernel's cached "instances" output as of `ordinal`, used to skip a
|
|
144
|
+
/// full replay from empty state.
|
|
145
|
+
pub struct Snapshot {
|
|
146
|
+
pub ordinal: i64,
|
|
147
|
+
pub seed: serde_json::Value,
|
|
148
|
+
/// One-time latch: false only for a snapshot written before saga
|
|
149
|
+
/// durability shipped, never a live signal about current saga state.
|
|
150
|
+
pub sagas_backfilled: bool,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// The domain's cached snapshot, if one has ever been written. `None`
|
|
154
|
+
/// before the first accepted command, when there's nothing to seed from yet.
|
|
155
|
+
pub async fn load_snapshot<C: GenericClient>(client: &C) -> anyhow::Result<Option<Snapshot>> {
|
|
156
|
+
let rows = client
|
|
157
|
+
.query("SELECT ordinal, seed, sagas_backfilled FROM hecks_lambda_snapshot", &[])
|
|
158
|
+
.await?;
|
|
159
|
+
Ok(rows.first().map(|row| Snapshot { ordinal: row.get(0), seed: row.get(1), sagas_backfilled: row.get(2) }))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Returns the new journal row's ordinal, for `save_snapshot` to pair with it.
|
|
163
|
+
pub async fn append<C: GenericClient>(
|
|
164
|
+
client: &C,
|
|
165
|
+
verb: &str,
|
|
166
|
+
args: &serde_json::Value,
|
|
167
|
+
) -> anyhow::Result<i64> {
|
|
168
|
+
let row = client
|
|
169
|
+
.query_one(
|
|
170
|
+
"INSERT INTO hecks_lambda_journal (verb, args) VALUES ($1, $2) RETURNING ordinal",
|
|
171
|
+
&[&verb, &args],
|
|
172
|
+
)
|
|
173
|
+
.await?;
|
|
174
|
+
Ok(row.get(0))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// Upserts the one-row snapshot cache at `ordinal`. Always sets
|
|
178
|
+
/// `sagas_backfilled = true`; only a row written before that column
|
|
179
|
+
/// existed can ever read back `false`.
|
|
180
|
+
pub async fn save_snapshot<C: GenericClient>(client: &C, ordinal: i64, seed: &serde_json::Value) -> anyhow::Result<()> {
|
|
181
|
+
client
|
|
182
|
+
.execute(
|
|
183
|
+
"INSERT INTO hecks_lambda_snapshot (id, ordinal, seed, sagas_backfilled) VALUES (true, $1, $2, true) \
|
|
184
|
+
ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, seed = EXCLUDED.seed, sagas_backfilled = true",
|
|
185
|
+
&[&ordinal, seed],
|
|
186
|
+
)
|
|
187
|
+
.await?;
|
|
188
|
+
Ok(())
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// One live saga/process-manager instance, as stored in `hecks_lambda_sagas`.
|
|
192
|
+
pub struct SagaRow {
|
|
193
|
+
pub process_manager: String,
|
|
194
|
+
pub correlation: String,
|
|
195
|
+
pub state: String,
|
|
196
|
+
pub memory: serde_json::Value,
|
|
197
|
+
pub completed_compensations: serde_json::Value,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// Every live saga instance for this domain, read once per invocation to
|
|
201
|
+
/// seed the kernel's in-memory `sagas` map.
|
|
202
|
+
pub async fn load_sagas<C: GenericClient>(client: &C) -> anyhow::Result<Vec<SagaRow>> {
|
|
203
|
+
let rows = client
|
|
204
|
+
.query(
|
|
205
|
+
"SELECT process_manager, correlation, state, memory, completed_compensations FROM hecks_lambda_sagas",
|
|
206
|
+
&[],
|
|
207
|
+
)
|
|
208
|
+
.await?;
|
|
209
|
+
Ok(rows
|
|
210
|
+
.into_iter()
|
|
211
|
+
.map(|row| SagaRow {
|
|
212
|
+
process_manager: row.get(0),
|
|
213
|
+
correlation: row.get(1),
|
|
214
|
+
state: row.get(2),
|
|
215
|
+
memory: row.get(3),
|
|
216
|
+
completed_compensations: row.get(4),
|
|
217
|
+
})
|
|
218
|
+
.collect())
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// Upserts one live saga instance's state. Must run in the same
|
|
222
|
+
/// transaction as `append`, never a post-commit connection.
|
|
223
|
+
pub async fn save_saga<C: GenericClient>(
|
|
224
|
+
client: &C,
|
|
225
|
+
process_manager: &str,
|
|
226
|
+
correlation: &str,
|
|
227
|
+
state: &str,
|
|
228
|
+
memory: &serde_json::Value,
|
|
229
|
+
completed_compensations: &serde_json::Value,
|
|
230
|
+
) -> anyhow::Result<()> {
|
|
231
|
+
client
|
|
232
|
+
.execute(
|
|
233
|
+
"INSERT INTO hecks_lambda_sagas (process_manager, correlation, state, memory, completed_compensations) \
|
|
234
|
+
VALUES ($1, $2, $3, $4, $5) \
|
|
235
|
+
ON CONFLICT (process_manager, correlation) DO UPDATE \
|
|
236
|
+
SET state = EXCLUDED.state, memory = EXCLUDED.memory, completed_compensations = EXCLUDED.completed_compensations",
|
|
237
|
+
&[&process_manager, &correlation, &state, memory, completed_compensations],
|
|
238
|
+
)
|
|
239
|
+
.await?;
|
|
240
|
+
Ok(())
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// Removes one saga instance that ended.
|
|
244
|
+
pub async fn delete_saga<C: GenericClient>(
|
|
245
|
+
client: &C,
|
|
246
|
+
process_manager: &str,
|
|
247
|
+
correlation: &str,
|
|
248
|
+
) -> anyhow::Result<()> {
|
|
249
|
+
client
|
|
250
|
+
.execute(
|
|
251
|
+
"DELETE FROM hecks_lambda_sagas WHERE process_manager = $1 AND correlation = $2",
|
|
252
|
+
&[&process_manager, &correlation],
|
|
253
|
+
)
|
|
254
|
+
.await?;
|
|
255
|
+
Ok(())
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// Which domain journal this binary writes as, and which era. Read from
|
|
259
|
+
/// `HECKS_DOMAIN`/main.rs's boot-time mint decision, never computed here.
|
|
260
|
+
pub struct LineageConfig {
|
|
261
|
+
pub domain: String,
|
|
262
|
+
/// `None` when lineage isn't provisioned for this domain (ADR 0034).
|
|
263
|
+
pub era: Option<i32>,
|
|
264
|
+
/// Aggregates mirrored into era-shaped head snapshots; `None` means all.
|
|
265
|
+
pub mirrored: Option<std::collections::BTreeSet<String>>,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
impl LineageConfig {
|
|
269
|
+
/// Whether `qualified_aggregate` is mirrored into the era-shaped head
|
|
270
|
+
/// snapshots.
|
|
271
|
+
pub fn mirrors(&self, qualified_aggregate: &str) -> bool {
|
|
272
|
+
match &self.mirrored {
|
|
273
|
+
None => true,
|
|
274
|
+
Some(capable) => capable.contains(qualified_aggregate),
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/// Ports `Naming.snake` (lib/hecks/naming.rb) verbatim — must match Ruby
|
|
280
|
+
/// exactly so both runtimes derive the same storage names.
|
|
281
|
+
pub fn snake(name: &str) -> String {
|
|
282
|
+
word_boundary(&acronym_boundary(name)).to_ascii_lowercase()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
fn acronym_boundary(s: &str) -> String {
|
|
286
|
+
let chars: Vec<char> = s.chars().collect();
|
|
287
|
+
let mut out = String::new();
|
|
288
|
+
let mut i = 0;
|
|
289
|
+
while i < chars.len() {
|
|
290
|
+
if chars[i].is_ascii_uppercase() {
|
|
291
|
+
let mut j = i;
|
|
292
|
+
while j < chars.len() && chars[j].is_ascii_uppercase() {
|
|
293
|
+
j += 1;
|
|
294
|
+
}
|
|
295
|
+
let run_len = j - i;
|
|
296
|
+
if run_len >= 2 && j < chars.len() && chars[j].is_ascii_lowercase() {
|
|
297
|
+
out.extend(&chars[i..j - 1]);
|
|
298
|
+
out.push('_');
|
|
299
|
+
out.push(chars[j - 1]);
|
|
300
|
+
i = j;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
out.extend(&chars[i..j]);
|
|
304
|
+
i = j;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
out.push(chars[i]);
|
|
308
|
+
i += 1;
|
|
309
|
+
}
|
|
310
|
+
out
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
fn word_boundary(s: &str) -> String {
|
|
314
|
+
let chars: Vec<char> = s.chars().collect();
|
|
315
|
+
let mut out = String::new();
|
|
316
|
+
for (idx, &c) in chars.iter().enumerate() {
|
|
317
|
+
if idx > 0 && c.is_ascii_uppercase() {
|
|
318
|
+
let prev = chars[idx - 1];
|
|
319
|
+
if prev.is_ascii_lowercase() || prev.is_ascii_digit() {
|
|
320
|
+
out.push('_');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
out.push(c);
|
|
324
|
+
}
|
|
325
|
+
out
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/// `aggregate.storage_name` ported from ir/aggregate.rb — demodulizes a
|
|
329
|
+
/// qualified name like `"Banking::Customer"` to `"Customer"` before snaking it.
|
|
330
|
+
fn storage_name(qualified_aggregate: &str) -> String {
|
|
331
|
+
let short = qualified_aggregate.rsplit("::").next().unwrap_or(qualified_aggregate);
|
|
332
|
+
snake(short)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/// Quotes a Postgres identifier `PG::Connection.quote_ident`-style —
|
|
336
|
+
/// needed since table names can't be bound as query parameters.
|
|
337
|
+
pub(crate) fn quote_ident(name: &str) -> String {
|
|
338
|
+
format!("\"{}\"", name.replace('"', "\"\""))
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
fn journal_table(domain: &str) -> String {
|
|
342
|
+
format!("hecks_journal_{}", snake(domain))
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/// Postgres's NAMEDATALEN-1 limit — an identifier over this length is
|
|
346
|
+
/// silently truncated, never refused, so two overlong names sharing a
|
|
347
|
+
/// prefix would otherwise collide.
|
|
348
|
+
const POSTGRES_IDENTIFIER_LIMIT: usize = 63;
|
|
349
|
+
|
|
350
|
+
/// Same algorithm as `Lineage#qualified_name` (lineage.rb): 63-byte limit,
|
|
351
|
+
/// SHA256 suffix, domain-qualified so two domains' storage names can't
|
|
352
|
+
/// collide (ADR 0059).
|
|
353
|
+
pub(crate) fn qualified_name(domain: &str, suffix: &str) -> String {
|
|
354
|
+
let full = format!("{}_{}", snake(domain), suffix);
|
|
355
|
+
if full.len() <= POSTGRES_IDENTIFIER_LIMIT {
|
|
356
|
+
return full;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
let digest = format!("{:x}", Sha256::digest(full.as_bytes()));
|
|
360
|
+
let digest = &digest[0..8];
|
|
361
|
+
let keep = POSTGRES_IDENTIFIER_LIMIT - digest.len() - 1;
|
|
362
|
+
format!("{}_{}", &full[0..keep], digest)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
fn head_snapshot_table(domain: &str, qualified_aggregate: &str, era: i32) -> String {
|
|
366
|
+
qualified_name(domain, &format!("{}_head_snapshot_{}", storage_name(qualified_aggregate), era))
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/// The highest era ordinal on file for `domain`, not a bare existence
|
|
370
|
+
/// check — `hecks_eras` is append-only, so a stale checkout still has a row.
|
|
371
|
+
pub async fn current_era(client: &Client, domain: &str) -> anyhow::Result<Option<i32>> {
|
|
372
|
+
let rows = client
|
|
373
|
+
.query(
|
|
374
|
+
"SELECT ordinal FROM hecks_eras WHERE domain = $1 ORDER BY ordinal DESC LIMIT 1",
|
|
375
|
+
&[&domain],
|
|
376
|
+
)
|
|
377
|
+
.await?;
|
|
378
|
+
Ok(rows.first().map(|row| row.get(0)))
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/// One `MutationRecord` from the kernel's JSON output, parsed generically
|
|
382
|
+
/// since rust/host never links the kernel crate directly (ADR 0012).
|
|
383
|
+
pub struct Mutation<'a> {
|
|
384
|
+
pub aggregate: &'a str,
|
|
385
|
+
pub id: &'a str,
|
|
386
|
+
pub operation: &'a str,
|
|
387
|
+
pub state: &'a serde_json::Value,
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/// Mirrors `PostgresEra#append`'s two-step write: journal insert, then a
|
|
391
|
+
/// head-snapshot upsert that never regresses on a stale/reordered write.
|
|
392
|
+
pub async fn append_lineage_mutation<C: GenericClient>(
|
|
393
|
+
client: &C,
|
|
394
|
+
config: &LineageConfig,
|
|
395
|
+
mutation: &Mutation<'_>,
|
|
396
|
+
) -> anyhow::Result<()> {
|
|
397
|
+
// ADR 0034 — the one chokepoint every lineage write passes through;
|
|
398
|
+
// dispatch::handle already checks `era.is_some()` for ordinary
|
|
399
|
+
// commands, so this bail! is the backstop for other callers.
|
|
400
|
+
let Some(era) = config.era else {
|
|
401
|
+
anyhow::bail!(
|
|
402
|
+
"cannot append a lineage mutation for {}::{}: no era is active — the lineage subsystem was never \
|
|
403
|
+
initialized for this domain",
|
|
404
|
+
config.domain,
|
|
405
|
+
mutation.aggregate
|
|
406
|
+
);
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
if mutation.operation != "save" {
|
|
410
|
+
anyhow::bail!(
|
|
411
|
+
"hecks_journal_{}: unsupported operation {:?} — only \"save\" is generated today",
|
|
412
|
+
snake(&config.domain),
|
|
413
|
+
mutation.operation
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
let journal = journal_table(&config.domain);
|
|
418
|
+
let snapshot = head_snapshot_table(&config.domain, mutation.aggregate, era);
|
|
419
|
+
let storage = storage_name(mutation.aggregate);
|
|
420
|
+
|
|
421
|
+
// Named per-statement: tokio_postgres's Display for a db error is
|
|
422
|
+
// just "db error" — the real message is on `source()`, which
|
|
423
|
+
// `main.rs` surfaces via `{:#}` formatting.
|
|
424
|
+
let row = client
|
|
425
|
+
.query_one(
|
|
426
|
+
&format!(
|
|
427
|
+
"INSERT INTO {} (era, aggregate, aggregate_id, operation, state) \
|
|
428
|
+
VALUES ($1, $2, $3, $4, $5) RETURNING ordinal",
|
|
429
|
+
quote_ident(&journal)
|
|
430
|
+
),
|
|
431
|
+
&[&era, &storage, &mutation.id, &mutation.operation, &mutation.state],
|
|
432
|
+
)
|
|
433
|
+
.await
|
|
434
|
+
.with_context(|| format!("journalling {} #{} into {journal} at era {era}", mutation.aggregate, mutation.id))?;
|
|
435
|
+
let ordinal: i64 = row.get(0);
|
|
436
|
+
|
|
437
|
+
client
|
|
438
|
+
.execute(
|
|
439
|
+
&format!(
|
|
440
|
+
"INSERT INTO {snap} (id, ordinal, state) VALUES ($1, $2, $3) \
|
|
441
|
+
ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, state = EXCLUDED.state \
|
|
442
|
+
WHERE {snap}.ordinal < EXCLUDED.ordinal",
|
|
443
|
+
snap = quote_ident(&snapshot)
|
|
444
|
+
),
|
|
445
|
+
&[&mutation.id, &ordinal, &mutation.state],
|
|
446
|
+
)
|
|
447
|
+
.await
|
|
448
|
+
.with_context(|| format!("upserting {} #{} into {snapshot}", mutation.aggregate, mutation.id))?;
|
|
449
|
+
|
|
450
|
+
Ok(())
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Not wired into dispatch::handle's replay seed — overlaying translated
|
|
454
|
+
// head state onto raw replayed steps would cause false "AlreadyExists" refusals.
|
|
455
|
+
pub(crate) fn head_view(domain: &str, storage_name: &str) -> String {
|
|
456
|
+
qualified_name(domain, &format!("{storage_name}_head"))
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/// Every row for one lineage-capable aggregate, already translated to its
|
|
460
|
+
/// current shape (reads the same head view Ruby's `Adapters::Postgres` does).
|
|
461
|
+
pub async fn read_lineage_head_all<C: GenericClient>(
|
|
462
|
+
client: &C,
|
|
463
|
+
domain: &str,
|
|
464
|
+
storage_name: &str,
|
|
465
|
+
) -> anyhow::Result<Vec<(String, serde_json::Value)>> {
|
|
466
|
+
let rows = client
|
|
467
|
+
.query(&format!("SELECT id, state FROM {}", quote_ident(&head_view(domain, storage_name))), &[])
|
|
468
|
+
.await?;
|
|
469
|
+
Ok(rows.into_iter().map(|row| (row.get(0), row.get(1))).collect())
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/// One row by id, already translated to its current shape.
|
|
473
|
+
pub async fn read_lineage_head_by_id<C: GenericClient>(
|
|
474
|
+
client: &C,
|
|
475
|
+
domain: &str,
|
|
476
|
+
storage_name: &str,
|
|
477
|
+
id: &str,
|
|
478
|
+
) -> anyhow::Result<Option<serde_json::Value>> {
|
|
479
|
+
let row = client
|
|
480
|
+
.query_opt(
|
|
481
|
+
&format!("SELECT state FROM {} WHERE id = $1", quote_ident(&head_view(domain, storage_name))),
|
|
482
|
+
&[&id],
|
|
483
|
+
)
|
|
484
|
+
.await?;
|
|
485
|
+
Ok(row.map(|r| r.get(0)))
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/// One row of `hecks_eras`, mirroring era_store.rb's `eras` shape.
|
|
489
|
+
#[derive(Debug, Clone)]
|
|
490
|
+
pub struct HeldEra {
|
|
491
|
+
pub ordinal: i32,
|
|
492
|
+
pub hash: Option<String>,
|
|
493
|
+
pub label: Option<String>,
|
|
494
|
+
pub held_text: String,
|
|
495
|
+
pub watermark: Option<i64>,
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/// Every held era for `domain`, each verified against its own digest — an
|
|
499
|
+
/// edited `held_text` refuses loudly instead of silently reporting no drift.
|
|
500
|
+
pub async fn held_eras<C: GenericClient>(client: &C, domain: &str) -> anyhow::Result<Vec<HeldEra>> {
|
|
501
|
+
let rows = client
|
|
502
|
+
.query(
|
|
503
|
+
"SELECT ordinal, hash, label, held_text, watermark, held_digest FROM hecks_eras \
|
|
504
|
+
WHERE domain = $1 ORDER BY ordinal",
|
|
505
|
+
&[&domain],
|
|
506
|
+
)
|
|
507
|
+
.await?;
|
|
508
|
+
|
|
509
|
+
rows.into_iter()
|
|
510
|
+
.map(|row| {
|
|
511
|
+
let ordinal: i32 = row.get("ordinal");
|
|
512
|
+
let held_text: String = row.get("held_text");
|
|
513
|
+
let held_digest: Option<String> = row.get("held_digest");
|
|
514
|
+
|
|
515
|
+
if let Some(stored) = &held_digest {
|
|
516
|
+
let digest = format!("{:x}", Sha256::digest(held_text.as_bytes()));
|
|
517
|
+
if &digest != stored {
|
|
518
|
+
anyhow::bail!(
|
|
519
|
+
"cannot boot {domain}: era {ordinal}'s held text does not match its own recorded digest \
|
|
520
|
+
— this row was edited outside the lineage tooling; run hecks reattest to acknowledge \
|
|
521
|
+
the change and re-seal it (Runtime::EraTamper's own recovery path)"
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
Ok(HeldEra {
|
|
527
|
+
ordinal,
|
|
528
|
+
hash: row.get("hash"),
|
|
529
|
+
label: row.get("label"),
|
|
530
|
+
held_text,
|
|
531
|
+
watermark: row.get("watermark"),
|
|
532
|
+
})
|
|
533
|
+
})
|
|
534
|
+
.collect()
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/// A Layer-3 approval, as recorded — mirrors era_store.rb's `approval_for`.
|
|
538
|
+
#[derive(Debug, Clone)]
|
|
539
|
+
pub struct Approval {
|
|
540
|
+
pub edge_digest: String,
|
|
541
|
+
pub reviewed_ordinal: i64,
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/// The latest approval for one shape-pair edge, if any (latest wins).
|
|
545
|
+
pub async fn approval_for<C: GenericClient>(
|
|
546
|
+
client: &C,
|
|
547
|
+
domain: &str,
|
|
548
|
+
from_label: &str,
|
|
549
|
+
to_label: &str,
|
|
550
|
+
) -> anyhow::Result<Option<Approval>> {
|
|
551
|
+
let row = client
|
|
552
|
+
.query_opt(
|
|
553
|
+
"SELECT edge_digest, reviewed_ordinal FROM hecks_approvals \
|
|
554
|
+
WHERE domain = $1 AND from_label = $2 AND to_label = $3 \
|
|
555
|
+
ORDER BY approved_at DESC, reviewed_ordinal DESC LIMIT 1",
|
|
556
|
+
&[&domain, &from_label, &to_label],
|
|
557
|
+
)
|
|
558
|
+
.await?;
|
|
559
|
+
|
|
560
|
+
Ok(row.map(|row| Approval { edge_digest: row.get("edge_digest"), reviewed_ordinal: row.get("reviewed_ordinal") }))
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/// Writes an approval into the journal, bound to the journal's tip as it stands, as
|
|
564
|
+
/// `LineageStore#record_approval!` does; how a committed approval joins the single history.
|
|
565
|
+
pub async fn record_approval<C: GenericClient>(
|
|
566
|
+
client: &C,
|
|
567
|
+
domain: &str,
|
|
568
|
+
from_label: &str,
|
|
569
|
+
to_label: &str,
|
|
570
|
+
edge_digest: &str,
|
|
571
|
+
) -> anyhow::Result<()> {
|
|
572
|
+
let tip = last_ordinal(client, domain).await?;
|
|
573
|
+
client
|
|
574
|
+
.execute(
|
|
575
|
+
"INSERT INTO hecks_approvals (domain, from_label, to_label, edge_digest, reviewed_ordinal) \
|
|
576
|
+
VALUES ($1, $2, $3, $4, $5)",
|
|
577
|
+
&[&domain, &from_label, &to_label, &edge_digest, &tip],
|
|
578
|
+
)
|
|
579
|
+
.await?;
|
|
580
|
+
Ok(())
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/// The journal's high-water ordinal — what a fresh approval binds to and
|
|
584
|
+
/// what a mint transaction captures as the new era's watermark.
|
|
585
|
+
pub async fn last_ordinal<C: GenericClient>(client: &C, domain: &str) -> anyhow::Result<i64> {
|
|
586
|
+
let row = client
|
|
587
|
+
.query_one(&format!("SELECT COALESCE(max(ordinal), 0) AS o FROM {}", quote_ident(&journal_table(domain))), &[])
|
|
588
|
+
.await?;
|
|
589
|
+
Ok(row.get("o"))
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
#[cfg(test)]
|
|
593
|
+
mod lineage_tests {
|
|
594
|
+
use super::*;
|
|
595
|
+
use tokio_postgres::NoTls;
|
|
596
|
+
|
|
597
|
+
// Mints a real era 2 via Ruby's LineageManager (not this crate's own
|
|
598
|
+
// writes) against a scratch DB with a genuinely fenced app role — so
|
|
599
|
+
// this proves staleness is refused by Postgres RLS, not a check here.
|
|
600
|
+
#[tokio::test]
|
|
601
|
+
async fn a_stale_era_write_is_refused_by_postgres_rls_not_this_crate() {
|
|
602
|
+
let db = "rust_host_rls_test";
|
|
603
|
+
let owner_role = "rust_host_rls_owner";
|
|
604
|
+
let app_role = "rust_host_rls_app";
|
|
605
|
+
|
|
606
|
+
let script = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/mint_stale_era.rb");
|
|
607
|
+
let status = std::process::Command::new("ruby")
|
|
608
|
+
.arg(&script)
|
|
609
|
+
.arg(db)
|
|
610
|
+
.arg(owner_role)
|
|
611
|
+
.arg(app_role)
|
|
612
|
+
.status()
|
|
613
|
+
.expect("run mint_stale_era.rb -- is `ruby` on PATH?");
|
|
614
|
+
assert!(status.success(), "mint_stale_era.rb failed -- see its own stderr above");
|
|
615
|
+
|
|
616
|
+
// The fenced app role — the same connection shape a real deployed
|
|
617
|
+
// rust/host uses (never the table-owning role, which bypasses RLS).
|
|
618
|
+
let (client, connection) =
|
|
619
|
+
tokio_postgres::connect(&crate::test_pg::conninfo_as(&db, &app_role), NoTls)
|
|
620
|
+
.await
|
|
621
|
+
.expect("connect as the app role");
|
|
622
|
+
tokio::spawn(async move {
|
|
623
|
+
let _ = connection.await;
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
let state = serde_json::json!({ "cents": 100 });
|
|
627
|
+
|
|
628
|
+
// The era this checkout speaks -- allowed.
|
|
629
|
+
let current_era = LineageConfig { domain: "Ledger".to_string(), era: Some(2), mirrored: None };
|
|
630
|
+
let accepted = append_lineage_mutation(
|
|
631
|
+
&client,
|
|
632
|
+
¤t_era,
|
|
633
|
+
&Mutation { aggregate: "Account", id: "current-era", operation: "save", state: &state },
|
|
634
|
+
)
|
|
635
|
+
.await;
|
|
636
|
+
assert!(accepted.is_ok(), "writing under the CURRENT era should succeed: {accepted:?}");
|
|
637
|
+
|
|
638
|
+
// The superseded era -- refused by Postgres's own RLS policy.
|
|
639
|
+
let stale_era = LineageConfig { domain: "Ledger".to_string(), era: Some(1), mirrored: None };
|
|
640
|
+
let refused = append_lineage_mutation(
|
|
641
|
+
&client,
|
|
642
|
+
&stale_era,
|
|
643
|
+
&Mutation { aggregate: "Account", id: "stale-era", operation: "save", state: &state },
|
|
644
|
+
)
|
|
645
|
+
.await;
|
|
646
|
+
assert!(refused.is_err(), "writing under a SUPERSEDED era should be refused");
|
|
647
|
+
let message = format!("{:#}", refused.unwrap_err()).to_lowercase();
|
|
648
|
+
assert!(
|
|
649
|
+
message.contains("row-level security") || message.contains("row level security"),
|
|
650
|
+
"the refusal should be Postgres's RLS policy specifically, not some other error: {message}"
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Proven against a real compute-migrated era Ruby minted, not a
|
|
655
|
+
// synthetic fixture, so a real hecks_approvals row is guaranteed
|
|
656
|
+
// to exist by the time this test reads it back.
|
|
657
|
+
#[tokio::test]
|
|
658
|
+
async fn held_eras_and_approval_for_read_exactly_what_ruby_s_own_mint_wrote() {
|
|
659
|
+
let db = "rust_host_era_read_test";
|
|
660
|
+
let owner_role = "rust_host_era_read_owner";
|
|
661
|
+
let app_role = "rust_host_era_read_app";
|
|
662
|
+
|
|
663
|
+
let script = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/mint_and_seed_lineage_compute.rb");
|
|
664
|
+
let output = std::process::Command::new("ruby")
|
|
665
|
+
.arg(&script)
|
|
666
|
+
.arg(db)
|
|
667
|
+
.arg(owner_role)
|
|
668
|
+
.arg(app_role)
|
|
669
|
+
.output()
|
|
670
|
+
.expect("run mint_and_seed_lineage_compute.rb -- is `ruby` on PATH?");
|
|
671
|
+
assert!(
|
|
672
|
+
output.status.success(),
|
|
673
|
+
"mint_and_seed_lineage_compute.rb failed:\n{}",
|
|
674
|
+
String::from_utf8_lossy(&output.stderr)
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
// Owner-authenticated: hecks_eras/hecks_approvals aren't grantable
|
|
678
|
+
// to the app role by design — only the mint path reads them, not
|
|
679
|
+
// the RLS-fenced data path the test above exists to prove.
|
|
680
|
+
let (client, connection) = tokio_postgres::connect(&crate::test_pg::conninfo_as(&db, &owner_role), NoTls)
|
|
681
|
+
.await
|
|
682
|
+
.expect("connect as owner");
|
|
683
|
+
tokio::spawn(async move {
|
|
684
|
+
let _ = connection.await;
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
let eras = held_eras(&client, "LedgerCompute").await.expect("held_eras");
|
|
688
|
+
assert_eq!(eras.len(), 2, "expected exactly the two eras mint_and_seed_lineage_compute.rb mints: {eras:?}");
|
|
689
|
+
assert_eq!(eras[0].ordinal, 1);
|
|
690
|
+
assert_eq!(eras[1].ordinal, 2);
|
|
691
|
+
let from_label = eras[0].label.clone().expect("era 1 has a minted label");
|
|
692
|
+
let to_label = eras[1].label.clone().expect("era 2 has a minted label");
|
|
693
|
+
assert_ne!(from_label, to_label, "a real compute edge names two DIFFERENT shapes");
|
|
694
|
+
|
|
695
|
+
let approval = approval_for(&client, "LedgerCompute", &from_label, &to_label)
|
|
696
|
+
.await
|
|
697
|
+
.expect("approval_for")
|
|
698
|
+
.expect("the real approval mint_and_seed_lineage_compute.rb recorded should still be readable");
|
|
699
|
+
assert!(!approval.edge_digest.is_empty());
|
|
700
|
+
// reviewed_ordinal binds to the journal's high-water mark at
|
|
701
|
+
// approval time; era 1's two prior writes mean it must be >= 2.
|
|
702
|
+
assert!(approval.reviewed_ordinal >= 2, "reviewed_ordinal should reflect era 1's real writes: {}", approval.reviewed_ordinal);
|
|
703
|
+
|
|
704
|
+
// last_ordinal must have advanced past the approval's own
|
|
705
|
+
// reviewed_ordinal by the one post-mint write.
|
|
706
|
+
let ordinal = last_ordinal(&client, "LedgerCompute").await.expect("last_ordinal");
|
|
707
|
+
assert!(ordinal > approval.reviewed_ordinal, "the journal should have advanced since the approval was reviewed: {ordinal} vs {}", approval.reviewed_ordinal);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// The boot-gate case the RLS test doesn't cover: this crate's own
|
|
711
|
+
// read, before any write is attempted. Proves `current_era` tells a
|
|
712
|
+
// genuinely-unminted domain apart from a minted-but-stale one, which
|
|
713
|
+
// a bare existence check can't.
|
|
714
|
+
#[tokio::test]
|
|
715
|
+
async fn current_era_tells_unminted_apart_from_stale_and_finds_the_live_ordinal() {
|
|
716
|
+
let (client, connection) = tokio_postgres::connect(&crate::test_pg::conninfo("postgres"), NoTls)
|
|
717
|
+
.await
|
|
718
|
+
.expect("connect to postgres");
|
|
719
|
+
tokio::spawn(async move {
|
|
720
|
+
let _ = connection.await;
|
|
721
|
+
});
|
|
722
|
+
client
|
|
723
|
+
.batch_execute("CREATE TABLE IF NOT EXISTS hecks_eras (domain text, ordinal int)")
|
|
724
|
+
.await
|
|
725
|
+
.unwrap();
|
|
726
|
+
client
|
|
727
|
+
.batch_execute("DELETE FROM hecks_eras WHERE domain = 'CurrentEraFixture'")
|
|
728
|
+
.await
|
|
729
|
+
.unwrap();
|
|
730
|
+
|
|
731
|
+
// A domain hecks_eras has never heard of at all.
|
|
732
|
+
let unminted = current_era(&client, "CurrentEraFixture").await.unwrap();
|
|
733
|
+
assert_eq!(unminted, None, "a domain with no hecks_eras rows at all has no current era");
|
|
734
|
+
|
|
735
|
+
// Era 1 minted, then era 2 supersedes it; hecks_eras is
|
|
736
|
+
// append-only, so both rows remain on file.
|
|
737
|
+
client
|
|
738
|
+
.execute(
|
|
739
|
+
"INSERT INTO hecks_eras (domain, ordinal) VALUES ($1, 1)",
|
|
740
|
+
&[&"CurrentEraFixture"],
|
|
741
|
+
)
|
|
742
|
+
.await
|
|
743
|
+
.unwrap();
|
|
744
|
+
assert_eq!(
|
|
745
|
+
current_era(&client, "CurrentEraFixture").await.unwrap(),
|
|
746
|
+
Some(1),
|
|
747
|
+
"with only era 1 on file, era 1 is current"
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
client
|
|
751
|
+
.execute(
|
|
752
|
+
"INSERT INTO hecks_eras (domain, ordinal) VALUES ($1, 2)",
|
|
753
|
+
&[&"CurrentEraFixture"],
|
|
754
|
+
)
|
|
755
|
+
.await
|
|
756
|
+
.unwrap();
|
|
757
|
+
assert_eq!(
|
|
758
|
+
current_era(&client, "CurrentEraFixture").await.unwrap(),
|
|
759
|
+
Some(2),
|
|
760
|
+
"era 1's row is still on file, but era 2 is now the live one -- \
|
|
761
|
+
a bare existence check on era 1 would have missed that it's stale"
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Proven against an aggregate that isn't Member — the whole point of
|
|
766
|
+
// extracting `member_row_by_email`/`member_rows`'s query into these
|
|
767
|
+
// generic functions. Builds the head view by hand rather than
|
|
768
|
+
// reproducing Ruby's mint path; that correctness is lineage_spec.rb's job.
|
|
769
|
+
#[tokio::test]
|
|
770
|
+
async fn read_lineage_head_reads_any_aggregates_view_generically() {
|
|
771
|
+
let (client, connection) = tokio_postgres::connect(&crate::test_pg::conninfo("postgres"), NoTls)
|
|
772
|
+
.await
|
|
773
|
+
.expect("connect to postgres");
|
|
774
|
+
tokio::spawn(async move {
|
|
775
|
+
let _ = connection.await;
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// Domain-qualified (ADR 0059); snake("Fixtures") == "fixtures".
|
|
779
|
+
client.batch_execute("DROP VIEW IF EXISTS fixtures_widget_head").await.unwrap();
|
|
780
|
+
client.batch_execute("DROP TABLE IF EXISTS fixtures_widget_head_snapshot_1").await.unwrap();
|
|
781
|
+
client
|
|
782
|
+
.batch_execute(
|
|
783
|
+
"CREATE TABLE fixtures_widget_head_snapshot_1 (id text PRIMARY KEY, ordinal bigint NOT NULL, state jsonb NOT NULL)",
|
|
784
|
+
)
|
|
785
|
+
.await
|
|
786
|
+
.unwrap();
|
|
787
|
+
client
|
|
788
|
+
.batch_execute("CREATE VIEW fixtures_widget_head AS SELECT id, state FROM fixtures_widget_head_snapshot_1")
|
|
789
|
+
.await
|
|
790
|
+
.unwrap();
|
|
791
|
+
|
|
792
|
+
let config = LineageConfig { domain: "Fixtures".to_string(), era: Some(1), mirrored: None };
|
|
793
|
+
client
|
|
794
|
+
.batch_execute("CREATE TABLE IF NOT EXISTS hecks_journal_fixtures (ordinal bigserial PRIMARY KEY, era int NOT NULL, aggregate text NOT NULL, aggregate_id text NOT NULL, operation text NOT NULL, state jsonb)")
|
|
795
|
+
.await
|
|
796
|
+
.unwrap();
|
|
797
|
+
append_lineage_mutation(
|
|
798
|
+
&client,
|
|
799
|
+
&config,
|
|
800
|
+
&Mutation { aggregate: "Widget", id: "widget-1", operation: "save", state: &serde_json::json!({ "name": "Gadget" }) },
|
|
801
|
+
)
|
|
802
|
+
.await
|
|
803
|
+
.unwrap();
|
|
804
|
+
append_lineage_mutation(
|
|
805
|
+
&client,
|
|
806
|
+
&config,
|
|
807
|
+
&Mutation { aggregate: "Widget", id: "widget-2", operation: "save", state: &serde_json::json!({ "name": "Gizmo" }) },
|
|
808
|
+
)
|
|
809
|
+
.await
|
|
810
|
+
.unwrap();
|
|
811
|
+
|
|
812
|
+
let one = read_lineage_head_by_id(&client, "Fixtures", "widget", "widget-1").await.unwrap();
|
|
813
|
+
assert_eq!(one, Some(serde_json::json!({ "name": "Gadget" })));
|
|
814
|
+
|
|
815
|
+
let missing = read_lineage_head_by_id(&client, "Fixtures", "widget", "widget-nonexistent").await.unwrap();
|
|
816
|
+
assert_eq!(missing, None);
|
|
817
|
+
|
|
818
|
+
let mut all = read_lineage_head_all(&client, "Fixtures", "widget").await.unwrap();
|
|
819
|
+
all.sort_by(|a, b| a.0.cmp(&b.0));
|
|
820
|
+
assert_eq!(
|
|
821
|
+
all,
|
|
822
|
+
vec![
|
|
823
|
+
("widget-1".to_string(), serde_json::json!({ "name": "Gadget" })),
|
|
824
|
+
("widget-2".to_string(), serde_json::json!({ "name": "Gizmo" })),
|
|
825
|
+
]
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
#[tokio::test]
|
|
830
|
+
async fn record_dead_letter_writes_a_real_durable_row() {
|
|
831
|
+
let (client, connection) = tokio_postgres::connect(&crate::test_pg::conninfo("postgres"), NoTls)
|
|
832
|
+
.await
|
|
833
|
+
.expect("connect to postgres");
|
|
834
|
+
tokio::spawn(async move {
|
|
835
|
+
let _ = connection.await;
|
|
836
|
+
});
|
|
837
|
+
ensure_schema(&client).await.unwrap();
|
|
838
|
+
client.batch_execute("DELETE FROM hecks_cross_domain_dead_letters").await.unwrap();
|
|
839
|
+
|
|
840
|
+
record_dead_letter(
|
|
841
|
+
&client,
|
|
842
|
+
"ReviewOnFreeze",
|
|
843
|
+
"Compliance",
|
|
844
|
+
"Compliance::AccountFreezeReview.Open",
|
|
845
|
+
&serde_json::json!({ "number": { "value": "acct-1" } }),
|
|
846
|
+
"ResourceNotFoundException: function not found",
|
|
847
|
+
3,
|
|
848
|
+
)
|
|
849
|
+
.await
|
|
850
|
+
.unwrap();
|
|
851
|
+
|
|
852
|
+
let rows = client
|
|
853
|
+
.query(
|
|
854
|
+
"SELECT policy, target_domain, target_verb, payload, error, attempts FROM hecks_cross_domain_dead_letters",
|
|
855
|
+
&[],
|
|
856
|
+
)
|
|
857
|
+
.await
|
|
858
|
+
.unwrap();
|
|
859
|
+
assert_eq!(rows.len(), 1, "should have written exactly one row");
|
|
860
|
+
let row = &rows[0];
|
|
861
|
+
let policy: String = row.get(0);
|
|
862
|
+
let target_domain: String = row.get(1);
|
|
863
|
+
let target_verb: String = row.get(2);
|
|
864
|
+
let payload: serde_json::Value = row.get(3);
|
|
865
|
+
let error: String = row.get(4);
|
|
866
|
+
let attempts: i32 = row.get(5);
|
|
867
|
+
assert_eq!(policy, "ReviewOnFreeze");
|
|
868
|
+
assert_eq!(target_domain, "Compliance");
|
|
869
|
+
assert_eq!(target_verb, "Compliance::AccountFreezeReview.Open");
|
|
870
|
+
assert_eq!(payload, serde_json::json!({ "number": { "value": "acct-1" } }));
|
|
871
|
+
assert_eq!(error, "ResourceNotFoundException: function not found");
|
|
872
|
+
assert_eq!(attempts, 3);
|
|
873
|
+
}
|
|
874
|
+
}
|