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,697 @@
|
|
|
1
|
+
//! Port of `rust/project/read_models.rb`, mirrored function for function —
|
|
2
|
+
//! read that file's own header for the full algorithm.
|
|
3
|
+
|
|
4
|
+
use crate::exemplar::Exemplar;
|
|
5
|
+
use crate::json::Json;
|
|
6
|
+
use crate::queries;
|
|
7
|
+
use crate::skip_reason::{reskip, skip, SkipReason};
|
|
8
|
+
use std::collections::HashMap;
|
|
9
|
+
|
|
10
|
+
const READ_MODEL_BARE_KEYS: &[&str] = &[
|
|
11
|
+
"name", "description", "reference_name", "reference_target", "query_name", "aggregate_heads", "wheres", "order_by", "offset", "limit", "freshness", "index_hints", "group_by", "null_semantics", "authorization", "count", "median_field",
|
|
12
|
+
"sum_field", "avg_field", "min_field", "max_field", "percentile_field", "percentile_at", "any_field", "all_field",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
// Every reduction beyond `count` (which needs no field) — mirrors
|
|
16
|
+
// Runtime::ReadModelInterpreter::REDUCTION_WORD/INTEGER_ONLY_REDUCTIONS/BOOLEAN_REDUCTIONS
|
|
17
|
+
// (ADR 0078).
|
|
18
|
+
const AGGREGATION_FIELD_KEYS: &[&str] = &["median_field", "sum_field", "avg_field", "min_field", "max_field", "percentile_field", "any_field", "all_field"];
|
|
19
|
+
const INTEGER_ONLY_AGGREGATION_FIELDS: &[&str] = &["sum_field", "avg_field"];
|
|
20
|
+
const BOOLEAN_AGGREGATION_FIELDS: &[&str] = &["any_field", "all_field"];
|
|
21
|
+
|
|
22
|
+
pub fn read_model_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String, &Json>, unsupported_names: &[String]) -> Option<SkipReason> {
|
|
23
|
+
let keys: Vec<&str> = match read_model {
|
|
24
|
+
Json::Object(pairs) => pairs.iter().filter(|(_, v)| !matches!(v, Json::Null)).map(|(k, _)| k.as_str()).collect(),
|
|
25
|
+
_ => Vec::new(),
|
|
26
|
+
};
|
|
27
|
+
let extra: Vec<&str> = keys.into_iter().filter(|k| !READ_MODEL_BARE_KEYS.contains(k)).collect();
|
|
28
|
+
if !extra.is_empty() {
|
|
29
|
+
return Some(read_model_options_skip_reason(&extra));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if read_model.get("group_by").map(Json::each).unwrap_or(&[]).iter().any(|_| true) {
|
|
33
|
+
return group_by_skip_reason(read_model, aggregates_by_name, unsupported_names);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let heads = read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]);
|
|
37
|
+
let reference_target = read_model.get("reference_target").map(Json::to_s).unwrap_or_default();
|
|
38
|
+
let root = heads.iter().find(|h| h.get("aggregate").map(Json::to_s).unwrap_or_default() == reference_target);
|
|
39
|
+
// Rootless (no `reference_to`) is generated — `read_models.rb`'s own
|
|
40
|
+
// comment on this check.
|
|
41
|
+
if root.is_none() && !reference_target.is_empty() {
|
|
42
|
+
return Some(skip(
|
|
43
|
+
"missing_root_head",
|
|
44
|
+
format!("declares reference_to {reference_target}, but includes no matching aggregate head — nothing for this generator's own root fetch to key off"),
|
|
45
|
+
));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let root_name = root.and_then(|h| h.get("aggregate")).map(Json::to_s).unwrap_or_default();
|
|
49
|
+
if !aggregates_by_name.contains_key(&root_name) && nested_entity_names(aggregates_by_name).contains(&root_name) {
|
|
50
|
+
return Some(entity_head_skip_reason(&root_name, "root", "fetch by id"));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for head in heads {
|
|
54
|
+
if let Some(reason) = read_model_head_skip_reason(head, aggregates_by_name, unsupported_names) {
|
|
55
|
+
return Some(reason);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if let Some(reason) = read_model_options_content_skip_reason(read_model, aggregates_by_name) {
|
|
60
|
+
return Some(reason);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
aggregation_skip_reason(read_model, aggregates_by_name)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn read_model_options_skip_reason(extra: &[&str]) -> SkipReason {
|
|
67
|
+
let mut sorted: Vec<&str> = extra.to_vec();
|
|
68
|
+
sorted.sort();
|
|
69
|
+
skip(sorted[0], format!(
|
|
70
|
+
"declares {} — out of scope for this generator: cursor/consistency/inspection are real capabilities Ports::Query::InMemory/Ports::Query::Ordering/TenantScope implement that this generator does not port (this file's own header has the full argument, the same boundary queries.rb already draws for a declared AGGREGATE query); freshness/use_index are never disqualifying on their own — neither is read by the in-memory interpreter path this kernel matches",
|
|
71
|
+
sorted.join(", ")
|
|
72
|
+
))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Every many-side head this read model declares.
|
|
76
|
+
fn read_model_many_heads(read_model: &Json) -> Vec<&Json> {
|
|
77
|
+
read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]).iter().filter(|h| h.get("many").map(Json::as_bool).unwrap_or(false)).collect()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Whether one declared option (its own `:target`, or `None` when untargeted) applies to
|
|
81
|
+
/// `head`. `target` names the included aggregate by TYPE (ADR 0055), not by its `as:` alias;
|
|
82
|
+
/// `None` only ever means "the sole many-side head" — Ruby's own `seal_query_options` refuses
|
|
83
|
+
/// an untargeted option whenever a read model declares more than one, so an untargeted option
|
|
84
|
+
/// reaching codegen at all is proof there's exactly one to mean.
|
|
85
|
+
fn option_target_matches_head(target: Option<&str>, head: &Json, many_heads: &[&Json]) -> bool {
|
|
86
|
+
let head_as = head.get("as").map(Json::to_s).unwrap_or_default();
|
|
87
|
+
match target {
|
|
88
|
+
None => many_heads.len() == 1 && many_heads[0].get("as").map(Json::to_s).unwrap_or_default() == head_as,
|
|
89
|
+
Some(target) => target == head.get("aggregate").map(Json::to_s).unwrap_or_default(),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fn json_target(option: &Json) -> Option<String> {
|
|
94
|
+
option.get("target").map(Json::to_s)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Whether `head` has at least one declared where/order_by/offset/limit (or, for the sole
|
|
98
|
+
/// many-side head only, a declared `authorize ..., tenant:` — `on:` doesn't extend to
|
|
99
|
+
/// `tenant:`) targeting it.
|
|
100
|
+
fn read_model_option_targets_head(read_model: &Json, head: &Json, many_heads: &[&Json]) -> bool {
|
|
101
|
+
let wheres = read_model.get("wheres").map(Json::each).unwrap_or(&[]);
|
|
102
|
+
if wheres.iter().any(|w| option_target_matches_head(json_target(w).as_deref(), head, many_heads)) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if let Some(order_by) = read_model.get("order_by") {
|
|
106
|
+
if option_target_matches_head(json_target(order_by).as_deref(), head, many_heads) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if let Some(limit) = read_model.get("limit") {
|
|
111
|
+
if option_target_matches_head(json_target(limit).as_deref(), head, many_heads) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if let Some(offset) = read_model.get("offset") {
|
|
116
|
+
if option_target_matches_head(json_target(offset).as_deref(), head, many_heads) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if read_model.get("authorization").and_then(|a| a.get("tenant")).is_some() && many_heads.len() == 1 {
|
|
121
|
+
return many_heads[0].get("as").map(Json::to_s).unwrap_or_default() == head.get("as").map(Json::to_s).unwrap_or_default();
|
|
122
|
+
}
|
|
123
|
+
false
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// Every many-side head this read model declares options for, in `aggregate_heads`' own
|
|
127
|
+
/// declared order — one entry when there's a single many-side head (the pre-ADR-0055 shape),
|
|
128
|
+
/// more than one only when `on:` targets several.
|
|
129
|
+
fn read_model_filtered_heads<'a>(read_model: &Json, many_heads: &[&'a Json]) -> Vec<&'a Json> {
|
|
130
|
+
many_heads.iter().copied().filter(|head| read_model_option_targets_head(read_model, head, many_heads)).collect()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// One targeted head's own where/order_by/offset/limit, pulled out of the read model's full
|
|
134
|
+
/// declared set. `authorization` carries `read_model`'s own declared authorization only for the
|
|
135
|
+
/// sole many-side head (`on:` doesn't extend to `tenant:`), `None` for every other targeted head.
|
|
136
|
+
struct HeadQuery<'a> {
|
|
137
|
+
wheres: Vec<&'a Json>,
|
|
138
|
+
order_by: Option<&'a Json>,
|
|
139
|
+
limit: Option<&'a Json>,
|
|
140
|
+
offset: Option<&'a Json>,
|
|
141
|
+
authorization: Option<&'a Json>,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fn read_model_head_query<'a>(read_model: &'a Json, head: &Json, many_heads: &[&Json]) -> HeadQuery<'a> {
|
|
145
|
+
let wheres = read_model.get("wheres").map(Json::each).unwrap_or(&[]).iter().filter(|w| option_target_matches_head(json_target(w).as_deref(), head, many_heads)).collect();
|
|
146
|
+
let order_by = read_model.get("order_by").filter(|ob| option_target_matches_head(json_target(ob).as_deref(), head, many_heads));
|
|
147
|
+
let limit = read_model.get("limit").filter(|l| option_target_matches_head(json_target(l).as_deref(), head, many_heads));
|
|
148
|
+
let offset = read_model.get("offset").filter(|o| option_target_matches_head(json_target(o).as_deref(), head, many_heads));
|
|
149
|
+
let authorization = if many_heads.len() == 1 && many_heads[0].get("as").map(Json::to_s).unwrap_or_default() == head.get("as").map(Json::to_s).unwrap_or_default() {
|
|
150
|
+
read_model.get("authorization")
|
|
151
|
+
} else {
|
|
152
|
+
None
|
|
153
|
+
};
|
|
154
|
+
HeadQuery { wheres, order_by, limit, offset, authorization }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Checks every targeted head's own where/order_by/limit for generability against ITS
|
|
158
|
+
/// aggregate (not the read model's root); `None` means clean.
|
|
159
|
+
fn read_model_options_content_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
|
|
160
|
+
let many_heads = read_model_many_heads(read_model);
|
|
161
|
+
|
|
162
|
+
for head in read_model_filtered_heads(read_model, &many_heads) {
|
|
163
|
+
let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
164
|
+
let Some(aggregate) = aggregates_by_name.get(&aggregate_name) else {
|
|
165
|
+
return Some(entity_head_skip_reason(&aggregate_name, "filtered head", "filter, order or authorize"));
|
|
166
|
+
};
|
|
167
|
+
let vos = aggregate.get("value_objects").map(Json::each).unwrap_or(&[]);
|
|
168
|
+
let value_objects_by_name: HashMap<String, &Json> = vos.iter().map(|vo| (vo.get("name").and_then(Json::as_str).unwrap_or("").to_string(), vo)).collect();
|
|
169
|
+
let as_name = head.get("as").map(Json::to_s).unwrap_or_default();
|
|
170
|
+
let head_label = format!("head {aggregate_name} (as {as_name})");
|
|
171
|
+
let head_query = read_model_head_query(read_model, head, &many_heads);
|
|
172
|
+
|
|
173
|
+
// A hop through a reference is generated when `query_hop_plan` resolves
|
|
174
|
+
// it — `read_models.rb`'s own loop, check for check.
|
|
175
|
+
for where_clause in head_query.wheres.iter().copied() {
|
|
176
|
+
let field = where_clause.get("field").map(Json::to_s).unwrap_or_default();
|
|
177
|
+
let hop = queries::query_hop_plan(aggregate, &field, aggregates_by_name);
|
|
178
|
+
if hop.is_none() && field.contains('/') {
|
|
179
|
+
return Some(skip(
|
|
180
|
+
"reference_hop_where",
|
|
181
|
+
format!(
|
|
182
|
+
"{head_label}'s own where clause on {} hops through a reference this generator can't resolve yet (more than one hop, the head isn't a real reference attribute, or the target aggregate isn't declared in this domain) — not generated yet",
|
|
183
|
+
crate::naming::ruby_inspect_string(&field)
|
|
184
|
+
),
|
|
185
|
+
));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if let Some(plan) = hop {
|
|
189
|
+
let target_value_objects_by_name = queries::value_objects_of(plan.target);
|
|
190
|
+
if let Some(reason) = queries::query_where_skip_reason(&queries::with_field(where_clause, &plan.inner_field), plan.target, &target_value_objects_by_name) {
|
|
191
|
+
return Some(reskip(&reason, format!("{head_label}'s own hop through {} to {}'s own {reason}", plan.via_field, plan.target_aggregate)));
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if let Some(reason) = queries::query_where_skip_reason(where_clause, aggregate, &value_objects_by_name) {
|
|
197
|
+
return Some(reskip(&reason, format!("{head_label}'s own {reason}")));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if let Some(reason) = queries::declared_authorization_skip_reason(head_query.authorization, aggregate, &value_objects_by_name) {
|
|
202
|
+
return Some(reason);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if let Some(reason) = queries::declared_order_by_skip_reason(head_query.order_by, aggregate, &value_objects_by_name) {
|
|
206
|
+
return Some(reason);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if let Some(reason) = queries::declared_offset_skip_reason(head_query.offset) {
|
|
210
|
+
return Some(reason);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if let Some(reason) = queries::declared_limit_skip_reason(head_query.limit) {
|
|
214
|
+
return Some(reason);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
None
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// `seal_aggregation` (Ruby, build time) already guarantees exactly one
|
|
222
|
+
// many-side head and mutual exclusion with `group_by` — only the one declared
|
|
223
|
+
// reduction's own field needs checking here (ADR 0078).
|
|
224
|
+
fn aggregation_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
|
|
225
|
+
let field_key = AGGREGATION_FIELD_KEYS.iter().find(|key| read_model.get(**key).is_some())?;
|
|
226
|
+
let field = Json::to_s(read_model.get(*field_key).expect("checked by find above"));
|
|
227
|
+
let word = field_key.strip_suffix("_field").unwrap_or(field_key);
|
|
228
|
+
|
|
229
|
+
let heads = read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]);
|
|
230
|
+
let target = heads.iter().find(|h| h.get("many").map(Json::as_bool).unwrap_or(false))?;
|
|
231
|
+
let aggregate_name = target.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
232
|
+
let Some(aggregate) = aggregates_by_name.get(&aggregate_name) else {
|
|
233
|
+
return Some(entity_head_skip_reason(&aggregate_name, &format!("{word} target"), "reduce"));
|
|
234
|
+
};
|
|
235
|
+
let vos = aggregate.get("value_objects").map(Json::each).unwrap_or(&[]);
|
|
236
|
+
let value_objects_by_name: HashMap<String, &Json> = vos.iter().map(|vo| (vo.get("name").and_then(Json::as_str).unwrap_or("").to_string(), vo)).collect();
|
|
237
|
+
|
|
238
|
+
let kind = queries::query_field_kind(aggregate, &field, &value_objects_by_name);
|
|
239
|
+
if kind == queries::FieldKind::Unknown {
|
|
240
|
+
return Some(skip(*field_key, format!("{word} names {field:?}, but {aggregate_name} declares no such attribute — not generated yet")));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if BOOLEAN_AGGREGATION_FIELDS.contains(field_key) {
|
|
244
|
+
return if queries::query_field_boolean(aggregate, &field, &value_objects_by_name) {
|
|
245
|
+
None
|
|
246
|
+
} else {
|
|
247
|
+
Some(skip(*field_key, format!("{word} names {field:?} on {aggregate_name}, which is not boolean — {word} needs a true/false field — not generated yet")))
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if kind != queries::FieldKind::Number {
|
|
252
|
+
return Some(skip(
|
|
253
|
+
*field_key,
|
|
254
|
+
format!("{word} names {field:?} on {aggregate_name}, which is not numeric — {word} needs a numeric field (a bare number, or a value object carrying one) — not generated yet"),
|
|
255
|
+
));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if INTEGER_ONLY_AGGREGATION_FIELDS.contains(field_key) {
|
|
259
|
+
if queries::query_field_numeric_type(aggregate, &field, &value_objects_by_name).as_deref() != Some("Integer") {
|
|
260
|
+
return Some(skip(
|
|
261
|
+
*field_key,
|
|
262
|
+
format!("{word} names {field:?} on {aggregate_name}, which is a Float field — {word} admits only an Integer field (summing/averaging Floats cannot be made to agree, byte for byte, between Ruby and Rust) — not generated yet"),
|
|
263
|
+
));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
None
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Mirrors `rust/project/read_models.rb`'s `group_by_skip_reason`: the one
|
|
271
|
+
// shape the corpus declares — a single rootless head, group_by alone.
|
|
272
|
+
fn group_by_skip_reason(read_model: &Json, aggregates_by_name: &HashMap<String, &Json>, unsupported_names: &[String]) -> Option<SkipReason> {
|
|
273
|
+
let heads = read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]);
|
|
274
|
+
if heads.len() != 1 {
|
|
275
|
+
return Some(skip("group_by", format!("declares group_by across {} aggregate heads — not generated yet (only a single, rootless head is)", heads.len())));
|
|
276
|
+
}
|
|
277
|
+
let reference_target = read_model.get("reference_target");
|
|
278
|
+
if reference_target.is_some() && !matches!(reference_target, Some(Json::Null)) {
|
|
279
|
+
return Some(skip("group_by", format!("declares group_by on a NON-rootless read model (reference_to {}) — not generated yet", reference_target.map(Json::to_s).unwrap_or_default())));
|
|
280
|
+
}
|
|
281
|
+
if read_model.get("count").is_some() || AGGREGATION_FIELD_KEYS.iter().any(|key| read_model.get(*key).is_some()) {
|
|
282
|
+
return Some(skip("group_by", "declares group_by alongside a reduction — not generated yet"));
|
|
283
|
+
}
|
|
284
|
+
let has_wheres = read_model.get("wheres").map(Json::each).unwrap_or(&[]).iter().any(|_| true);
|
|
285
|
+
if has_wheres || read_model.get("order_by").is_some() || read_model.get("limit").is_some() || read_model.get("offset").is_some() {
|
|
286
|
+
return Some(skip("group_by", "declares group_by alongside where/order_by/limit/offset — not generated yet"));
|
|
287
|
+
}
|
|
288
|
+
if read_model.get("authorization").is_some() {
|
|
289
|
+
return Some(skip("group_by", "declares group_by with an authorize policy — not generated yet"));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let head = &heads[0];
|
|
293
|
+
if let Some(reason) = read_model_head_skip_reason(head, aggregates_by_name, unsupported_names) {
|
|
294
|
+
return Some(reason);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
298
|
+
let Some(aggregate) = aggregates_by_name.get(&aggregate_name) else {
|
|
299
|
+
return Some(entity_head_skip_reason(&aggregate_name, "group_by head", "group"));
|
|
300
|
+
};
|
|
301
|
+
let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
302
|
+
let lifecycle_field = aggregate.get("lifecycle").and_then(|l| l.get("field")).map(Json::to_s);
|
|
303
|
+
for row in read_model.get("group_by").map(Json::each).unwrap_or(&[]) {
|
|
304
|
+
let field = row.get("field").map(Json::to_s).unwrap_or_default();
|
|
305
|
+
let is_attr = attrs.iter().any(|a| crate::attr::name(a) == field);
|
|
306
|
+
let is_lifecycle = lifecycle_field.as_deref() == Some(field.as_str());
|
|
307
|
+
if !is_attr && !is_lifecycle {
|
|
308
|
+
return Some(skip("group_by", format!("group_by names {field:?}, but {aggregate_name} declares no such attribute — not generated yet")));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
None
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Every entity name nested at any depth under a declared aggregate.
|
|
315
|
+
fn nested_entity_names(aggregates_by_name: &HashMap<String, &Json>) -> Vec<String> {
|
|
316
|
+
fn collect(owner: &Json, out: &mut Vec<String>) {
|
|
317
|
+
for entity in owner.get("entities").map(Json::each).unwrap_or(&[]) {
|
|
318
|
+
out.push(entity.get("name").map(Json::to_s).unwrap_or_default());
|
|
319
|
+
collect(entity, out);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
let mut out = Vec::new();
|
|
323
|
+
for aggregate in aggregates_by_name.values() {
|
|
324
|
+
collect(aggregate, &mut out);
|
|
325
|
+
}
|
|
326
|
+
out
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/// Port of `read_models.rb#entity_head_skip_reason`.
|
|
330
|
+
fn entity_head_skip_reason(aggregate_name: &str, role: &str, purpose: &str) -> SkipReason {
|
|
331
|
+
skip(
|
|
332
|
+
"include_entity_head",
|
|
333
|
+
format!("includes {aggregate_name}, a nested entity, as the {role} — an entity has no rows of its own (ReadModelInterpreter#records reads it as empty), so there is nothing to {purpose} — not generated yet"),
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
fn read_model_head_skip_reason(head: &Json, aggregates_by_name: &HashMap<String, &Json>, unsupported_names: &[String]) -> Option<SkipReason> {
|
|
338
|
+
let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
339
|
+
if !aggregates_by_name.contains_key(&aggregate_name) {
|
|
340
|
+
if nested_entity_names(aggregates_by_name).contains(&aggregate_name) {
|
|
341
|
+
return None;
|
|
342
|
+
}
|
|
343
|
+
return Some(skip("include_undeclared_aggregate", format!("includes {aggregate_name}, which this domain never declares")));
|
|
344
|
+
}
|
|
345
|
+
if unsupported_names.contains(&aggregate_name) {
|
|
346
|
+
return Some(skip(
|
|
347
|
+
"include_unsupported_aggregate",
|
|
348
|
+
format!("includes {aggregate_name}, which this generator couldn't itself generate (unsupported attribute type — see this domain's own aggregate-level manifest entry)"),
|
|
349
|
+
));
|
|
350
|
+
}
|
|
351
|
+
None
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
pub struct ReferenceField {
|
|
355
|
+
pub target: String,
|
|
356
|
+
pub field: String,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
fn read_model_reference_fields(head_aggregate: &Json) -> Vec<ReferenceField> {
|
|
360
|
+
let attrs = head_aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
361
|
+
attrs
|
|
362
|
+
.iter()
|
|
363
|
+
.filter_map(|attr| {
|
|
364
|
+
let target = crate::naming::reference_target(crate::attr::type_name(attr))?;
|
|
365
|
+
Some(ReferenceField { target: target.to_string(), field: crate::attr::name(attr).to_string() })
|
|
366
|
+
})
|
|
367
|
+
.collect()
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
fn emit_reference_field(domain_name: &str, rf: &ReferenceField) -> String {
|
|
371
|
+
let qualified_target = format!("{domain_name}::{}", rf.target);
|
|
372
|
+
format!("crate::kernel::read_model::ReferenceField {{ target_aggregate: {}, field: {} }}", crate::naming::ruby_inspect_string(&qualified_target), crate::naming::ruby_inspect_string(&rf.field))
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
fn emit_read_model_head(domain_name: &str, head: &Json, is_root: bool, aggregates_by_name: &HashMap<String, &Json>) -> String {
|
|
376
|
+
let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
377
|
+
let reference_fields: Vec<ReferenceField> = if is_root { Vec::new() } else { aggregates_by_name.get(&aggregate_name).map(|a| read_model_reference_fields(a)).unwrap_or_default() };
|
|
378
|
+
let reference_fields_expr = reference_fields.iter().map(|rf| emit_reference_field(domain_name, rf)).collect::<Vec<_>>().join(", ");
|
|
379
|
+
let qualified_aggregate = format!("{domain_name}::{aggregate_name}");
|
|
380
|
+
let as_name = head.get("as").map(Json::to_s).unwrap_or_default();
|
|
381
|
+
let many = head.get("many").map(Json::as_bool).unwrap_or(false);
|
|
382
|
+
|
|
383
|
+
format!(
|
|
384
|
+
"crate::kernel::read_model::ReadModelHead {{ aggregate: {}, as_name: {}, many: {many}, is_root: {is_root}, reference_fields: &[{reference_fields_expr}] }}",
|
|
385
|
+
crate::naming::ruby_inspect_string(&qualified_aggregate),
|
|
386
|
+
crate::naming::ruby_inspect_string(&as_name)
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
fn emit_read_model_order_by(order_by: &Json, null_semantics: Option<&Json>) -> String {
|
|
391
|
+
let descending = order_by.get("direction").map(Json::to_s).unwrap_or_default() == "desc";
|
|
392
|
+
format!(
|
|
393
|
+
"crate::kernel::read_model::ReadModelOrderBy {{ field: {}, descending: {descending}, nulls: {} }}",
|
|
394
|
+
crate::naming::ruby_inspect_string(&order_by.get("field").map(Json::to_s).unwrap_or_default()),
|
|
395
|
+
queries::null_semantics_variant(null_semantics)
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
fn emit_read_model_limit(limit: &Json) -> String {
|
|
400
|
+
let raw = limit.get("value").map(Json::to_s).unwrap_or_default();
|
|
401
|
+
if let Some(arg) = raw.strip_prefix(':') {
|
|
402
|
+
return format!("crate::kernel::read_model::ReadModelLimit::Arg({})", crate::naming::ruby_inspect_string(arg));
|
|
403
|
+
}
|
|
404
|
+
format!("crate::kernel::read_model::ReadModelLimit::Literal({})", ruby_to_i(&raw))
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// `ReadModelOffset` is a type alias for `ReadModelLimit`'s own type, so this
|
|
408
|
+
// reuses `emit_read_model_limit`'s computation and swaps the type name.
|
|
409
|
+
fn emit_read_model_offset(offset: &Json) -> String {
|
|
410
|
+
emit_read_model_limit(offset).replace("read_model::ReadModelLimit::", "read_model::ReadModelOffset::")
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
fn ruby_to_i(s: &str) -> i64 {
|
|
414
|
+
let trimmed = s.trim_start();
|
|
415
|
+
let bytes = trimmed.as_bytes();
|
|
416
|
+
let mut end = 0;
|
|
417
|
+
if end < bytes.len() && (bytes[end] == b'-' || bytes[end] == b'+') {
|
|
418
|
+
end += 1;
|
|
419
|
+
}
|
|
420
|
+
let digits_start = end;
|
|
421
|
+
while end < bytes.len() && bytes[end].is_ascii_digit() {
|
|
422
|
+
end += 1;
|
|
423
|
+
}
|
|
424
|
+
if end == digits_start {
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
trimmed[..end].parse().unwrap_or(0)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/// One targeted many-side head's own where/order_by/offset/limit — the per-head analogue of
|
|
431
|
+
/// `ReadModelDef` itself. At most one when there's a single many-side head (the pre-ADR-0055
|
|
432
|
+
/// shape); more than one only when `on:` targets several.
|
|
433
|
+
pub struct FilteredHeadDef {
|
|
434
|
+
pub as_name: String,
|
|
435
|
+
pub conditions: Vec<queries::Condition>,
|
|
436
|
+
pub reference_hop_conditions: Vec<queries::HopCondition>,
|
|
437
|
+
pub order_by: Option<String>,
|
|
438
|
+
pub offset: Option<String>,
|
|
439
|
+
pub limit: Option<String>,
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
pub struct ReadModelDef {
|
|
443
|
+
pub verb: String,
|
|
444
|
+
pub reference_name: Option<String>,
|
|
445
|
+
pub heads: Vec<String>,
|
|
446
|
+
pub filtered_heads: Vec<FilteredHeadDef>,
|
|
447
|
+
pub authorization: Option<String>,
|
|
448
|
+
pub group_by_fn: Option<String>,
|
|
449
|
+
pub group_by_fn_body: Option<String>,
|
|
450
|
+
pub count: bool,
|
|
451
|
+
pub median_field: Option<String>,
|
|
452
|
+
pub sum_field: Option<String>,
|
|
453
|
+
pub avg_field: Option<String>,
|
|
454
|
+
pub min_field: Option<String>,
|
|
455
|
+
pub max_field: Option<String>,
|
|
456
|
+
pub percentile_field: Option<String>,
|
|
457
|
+
pub percentile_at: Option<String>,
|
|
458
|
+
pub any_field: Option<String>,
|
|
459
|
+
pub all_field: Option<String>,
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/// `local_wheres, hop_wheres` compiled for one targeted head, mirroring
|
|
463
|
+
/// `read_models.rb#filtered_head_def`.
|
|
464
|
+
fn filtered_head_def(domain_name: &str, read_model: &Json, head: &Json, many_heads: &[&Json], aggregates_by_name: &HashMap<String, &Json>) -> FilteredHeadDef {
|
|
465
|
+
let aggregate_name = head.get("aggregate").map(Json::to_s).unwrap_or_default();
|
|
466
|
+
let aggregate = aggregates_by_name[&aggregate_name];
|
|
467
|
+
let head_query = read_model_head_query(read_model, head, many_heads);
|
|
468
|
+
let (local_wheres, hop_wheres): (Vec<&Json>, Vec<&Json>) =
|
|
469
|
+
head_query.wheres.iter().copied().partition(|w| queries::query_hop_plan(aggregate, &w.get("field").map(Json::to_s).unwrap_or_default(), aggregates_by_name).is_none());
|
|
470
|
+
let synthetic = with_wheres_and_authorization(&local_wheres, head_query.authorization);
|
|
471
|
+
let conditions = if head_query.authorization.is_some() { queries::query_conditions_with_authorization(&synthetic) } else { queries::query_conditions(&synthetic) };
|
|
472
|
+
|
|
473
|
+
FilteredHeadDef {
|
|
474
|
+
as_name: head.get("as").map(Json::to_s).unwrap_or_default(),
|
|
475
|
+
conditions,
|
|
476
|
+
reference_hop_conditions: queries::read_model_hop_conditions(domain_name, &hop_wheres, aggregate, aggregates_by_name),
|
|
477
|
+
order_by: head_query.order_by.map(|ob| emit_read_model_order_by(ob, read_model.get("null_semantics"))),
|
|
478
|
+
offset: head_query.offset.map(emit_read_model_offset),
|
|
479
|
+
limit: head_query.limit.map(emit_read_model_limit),
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
pub fn read_model_def(domain_name: &str, read_model: &Json, aggregates_by_name: &HashMap<String, &Json>) -> ReadModelDef {
|
|
484
|
+
let reference_target = read_model.get("reference_target").map(Json::to_s).unwrap_or_default();
|
|
485
|
+
let heads_json = read_model.get("aggregate_heads").map(Json::each).unwrap_or(&[]);
|
|
486
|
+
let heads: Vec<String> = heads_json
|
|
487
|
+
.iter()
|
|
488
|
+
.map(|head| {
|
|
489
|
+
let is_root = head.get("aggregate").map(Json::to_s).unwrap_or_default() == reference_target;
|
|
490
|
+
emit_read_model_head(domain_name, head, is_root, aggregates_by_name)
|
|
491
|
+
})
|
|
492
|
+
.collect();
|
|
493
|
+
|
|
494
|
+
let many_heads = read_model_many_heads(read_model);
|
|
495
|
+
let filtered_heads: Vec<FilteredHeadDef> =
|
|
496
|
+
read_model_filtered_heads(read_model, &many_heads).into_iter().map(|head| filtered_head_def(domain_name, read_model, head, &many_heads, aggregates_by_name)).collect();
|
|
497
|
+
let sole_head_as: bool = many_heads.len() == 1;
|
|
498
|
+
|
|
499
|
+
let read_model_name = read_model.get("name").map(Json::to_s).unwrap_or_default();
|
|
500
|
+
let group_by_fields: Vec<String> = read_model.get("group_by").map(Json::each).unwrap_or(&[]).iter().map(|row| row.get("field").map(Json::to_s).unwrap_or_default()).collect();
|
|
501
|
+
let (group_by_fn, group_by_fn_body) = if !group_by_fields.is_empty() {
|
|
502
|
+
let fn_name = format!("group_by_{}", read_model_name.to_lowercase());
|
|
503
|
+
let aggregate_name = heads_json.first().and_then(|h| h.get("aggregate")).map(Json::to_s).unwrap_or_default();
|
|
504
|
+
let aggregate = aggregates_by_name[&aggregate_name];
|
|
505
|
+
(Some(fn_name.clone()), Some(emit_group_by_transform(&fn_name, &read_model_name, aggregate, &group_by_fields)))
|
|
506
|
+
} else {
|
|
507
|
+
(None, None)
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
ReadModelDef {
|
|
511
|
+
verb: format!("{domain_name}.{read_model_name}"),
|
|
512
|
+
reference_name: read_model.get("reference_name").map(Json::to_s),
|
|
513
|
+
heads,
|
|
514
|
+
filtered_heads,
|
|
515
|
+
authorization: if sole_head_as { queries::emit_query_authorization(&read_model_name, read_model.get("authorization")) } else { None },
|
|
516
|
+
group_by_fn,
|
|
517
|
+
group_by_fn_body,
|
|
518
|
+
count: read_model.get("count").is_some(),
|
|
519
|
+
median_field: read_model.get("median_field").map(Json::to_s),
|
|
520
|
+
sum_field: read_model.get("sum_field").map(Json::to_s),
|
|
521
|
+
avg_field: read_model.get("avg_field").map(Json::to_s),
|
|
522
|
+
min_field: read_model.get("min_field").map(Json::to_s),
|
|
523
|
+
max_field: read_model.get("max_field").map(Json::to_s),
|
|
524
|
+
percentile_field: read_model.get("percentile_field").map(Json::to_s),
|
|
525
|
+
percentile_at: if read_model.get("percentile_field").is_some() { read_model.get("percentile_at").map(Json::to_s) } else { None },
|
|
526
|
+
any_field: read_model.get("any_field").map(Json::to_s),
|
|
527
|
+
all_field: read_model.get("all_field").map(Json::to_s),
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/// A minimal `{wheres:, authorization:}` object — everything `query_conditions`/
|
|
532
|
+
/// `query_conditions_with_authorization` actually read — for one targeted head's own local
|
|
533
|
+
/// (non-hop) where clauses.
|
|
534
|
+
fn with_wheres_and_authorization(wheres: &[&Json], authorization: Option<&Json>) -> Json {
|
|
535
|
+
let wheres_json = Json::Array(wheres.iter().map(|w| (*w).clone()).collect());
|
|
536
|
+
let mut pairs = vec![("wheres".to_string(), wheres_json)];
|
|
537
|
+
if let Some(auth) = authorization {
|
|
538
|
+
pairs.push(("authorization".to_string(), auth.clone()));
|
|
539
|
+
}
|
|
540
|
+
Json::Object(pairs)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
pub fn emit_read_model_def(rmd: &ReadModelDef) -> String {
|
|
544
|
+
let heads = rmd.heads.iter().map(|h| format!(" {h},")).collect::<Vec<_>>().join("\n");
|
|
545
|
+
let filtered_heads = rmd.filtered_heads.iter().map(|fh| format!(" {}", emit_filtered_head(fh))).collect::<Vec<_>>().join("\n");
|
|
546
|
+
let authorization = match &rmd.authorization {
|
|
547
|
+
Some(a) => format!("Some({a})"),
|
|
548
|
+
None => "None".to_string(),
|
|
549
|
+
};
|
|
550
|
+
let reference_name = match &rmd.reference_name {
|
|
551
|
+
Some(r) => format!("Some({})", crate::naming::ruby_inspect_string(r)),
|
|
552
|
+
None => "None".to_string(),
|
|
553
|
+
};
|
|
554
|
+
let group_by = match &rmd.group_by_fn {
|
|
555
|
+
Some(f) => format!("Some({f})"),
|
|
556
|
+
None => "None".to_string(),
|
|
557
|
+
};
|
|
558
|
+
let count = if rmd.count { "true" } else { "false" };
|
|
559
|
+
let field_some = |f: &Option<String>| match f {
|
|
560
|
+
Some(v) => format!("Some({})", crate::naming::ruby_inspect_string(v)),
|
|
561
|
+
None => "None".to_string(),
|
|
562
|
+
};
|
|
563
|
+
let median_field = field_some(&rmd.median_field);
|
|
564
|
+
let sum_field = field_some(&rmd.sum_field);
|
|
565
|
+
let avg_field = field_some(&rmd.avg_field);
|
|
566
|
+
let min_field = field_some(&rmd.min_field);
|
|
567
|
+
let max_field = field_some(&rmd.max_field);
|
|
568
|
+
let percentile_field = field_some(&rmd.percentile_field);
|
|
569
|
+
let percentile_at = match &rmd.percentile_at {
|
|
570
|
+
Some(at) => format!("Some({at}_f64)"),
|
|
571
|
+
None => "None".to_string(),
|
|
572
|
+
};
|
|
573
|
+
let any_field = field_some(&rmd.any_field);
|
|
574
|
+
let all_field = field_some(&rmd.all_field);
|
|
575
|
+
|
|
576
|
+
format!(
|
|
577
|
+
"crate::kernel::read_model::ReadModelDef {{\n verb: {},\n reference_name: {reference_name},\n heads: &[\n{heads}\n ],\n filtered_heads: &[\n{filtered_heads}\n ],\n authorization: {authorization},\n group_by: {group_by},\n count: {count},\n median_field: {median_field},\n sum_field: {sum_field},\n avg_field: {avg_field},\n min_field: {min_field},\n max_field: {max_field},\n percentile_field: {percentile_field},\n percentile_at: {percentile_at},\n any_field: {any_field},\n all_field: {all_field},\n}},",
|
|
578
|
+
crate::naming::ruby_inspect_string(&rmd.verb)
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/// One `FilteredHead` entry, always emitted as a single line — matching this generator's own
|
|
583
|
+
/// established idiom for every OTHER leaf struct (`ReadModelHead`, `ReferenceField`,
|
|
584
|
+
/// `ReferenceHopCondition`, `QueryCondition` are all single-line too; only the top-level
|
|
585
|
+
/// `ReadModelDef`/`QueryDef` get pretty multi-line formatting with their arrays broken apart).
|
|
586
|
+
fn emit_filtered_head(fh: &FilteredHeadDef) -> String {
|
|
587
|
+
let conditions = fh.conditions.iter().map(queries::emit_query_condition).collect::<Vec<_>>().join(" ");
|
|
588
|
+
let reference_hop_conditions = fh.reference_hop_conditions.iter().map(queries::emit_reference_hop_condition).collect::<Vec<_>>().join(" ");
|
|
589
|
+
let order_by = match &fh.order_by {
|
|
590
|
+
Some(o) => format!("Some({o})"),
|
|
591
|
+
None => "None".to_string(),
|
|
592
|
+
};
|
|
593
|
+
let offset = match &fh.offset {
|
|
594
|
+
Some(o) => format!("Some({o})"),
|
|
595
|
+
None => "None".to_string(),
|
|
596
|
+
};
|
|
597
|
+
let limit = match &fh.limit {
|
|
598
|
+
Some(l) => format!("Some({l})"),
|
|
599
|
+
None => "None".to_string(),
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
format!(
|
|
603
|
+
"crate::kernel::read_model::FilteredHead {{ as_name: {}, conditions: &[{conditions}], reference_hop_conditions: &[{reference_hop_conditions}], order_by: {order_by}, offset: {offset}, limit: {limit} }},",
|
|
604
|
+
crate::naming::ruby_inspect_string(&fh.as_name)
|
|
605
|
+
)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Keeps this aggregate's declared attributes plus any `projects` field, id
|
|
609
|
+
// and lifecycle field, excluding other capabilities' synthetic fields (like
|
|
610
|
+
// `corrects`'s `emitted_*` flags), and unwraps value objects recursively.
|
|
611
|
+
fn emit_group_by_transform(fn_name: &str, read_model_name: &str, aggregate: &Json, group_by_fields: &[String]) -> String {
|
|
612
|
+
let value_objects: Vec<&Json> = aggregate.get("value_objects").map(Json::each).unwrap_or(&[]).iter().collect();
|
|
613
|
+
let value_objects_by_name: HashMap<String, &Json> = value_objects.iter().map(|vo| (vo.get("name").map(Json::to_s).unwrap_or_default(), *vo)).collect();
|
|
614
|
+
let lifecycle_field = aggregate.get("lifecycle").and_then(|l| l.get("field")).map(Json::to_s);
|
|
615
|
+
let mut fields: Vec<Json> = aggregate.get("attributes").map(Json::each).unwrap_or(&[]).to_vec();
|
|
616
|
+
fields.extend(crate::types::projected_field_pseudo_attributes(aggregate));
|
|
617
|
+
|
|
618
|
+
let mut kept_keys: Vec<String> = fields.iter().map(|a| crate::attr::name(a).to_string()).collect();
|
|
619
|
+
kept_keys.push("id".to_string());
|
|
620
|
+
if let Some(lf) = &lifecycle_field {
|
|
621
|
+
kept_keys.push(lf.clone());
|
|
622
|
+
}
|
|
623
|
+
let keep_cond = kept_keys.iter().map(|k| format!("k == {k:?}")).collect::<Vec<_>>().join(" || ");
|
|
624
|
+
|
|
625
|
+
let arms: Vec<String> = fields
|
|
626
|
+
.iter()
|
|
627
|
+
.map(|a| {
|
|
628
|
+
let unwrapped = unwrap_json_expr("v", crate::attr::type_name(a), crate::attr::list(a), aggregate, &value_objects_by_name);
|
|
629
|
+
format!("{:?} => {unwrapped},", crate::attr::name(a))
|
|
630
|
+
})
|
|
631
|
+
.collect();
|
|
632
|
+
let arms = arms.join("\n ");
|
|
633
|
+
|
|
634
|
+
let fields_literal = format!("&[{}]", group_by_fields.iter().map(|f| format!("{f:?}")).collect::<Vec<_>>().join(", "));
|
|
635
|
+
let leaf_check = group_by_leaf_check(read_model_name, aggregate, group_by_fields);
|
|
636
|
+
|
|
637
|
+
format!(
|
|
638
|
+
"pub fn {fn_name}(rows: Vec<(String, crate::kernel::Json)>) -> Result<crate::kernel::Json, crate::kernel::Refusal> {{\n let unwrapped: Vec<crate::kernel::Json> = rows\n .into_iter()\n .map(|(id, record)| {{\n let wrapped = crate::kernel::repository::row_json(id, record);\n match wrapped {{\n crate::kernel::Json::Object(fields) => crate::kernel::Json::Object(\n fields\n .into_iter()\n .filter(|(k, _)| {keep_cond})\n .map(|(k, v)| {{\n let new_v = match k.as_str() {{\n {arms}\n _ => v,\n }};\n (k, new_v)\n }})\n .collect(),\n ),\n other => other,\n }}\n }})\n .collect();\n crate::kernel::read_model::nest(unwrapped, {fields_literal}, {leaf_check})\n}}"
|
|
639
|
+
)
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// A key path naming every identity head cannot collide, so its leaves go
|
|
643
|
+
// unchecked; any other refuses a second row (ADR 0061).
|
|
644
|
+
fn group_by_leaf_check(read_model_name: &str, aggregate: &Json, group_by_fields: &[String]) -> String {
|
|
645
|
+
let mut identity: Vec<String> = Vec::new();
|
|
646
|
+
for path in aggregate.get("identified_by").map(Json::each).unwrap_or(&[]) {
|
|
647
|
+
let head = path.to_s().split('.').next().unwrap_or_default().to_string();
|
|
648
|
+
if !identity.contains(&head) {
|
|
649
|
+
identity.push(head);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if !identity.is_empty() && identity.iter().all(|head| group_by_fields.contains(head)) {
|
|
653
|
+
return "crate::kernel::read_model::LeafCheck::IdentityCovered".to_string();
|
|
654
|
+
}
|
|
655
|
+
format!("crate::kernel::read_model::LeafCheck::RefuseCollision({})", crate::naming::ruby_inspect_string(read_model_name))
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Port of `Value.materialize_unwrapped` (Ruby).
|
|
659
|
+
fn unwrap_json_expr(expr: &str, type_name: &str, list: bool, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> String {
|
|
660
|
+
if list {
|
|
661
|
+
let inner = unwrap_json_expr("item", type_name, false, aggregate, value_objects_by_name);
|
|
662
|
+
return format!("match {expr} {{ crate::kernel::Json::Array(items) => crate::kernel::Json::Array(items.into_iter().map(|item| {inner}).collect()), other => other }}");
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
let vo = value_objects_by_name.get(type_name).copied();
|
|
666
|
+
let entity = aggregate.get("entities").map(Json::each).unwrap_or(&[]).iter().find(|e| e.get("name").map(Json::to_s).unwrap_or_default() == type_name);
|
|
667
|
+
let fields_meta: Option<&[Json]> = if let Some(vo) = vo {
|
|
668
|
+
vo.get("attributes").map(Json::each)
|
|
669
|
+
} else {
|
|
670
|
+
entity.and_then(|e| e.get("attributes").map(Json::each))
|
|
671
|
+
};
|
|
672
|
+
let Some(fields_meta) = fields_meta else { return expr.to_string() };
|
|
673
|
+
|
|
674
|
+
if vo.is_some() && fields_meta.len() == 1 {
|
|
675
|
+
let field = &fields_meta[0];
|
|
676
|
+
let unwrapped = unwrap_json_expr("field_value", crate::attr::type_name(field), crate::attr::list(field), aggregate, value_objects_by_name);
|
|
677
|
+
let field_name = crate::attr::name(field);
|
|
678
|
+
return format!("match {expr} {{ crate::kernel::Json::Object(fields) => fields.into_iter().find(|(k, _)| k == {field_name:?}).map(|(_, field_value)| {unwrapped}).unwrap_or(crate::kernel::Json::Null), other => other }}");
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
let inner_arms: Vec<String> = fields_meta
|
|
682
|
+
.iter()
|
|
683
|
+
.map(|f| {
|
|
684
|
+
let unwrapped = unwrap_json_expr("v", crate::attr::type_name(f), crate::attr::list(f), aggregate, value_objects_by_name);
|
|
685
|
+
format!("{:?} => {unwrapped},", crate::attr::name(f))
|
|
686
|
+
})
|
|
687
|
+
.collect();
|
|
688
|
+
let inner_arms = inner_arms.join(" ");
|
|
689
|
+
format!("match {expr} {{ crate::kernel::Json::Object(fields) => crate::kernel::Json::Object(fields.into_iter().map(|(k, v)| {{ let new_v = match k.as_str() {{ {inner_arms} _ => v }}; (k, new_v) }}).collect()), other => other }}")
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const READ_MODEL_TABLE_ROW_PLACEHOLDER: &str = "crate::kernel::read_model::ReadModelDef {\n verb: \"tmpl_verb\",\n reference_name: Some(\"tmpl_reference_name\"),\n heads: &[\n crate::kernel::read_model::ReadModelHead {\n aggregate: \"tmpl_aggregate\",\n as_name: \"tmpl_as_name\",\n many: true,\n is_root: false,\n reference_fields: &[\n crate::kernel::read_model::ReferenceField { target_aggregate: \"tmpl_target_aggregate\", field: \"tmpl_field\" },\n ],\n },\n ],\n filtered_heads: &[\n crate::kernel::read_model::FilteredHead { as_name: \"tmpl_as_name\", conditions: &[crate::kernel::QueryCondition { field: \"tmpl_field\", comparator: crate::kernel::query_comparators::QueryComparator::Eq, value: crate::kernel::QueryConditionValue::Literal(\"tmpl_literal\") },], reference_hop_conditions: &[crate::kernel::read_model::ReferenceHopCondition { via_field: \"tmpl_via_field\", target_aggregate: \"tmpl_target_aggregate\", through: &[crate::kernel::read_model::HopStep { via_field: \"tmpl_via_field\", target_aggregate: \"tmpl_target_aggregate\" }], inner_field: \"tmpl_inner_field\", inner_comparator: crate::kernel::query_comparators::QueryComparator::Eq, inner_value: crate::kernel::QueryConditionValue::Literal(\"tmpl_literal\") },], order_by: Some(crate::kernel::read_model::ReadModelOrderBy { field: \"tmpl_order_field\", descending: true, nulls: crate::kernel::query_ordering::NullsMode::Last }), offset: Some(crate::kernel::read_model::ReadModelOffset::Literal(1)), limit: Some(crate::kernel::read_model::ReadModelLimit::Literal(5)) },\n ],\n authorization: Some(crate::kernel::named_query::TenantAuth { query_name: \"tmpl_query_name\", tenant_field: \"tmpl_tenant_field\", policy: \"tmpl_policy\" }),\n group_by: None,\n count: false,\n median_field: None,\n sum_field: None,\n avg_field: None,\n min_field: None,\n max_field: None,\n percentile_field: None,\n percentile_at: None,\n any_field: None,\n all_field: None,\n},";
|
|
693
|
+
|
|
694
|
+
pub fn emit_read_model_table(exemplar: &Exemplar, read_model_defs: &[ReadModelDef]) -> String {
|
|
695
|
+
let rows: Vec<String> = read_model_defs.iter().map(emit_read_model_def).collect();
|
|
696
|
+
exemplar.render("read_model_table", &[(READ_MODEL_TABLE_ROW_PLACEHOLDER, rows.join("\n"))])
|
|
697
|
+
}
|