hecks 2.9.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +13 -4
- 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 +15 -8
- 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 +15 -5
- 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,786 @@
|
|
|
1
|
+
//! Port of `rust/project/queries.rb`, mirrored function for function.
|
|
2
|
+
|
|
3
|
+
use crate::exemplar::Exemplar;
|
|
4
|
+
use crate::json::Json;
|
|
5
|
+
use crate::literal::{self, Literal};
|
|
6
|
+
use crate::naming;
|
|
7
|
+
use crate::skip_reason::{reskip, skip, SkipReason};
|
|
8
|
+
use std::collections::HashMap;
|
|
9
|
+
|
|
10
|
+
const COMPARATORS_NEEDING_NUMERIC_FIDELITY: &[&str] = &["gt", "gte", "lt", "lte"];
|
|
11
|
+
const COMPARATORS_EXEMPT_FROM_LITERAL_TYPING: &[&str] = &["in", "contains"];
|
|
12
|
+
|
|
13
|
+
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
|
14
|
+
pub enum FieldKind {
|
|
15
|
+
String,
|
|
16
|
+
Number,
|
|
17
|
+
Other,
|
|
18
|
+
Unknown,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Classifies `field`: its head names an attribute or the lifecycle field, and any
|
|
22
|
+
/// later segments walk nested value objects.
|
|
23
|
+
pub fn query_field_kind(aggregate: &Json, field: &str, value_objects_by_name: &HashMap<String, &Json>) -> FieldKind {
|
|
24
|
+
let mut segments = field.split('.');
|
|
25
|
+
let head = segments.next().unwrap_or("");
|
|
26
|
+
let rest: Vec<&str> = segments.collect();
|
|
27
|
+
|
|
28
|
+
let lifecycle_field = aggregate.get("lifecycle").and_then(|l| l.get("field")).map(Json::to_s);
|
|
29
|
+
if let Some(lf) = &lifecycle_field {
|
|
30
|
+
if lf == head {
|
|
31
|
+
return if rest.is_empty() { FieldKind::String } else { FieldKind::Unknown };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
36
|
+
let Some(attr) = attrs.iter().find(|a| crate::attr::name(a) == head) else { return FieldKind::Unknown };
|
|
37
|
+
if crate::attr::list(attr) {
|
|
38
|
+
return FieldKind::Other;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
query_type_kind(crate::attr::type_name(attr), &rest, value_objects_by_name)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn query_type_kind(type_name: &str, segments: &[&str], value_objects_by_name: &HashMap<String, &Json>) -> FieldKind {
|
|
45
|
+
if naming::reference_type(type_name) {
|
|
46
|
+
return if segments.is_empty() { FieldKind::String } else { FieldKind::Unknown };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if segments.is_empty() {
|
|
50
|
+
return query_scalar_or_vo_kind(type_name, value_objects_by_name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let Some(vo) = value_objects_by_name.get(type_name) else { return FieldKind::Unknown };
|
|
54
|
+
let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
55
|
+
let Some(member) = attrs.iter().find(|a| crate::attr::name(a) == segments[0]) else { return FieldKind::Unknown };
|
|
56
|
+
if crate::attr::list(member) {
|
|
57
|
+
return FieldKind::Unknown;
|
|
58
|
+
}
|
|
59
|
+
query_type_kind(crate::attr::type_name(member), &segments[1..], value_objects_by_name)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn query_scalar_or_vo_kind(type_name: &str, value_objects_by_name: &HashMap<String, &Json>) -> FieldKind {
|
|
63
|
+
match type_name {
|
|
64
|
+
"String" => FieldKind::String,
|
|
65
|
+
"Integer" | "Float" => FieldKind::Number,
|
|
66
|
+
"TrueClass" | "FalseClass" => FieldKind::Other,
|
|
67
|
+
_ => match value_objects_by_name.get(type_name) {
|
|
68
|
+
Some(vo) => query_vo_collapse_kind(vo, value_objects_by_name),
|
|
69
|
+
None => FieldKind::Unknown,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn query_vo_collapse_kind(vo: &Json, value_objects_by_name: &HashMap<String, &Json>) -> FieldKind {
|
|
75
|
+
let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
76
|
+
if attrs.iter().any(|a| matches!(crate::attr::type_name(a), "Integer" | "Float")) {
|
|
77
|
+
return FieldKind::Number;
|
|
78
|
+
}
|
|
79
|
+
if attrs.len() == 1 {
|
|
80
|
+
return query_type_kind(crate::attr::type_name(&attrs[0]), &[], value_objects_by_name);
|
|
81
|
+
}
|
|
82
|
+
FieldKind::Other
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Whether a field (or dotted path) reduces specifically to a `TrueClass`/`FalseClass` —
|
|
86
|
+
/// `any`/`all` need this, distinct from `query_field_kind`'s coarser `Other` bucket (ADR 0078).
|
|
87
|
+
pub fn query_field_boolean(aggregate: &Json, field: &str, value_objects_by_name: &HashMap<String, &Json>) -> bool {
|
|
88
|
+
let mut segments = field.split('.');
|
|
89
|
+
let head = segments.next().unwrap_or("");
|
|
90
|
+
let rest: Vec<&str> = segments.collect();
|
|
91
|
+
|
|
92
|
+
let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
93
|
+
let Some(attr) = attrs.iter().find(|a| crate::attr::name(a) == head) else { return false };
|
|
94
|
+
if crate::attr::list(attr) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
query_type_boolean(crate::attr::type_name(attr), &rest, value_objects_by_name)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn query_type_boolean(type_name: &str, segments: &[&str], value_objects_by_name: &HashMap<String, &Json>) -> bool {
|
|
101
|
+
if segments.is_empty() {
|
|
102
|
+
if matches!(type_name, "TrueClass" | "FalseClass") {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if naming::reference_type(type_name) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
let Some(vo) = value_objects_by_name.get(type_name) else { return false };
|
|
109
|
+
let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
110
|
+
if attrs.len() == 1 {
|
|
111
|
+
return query_type_boolean(crate::attr::type_name(&attrs[0]), &[], value_objects_by_name);
|
|
112
|
+
}
|
|
113
|
+
false
|
|
114
|
+
} else {
|
|
115
|
+
let Some(vo) = value_objects_by_name.get(type_name) else { return false };
|
|
116
|
+
let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
117
|
+
let Some(member) = attrs.iter().find(|a| crate::attr::name(a) == segments[0]) else { return false };
|
|
118
|
+
if crate::attr::list(member) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
query_type_boolean(crate::attr::type_name(member), &segments[1..], value_objects_by_name)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// The winning member's own type name for a numeric field ("the sole numeric member wins",
|
|
126
|
+
/// mirroring `query_vo_collapse_kind`) — "Integer" or "Float" distinctly, where
|
|
127
|
+
/// `query_field_kind`'s own `Number` collapses both. `sum`/`avg` need the distinction
|
|
128
|
+
/// (ADR 0078).
|
|
129
|
+
pub fn query_field_numeric_type(aggregate: &Json, field: &str, value_objects_by_name: &HashMap<String, &Json>) -> Option<String> {
|
|
130
|
+
let mut segments = field.split('.');
|
|
131
|
+
let head = segments.next().unwrap_or("");
|
|
132
|
+
let rest: Vec<&str> = segments.collect();
|
|
133
|
+
|
|
134
|
+
let attrs = aggregate.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
135
|
+
let attr = attrs.iter().find(|a| crate::attr::name(a) == head)?;
|
|
136
|
+
if crate::attr::list(attr) {
|
|
137
|
+
return None;
|
|
138
|
+
}
|
|
139
|
+
query_type_numeric_type(crate::attr::type_name(attr), &rest, value_objects_by_name)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn query_type_numeric_type(type_name: &str, segments: &[&str], value_objects_by_name: &HashMap<String, &Json>) -> Option<String> {
|
|
143
|
+
if segments.is_empty() {
|
|
144
|
+
if matches!(type_name, "Integer" | "Float") {
|
|
145
|
+
return Some(type_name.to_string());
|
|
146
|
+
}
|
|
147
|
+
if naming::reference_type(type_name) {
|
|
148
|
+
return None;
|
|
149
|
+
}
|
|
150
|
+
let vo = value_objects_by_name.get(type_name)?;
|
|
151
|
+
let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
152
|
+
if let Some(numeric_member) = attrs.iter().find(|a| matches!(crate::attr::type_name(a), "Integer" | "Float")) {
|
|
153
|
+
return Some(crate::attr::type_name(numeric_member).to_string());
|
|
154
|
+
}
|
|
155
|
+
if attrs.len() == 1 {
|
|
156
|
+
return query_type_numeric_type(crate::attr::type_name(&attrs[0]), &[], value_objects_by_name);
|
|
157
|
+
}
|
|
158
|
+
None
|
|
159
|
+
} else {
|
|
160
|
+
let vo = value_objects_by_name.get(type_name)?;
|
|
161
|
+
let attrs = vo.get("attributes").map(Json::each).unwrap_or(&[]);
|
|
162
|
+
let member = attrs.iter().find(|a| crate::attr::name(a) == segments[0])?;
|
|
163
|
+
if crate::attr::list(member) {
|
|
164
|
+
return None;
|
|
165
|
+
}
|
|
166
|
+
query_type_numeric_type(crate::attr::type_name(member), &segments[1..], value_objects_by_name)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
pub fn query_where_skip_reason(where_clause: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
|
|
171
|
+
let field = where_clause.get("field").map(Json::to_s).unwrap_or_default();
|
|
172
|
+
let kind = query_field_kind(aggregate, &field, value_objects_by_name);
|
|
173
|
+
if kind == FieldKind::Unknown {
|
|
174
|
+
let construct = if field.contains('/') { "reference_hop_where" } else { "where_unrecognized_field" };
|
|
175
|
+
return Some(skip(
|
|
176
|
+
construct,
|
|
177
|
+
format!(
|
|
178
|
+
"where clause on {} isn't a recognized attribute of this aggregate — a hop through a reference, an entity-scoped field, or simply undeclared here; cross-aggregate joins are read_model territory, not this generator's job",
|
|
179
|
+
naming::ruby_inspect_string(&field)
|
|
180
|
+
),
|
|
181
|
+
));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let raw_value = where_clause.get("value").map(Json::to_s).unwrap_or_default();
|
|
185
|
+
if raw_value.starts_with(':') {
|
|
186
|
+
return None;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let op = where_clause.get("op").map(Json::to_s).unwrap_or_default();
|
|
190
|
+
if !known_query_comparator(&op) {
|
|
191
|
+
return Some(skip(
|
|
192
|
+
"where_none_in_state",
|
|
193
|
+
format!(
|
|
194
|
+
"where clause on {} uses op {} — Vocabulary::QueryComparator admits it, and rust/src/kernel/query_comparators.rs's own QueryComparator::NoneInState variant now exists and is proven correct (item #9, whole-project table-unification survey) — but no generated domain has any way to hand it a cross-domain search list at the call site (named_query::run's own thin `run_cross_domain([])` wrapper is what every generated QUERIES table actually calls), so generating this condition today would silently answer every row 'true' rather than a real anti-join — deliberately left ungenerated until a real cross-domain-search call site exists, the same honest-refusal-over-silently-wrong choice this generator makes everywhere else",
|
|
195
|
+
naming::ruby_inspect_string(&field),
|
|
196
|
+
naming::ruby_inspect_string(&op)
|
|
197
|
+
),
|
|
198
|
+
));
|
|
199
|
+
}
|
|
200
|
+
if COMPARATORS_NEEDING_NUMERIC_FIDELITY.contains(&op.as_str()) {
|
|
201
|
+
if kind == FieldKind::Number {
|
|
202
|
+
return None;
|
|
203
|
+
}
|
|
204
|
+
return Some(skip(
|
|
205
|
+
"where_literal",
|
|
206
|
+
format!(
|
|
207
|
+
"where clause on {} uses op {} against a LITERAL value whose target field doesn't reduce to a plain JSON number (kind: {}) — gt/gte/lt/lte only mean anything against a number (query_comparators.rs's own `ordered?` gate)",
|
|
208
|
+
naming::ruby_inspect_string(&field),
|
|
209
|
+
naming::ruby_inspect_string(&op),
|
|
210
|
+
kind_name(kind)
|
|
211
|
+
),
|
|
212
|
+
));
|
|
213
|
+
}
|
|
214
|
+
if COMPARATORS_EXEMPT_FROM_LITERAL_TYPING.contains(&op.as_str()) {
|
|
215
|
+
return None;
|
|
216
|
+
}
|
|
217
|
+
if kind == FieldKind::String {
|
|
218
|
+
return None;
|
|
219
|
+
}
|
|
220
|
+
if kind == FieldKind::Number {
|
|
221
|
+
return None;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
Some(skip(
|
|
225
|
+
"where_literal",
|
|
226
|
+
format!(
|
|
227
|
+
"where clause on {} uses op {} against a LITERAL value whose target field doesn't reduce to a plain JSON string (kind: {}) — its true wire type can't be recovered from the exported IR",
|
|
228
|
+
naming::ruby_inspect_string(&field),
|
|
229
|
+
naming::ruby_inspect_string(&op),
|
|
230
|
+
kind_name(kind)
|
|
231
|
+
),
|
|
232
|
+
))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fn kind_name(kind: FieldKind) -> &'static str {
|
|
236
|
+
match kind {
|
|
237
|
+
FieldKind::String => "string",
|
|
238
|
+
FieldKind::Number => "number",
|
|
239
|
+
FieldKind::Other => "other",
|
|
240
|
+
FieldKind::Unknown => "unknown",
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// A `/` hop chain through Reference-typed attributes to aggregates this domain declares.
|
|
245
|
+
pub struct HopPlan<'a> {
|
|
246
|
+
pub via_field: String,
|
|
247
|
+
pub target_aggregate: String,
|
|
248
|
+
/// Further `(via_field, bare target aggregate)` steps of a chain.
|
|
249
|
+
pub through: Vec<(String, String)>,
|
|
250
|
+
pub target: &'a Json,
|
|
251
|
+
pub inner_field: String,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Matches `HopPath::MAX_HOPS`.
|
|
255
|
+
const HOP_CHAIN_LIMIT: usize = 8;
|
|
256
|
+
|
|
257
|
+
pub fn query_hop_plan<'a>(aggregate: &'a Json, field: &str, aggregates_by_name: &HashMap<String, &'a Json>) -> Option<HopPlan<'a>> {
|
|
258
|
+
let segments: Vec<&str> = field.split('/').collect();
|
|
259
|
+
if segments.len() < 2 || segments.len() - 1 > HOP_CHAIN_LIMIT {
|
|
260
|
+
return None;
|
|
261
|
+
}
|
|
262
|
+
let mut steps: Vec<(String, String)> = Vec::new();
|
|
263
|
+
let mut current: &'a Json = aggregate;
|
|
264
|
+
for segment in &segments[..segments.len() - 1] {
|
|
265
|
+
let via = current.get("attributes").map(Json::each).unwrap_or(&[]).iter().find(|a| crate::attr::name(a) == *segment)?;
|
|
266
|
+
let type_name = crate::attr::type_name(via);
|
|
267
|
+
if !naming::reference_type(type_name) {
|
|
268
|
+
return None;
|
|
269
|
+
}
|
|
270
|
+
let target_name = naming::reference_target(type_name)?;
|
|
271
|
+
let target = *aggregates_by_name.get(target_name)?;
|
|
272
|
+
steps.push((segment.to_string(), target_name.to_string()));
|
|
273
|
+
current = target;
|
|
274
|
+
}
|
|
275
|
+
let (via_field, target_aggregate) = steps.remove(0);
|
|
276
|
+
Some(HopPlan { via_field, target_aggregate, through: steps, target: current, inner_field: segments[segments.len() - 1].to_string() })
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
pub fn with_field(where_clause: &Json, field: &str) -> Json {
|
|
280
|
+
match where_clause {
|
|
281
|
+
Json::Object(pairs) => Json::Object(
|
|
282
|
+
pairs
|
|
283
|
+
.iter()
|
|
284
|
+
.map(|(key, value)| if key == "field" { (key.clone(), Json::String(field.to_string())) } else { (key.clone(), value.clone()) })
|
|
285
|
+
.collect(),
|
|
286
|
+
),
|
|
287
|
+
other => other.clone(),
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
pub fn value_objects_of(aggregate: &Json) -> HashMap<String, &Json> {
|
|
292
|
+
aggregate
|
|
293
|
+
.get("value_objects")
|
|
294
|
+
.map(Json::each)
|
|
295
|
+
.unwrap_or(&[])
|
|
296
|
+
.iter()
|
|
297
|
+
.map(|vo| (vo.get("name").and_then(Json::as_str).unwrap_or("").to_string(), vo))
|
|
298
|
+
.collect()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
pub fn query_skip_reason(query: &Json, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>, aggregates_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
|
|
302
|
+
let extra_keys = ["cursor", "consistency", "freshness", "inspection"];
|
|
303
|
+
let extras: Vec<&str> = extra_keys.iter().filter(|k| query.get(k).is_some()).copied().collect();
|
|
304
|
+
if !extras.is_empty() {
|
|
305
|
+
return Some(skip(extras[0], format!("declares {} — out of scope for this generator (rust/project/queries.rb's own header has the full argument)", extras.join(", "))));
|
|
306
|
+
}
|
|
307
|
+
if query.get("index_hints").map(Json::each).unwrap_or(&[]).iter().any(|_| true) {
|
|
308
|
+
return Some(skip("index_hints", "declares use_index, out of scope for the same reason the extras above are"));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// A declared tenant synthesizes its own where clause (`query_conditions_with_authorization`),
|
|
312
|
+
// so a query with no ordinary wheres but a tenant gate is still generable. The tenant
|
|
313
|
+
// field itself is validated afterward by `declared_authorization_skip_reason`.
|
|
314
|
+
let declared_tenant = query.get("authorization").and_then(|a| a.get("tenant")).is_some();
|
|
315
|
+
let wheres = query.get("wheres").map(Json::each).unwrap_or(&[]);
|
|
316
|
+
if wheres.is_empty() && !declared_tenant {
|
|
317
|
+
return Some(skip("no_wheres", "declares no where clauses at all — nothing for filter_entries to bake in"));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
for where_clause in wheres {
|
|
321
|
+
let field = where_clause.get("field").map(Json::to_s).unwrap_or_default();
|
|
322
|
+
if let Some(plan) = query_hop_plan(aggregate, &field, aggregates_by_name) {
|
|
323
|
+
let target_value_objects_by_name = value_objects_of(plan.target);
|
|
324
|
+
if let Some(reason) = query_where_skip_reason(&with_field(where_clause, &plan.inner_field), plan.target, &target_value_objects_by_name) {
|
|
325
|
+
return Some(reskip(&reason, format!("hop through {} to {}'s own {reason}", plan.via_field, plan.target_aggregate)));
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if let Some(reason) = query_where_skip_reason(where_clause, aggregate, value_objects_by_name) {
|
|
330
|
+
return Some(reason);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if let Some(reason) = declared_authorization_skip_reason(query.get("authorization"), aggregate, value_objects_by_name) {
|
|
335
|
+
return Some(reason);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if let Some(reason) = declared_order_by_skip_reason(query.get("order_by"), aggregate, value_objects_by_name) {
|
|
339
|
+
return Some(reason);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if let Some(reason) = declared_offset_skip_reason(query.get("offset")) {
|
|
343
|
+
return Some(reason);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
declared_limit_skip_reason(query.get("limit"))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
pub fn declared_order_by_skip_reason(order_by: Option<&Json>, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
|
|
350
|
+
let order_by = order_by?;
|
|
351
|
+
let field = order_by.get("field").map(Json::to_s).unwrap_or_default();
|
|
352
|
+
let kind = query_field_kind(aggregate, &field, value_objects_by_name);
|
|
353
|
+
if matches!(kind, FieldKind::String | FieldKind::Number) {
|
|
354
|
+
return None;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
Some(skip(
|
|
358
|
+
"order_by",
|
|
359
|
+
format!(
|
|
360
|
+
"declares order_by on {} — this generator can only sort a field that reduces to a plain JSON string or number (kind: {}); a hop through a reference, an entity-scoped field, a list_of field, or a multi-member non-numeric value object can't be compared generically",
|
|
361
|
+
naming::ruby_inspect_string(&field),
|
|
362
|
+
kind_name(kind)
|
|
363
|
+
),
|
|
364
|
+
))
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
pub fn declared_limit_skip_reason(limit: Option<&Json>) -> Option<SkipReason> {
|
|
368
|
+
let limit = limit?;
|
|
369
|
+
let raw = limit.get("value").map(Json::to_s).unwrap_or_default();
|
|
370
|
+
if raw.starts_with(':') || is_plain_integer(&raw) {
|
|
371
|
+
return None;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
Some(skip("limit", format!("declares limit {} — not a literal integer or a caller-bound Symbol arg, so this generator can't compile a real limit count from it", naming::ruby_inspect_string(&raw))))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Same check as `declared_limit_skip_reason`, kept separate so the reason names "offset".
|
|
378
|
+
pub fn declared_offset_skip_reason(offset: Option<&Json>) -> Option<SkipReason> {
|
|
379
|
+
let offset = offset?;
|
|
380
|
+
let raw = offset.get("value").map(Json::to_s).unwrap_or_default();
|
|
381
|
+
if raw.starts_with(':') || is_plain_integer(&raw) {
|
|
382
|
+
return None;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
Some(skip("offset", format!("declares offset {} — not a literal integer or a caller-bound Symbol arg, so this generator can't compile a real offset count from it", naming::ruby_inspect_string(&raw))))
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
fn is_plain_integer(raw: &str) -> bool {
|
|
389
|
+
let s = raw.strip_prefix('-').unwrap_or(raw);
|
|
390
|
+
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// A `nil` tenant is a no-op, never disqualifying. A real tenant is checked by running a
|
|
394
|
+
// synthetic arg-bound where through `query_where_skip_reason`.
|
|
395
|
+
pub fn declared_authorization_skip_reason(authorization: Option<&Json>, aggregate: &Json, value_objects_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
|
|
396
|
+
let tenant = authorization.and_then(|a| a.get("tenant")).map(Json::to_s)?;
|
|
397
|
+
let synthetic_where = Json::Object(vec![
|
|
398
|
+
("field".to_string(), Json::String(tenant.clone())),
|
|
399
|
+
("op".to_string(), Json::String("eq".to_string())),
|
|
400
|
+
("value".to_string(), Json::String(format!(":{tenant}"))),
|
|
401
|
+
]);
|
|
402
|
+
query_where_skip_reason(&synthetic_where, aggregate, value_objects_by_name).map(|reason| skip("authorization", reason.text))
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
pub fn emit_query_order_by(order_by: &Json, null_semantics: Option<&Json>) -> String {
|
|
406
|
+
let descending = order_by.get("direction").map(Json::to_s).unwrap_or_default() == "desc";
|
|
407
|
+
format!(
|
|
408
|
+
"crate::kernel::query_ordering::OrderBy {{ field: {}, descending: {descending}, nulls: {} }}",
|
|
409
|
+
naming::ruby_inspect_string(&order_by.get("field").map(Json::to_s).unwrap_or_default()),
|
|
410
|
+
null_semantics_variant(null_semantics)
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// `nulls(mode)` is unvalidated, so an unrecognized mode falls back to `Native`
|
|
415
|
+
// like `NullPolicy.order`'s `else` arm.
|
|
416
|
+
pub fn null_semantics_variant(null_semantics: Option<&Json>) -> &'static str {
|
|
417
|
+
let mode = null_semantics.and_then(|ns| ns.get("mode")).map(Json::to_s).unwrap_or_default();
|
|
418
|
+
match mode.as_str() {
|
|
419
|
+
"first" => "crate::kernel::query_ordering::NullsMode::First",
|
|
420
|
+
"last" => "crate::kernel::query_ordering::NullsMode::Last",
|
|
421
|
+
_ => "crate::kernel::query_ordering::NullsMode::Native",
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
pub fn emit_query_limit(limit: &Json) -> String {
|
|
426
|
+
let raw = limit.get("value").map(Json::to_s).unwrap_or_default();
|
|
427
|
+
if let Some(arg) = raw.strip_prefix(':') {
|
|
428
|
+
return format!("crate::kernel::query_ordering::Limit::Arg({})", naming::ruby_inspect_string(arg));
|
|
429
|
+
}
|
|
430
|
+
format!("crate::kernel::query_ordering::Limit::Literal({})", ruby_to_i(&raw))
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// `Offset` aliases `Limit`; only the spelled type name is swapped so a generated
|
|
434
|
+
// `offset:` field does not read as a `Limit`.
|
|
435
|
+
pub fn emit_query_offset(offset: &Json) -> String {
|
|
436
|
+
emit_query_limit(offset).replace("query_ordering::Limit::", "query_ordering::Offset::")
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
fn ruby_to_i(s: &str) -> i64 {
|
|
440
|
+
let trimmed = s.trim_start();
|
|
441
|
+
let bytes = trimmed.as_bytes();
|
|
442
|
+
let mut end = 0;
|
|
443
|
+
if end < bytes.len() && (bytes[end] == b'-' || bytes[end] == b'+') {
|
|
444
|
+
end += 1;
|
|
445
|
+
}
|
|
446
|
+
let digits_start = end;
|
|
447
|
+
while end < bytes.len() && bytes[end].is_ascii_digit() {
|
|
448
|
+
end += 1;
|
|
449
|
+
}
|
|
450
|
+
if end == digits_start {
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
trimmed[..end].parse().unwrap_or(0)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
pub struct Condition {
|
|
457
|
+
pub field: String,
|
|
458
|
+
pub op: String,
|
|
459
|
+
pub arg: Option<String>,
|
|
460
|
+
pub literal: Option<Literal>,
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Assumes `query_skip_reason` already accepted the query.
|
|
464
|
+
pub fn query_conditions(query: &Json) -> Vec<Condition> {
|
|
465
|
+
let wheres = query.get("wheres").map(Json::each).unwrap_or(&[]);
|
|
466
|
+
wheres.iter().map(condition_for).collect()
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
fn condition_for(w: &Json) -> Condition {
|
|
470
|
+
let raw_value = w.get("value").map(Json::to_s).unwrap_or_default();
|
|
471
|
+
let symbol = raw_value.starts_with(':');
|
|
472
|
+
Condition {
|
|
473
|
+
field: w.get("field").map(Json::to_s).unwrap_or_default(),
|
|
474
|
+
op: w.get("op").map(Json::to_s).unwrap_or_default(),
|
|
475
|
+
arg: if symbol { Some(raw_value.trim_start_matches(':').to_string()) } else { None },
|
|
476
|
+
literal: if symbol { None } else { Some(literal::read(&raw_value)) },
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/// One hop where clause, compiled; `condition.field` is the hop's inner field.
|
|
481
|
+
pub struct HopCondition {
|
|
482
|
+
pub via_field: String,
|
|
483
|
+
pub target_aggregate: String,
|
|
484
|
+
/// Further `(via_field, qualified target aggregate)` steps.
|
|
485
|
+
pub through: Vec<(String, String)>,
|
|
486
|
+
pub condition: Condition,
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
pub fn query_conditions_and_hops(domain_name: &str, query: &Json, aggregate: &Json, aggregates_by_name: &HashMap<String, &Json>) -> (Vec<Condition>, Vec<HopCondition>) {
|
|
490
|
+
let mut local = Vec::new();
|
|
491
|
+
let mut hops = Vec::new();
|
|
492
|
+
for w in query.get("wheres").map(Json::each).unwrap_or(&[]) {
|
|
493
|
+
let field = w.get("field").map(Json::to_s).unwrap_or_default();
|
|
494
|
+
match query_hop_plan(aggregate, &field, aggregates_by_name) {
|
|
495
|
+
Some(plan) => hops.push(hop_condition(domain_name, plan, w)),
|
|
496
|
+
None => local.push(condition_for(w)),
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if let Some(tenant) = query.get("authorization").and_then(|a| a.get("tenant")).map(Json::to_s) {
|
|
500
|
+
local.push(Condition { field: tenant.clone(), op: "eq".to_string(), arg: Some(tenant), literal: None });
|
|
501
|
+
}
|
|
502
|
+
(local, hops)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Assumes the skip check already confirmed every `hop_wheres` entry resolves.
|
|
506
|
+
pub fn read_model_hop_conditions(domain_name: &str, hop_wheres: &[&Json], aggregate: &Json, aggregates_by_name: &HashMap<String, &Json>) -> Vec<HopCondition> {
|
|
507
|
+
hop_wheres
|
|
508
|
+
.iter()
|
|
509
|
+
.filter_map(|w| {
|
|
510
|
+
let plan = query_hop_plan(aggregate, &w.get("field").map(Json::to_s).unwrap_or_default(), aggregates_by_name)?;
|
|
511
|
+
Some(hop_condition(domain_name, plan, w))
|
|
512
|
+
})
|
|
513
|
+
.collect()
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
fn hop_condition(domain_name: &str, plan: HopPlan<'_>, w: &Json) -> HopCondition {
|
|
517
|
+
let mut condition = condition_for(w);
|
|
518
|
+
condition.field = plan.inner_field;
|
|
519
|
+
let through = plan.through.into_iter().map(|(via, target)| (via, format!("{domain_name}::{target}"))).collect();
|
|
520
|
+
HopCondition { via_field: plan.via_field, target_aggregate: format!("{domain_name}::{}", plan.target_aggregate), through, condition }
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
pub fn emit_reference_hop_condition(hop: &HopCondition) -> String {
|
|
524
|
+
let through = hop
|
|
525
|
+
.through
|
|
526
|
+
.iter()
|
|
527
|
+
.map(|(via, target)| format!("crate::kernel::read_model::HopStep {{ via_field: {}, target_aggregate: {} }}", naming::ruby_inspect_string(via), naming::ruby_inspect_string(target)))
|
|
528
|
+
.collect::<Vec<_>>()
|
|
529
|
+
.join(", ");
|
|
530
|
+
format!(
|
|
531
|
+
"crate::kernel::read_model::ReferenceHopCondition {{ via_field: {}, target_aggregate: {}, through: &[{through}], inner_field: {}, inner_comparator: crate::kernel::query_comparators::QueryComparator::{}, inner_value: {} }},",
|
|
532
|
+
naming::ruby_inspect_string(&hop.via_field),
|
|
533
|
+
naming::ruby_inspect_string(&hop.target_aggregate),
|
|
534
|
+
naming::ruby_inspect_string(&hop.condition.field),
|
|
535
|
+
query_comparator_variant(&hop.condition.op),
|
|
536
|
+
emit_query_condition_value(&hop.condition)
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Adds `Runtime::TenantScope.apply`'s synthetic tenant clause at codegen time.
|
|
541
|
+
pub fn query_conditions_with_authorization(query: &Json) -> Vec<Condition> {
|
|
542
|
+
let mut conditions = query_conditions(query);
|
|
543
|
+
if let Some(tenant) = query.get("authorization").and_then(|a| a.get("tenant")).map(Json::to_s) {
|
|
544
|
+
conditions.push(Condition { field: tenant.clone(), op: "eq".to_string(), arg: Some(tenant), literal: None });
|
|
545
|
+
}
|
|
546
|
+
conditions
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// `None` unless a tenant is declared. `policy` is carried on the wire but not enforced,
|
|
550
|
+
// matching Ruby's `TenantScope.apply`; an absent policy becomes `""`.
|
|
551
|
+
pub fn emit_query_authorization(query_name: &str, authorization: Option<&Json>) -> Option<String> {
|
|
552
|
+
let tenant = authorization.and_then(|a| a.get("tenant")).map(Json::to_s)?;
|
|
553
|
+
let policy = authorization.and_then(|a| a.get("policy")).map(Json::to_s).unwrap_or_default();
|
|
554
|
+
Some(format!(
|
|
555
|
+
"crate::kernel::named_query::TenantAuth {{ query_name: {}, tenant_field: {}, policy: {} }}",
|
|
556
|
+
naming::ruby_inspect_string(query_name),
|
|
557
|
+
naming::ruby_inspect_string(&tenant),
|
|
558
|
+
naming::ruby_inspect_string(&policy)
|
|
559
|
+
))
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// `none_in_state` is left out on purpose: no generated call site can supply its
|
|
563
|
+
// cross-domain search list, so it would answer every row `true`. `query_where_skip_reason`
|
|
564
|
+
// checks this first, making the `panic!` in `query_comparator_variant` a backstop.
|
|
565
|
+
fn known_query_comparator(op: &str) -> bool {
|
|
566
|
+
matches!(op, "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "in" | "contains")
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
fn query_comparator_variant(op: &str) -> &'static str {
|
|
570
|
+
match op {
|
|
571
|
+
"eq" => "Eq",
|
|
572
|
+
"ne" => "Ne",
|
|
573
|
+
"gt" => "Gt",
|
|
574
|
+
"gte" => "Gte",
|
|
575
|
+
"lt" => "Lt",
|
|
576
|
+
"lte" => "Lte",
|
|
577
|
+
"in" => "In",
|
|
578
|
+
"contains" => "Contains",
|
|
579
|
+
other => panic!("unknown query comparator {other:?} — query_comparator_variant doesn't cover this shape (grammar-validated at declare time, so this should be unreachable for a real declared query; query_where_skip_reason should have already skipped it with an honest reason)"),
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Ruby's `Object#inspect` for a `Literal`. Numbers never reach here: an unquoted number
|
|
584
|
+
// where `QueryConditionValue::Literal` expects a `&str` would not compile, so
|
|
585
|
+
// `emit_query_condition_value` emits `NumericLiteral` for them.
|
|
586
|
+
fn literal_inspect(lit: &Literal) -> String {
|
|
587
|
+
match lit {
|
|
588
|
+
Literal::Str(s) => naming::ruby_inspect_string(s),
|
|
589
|
+
Literal::Int(n) => n.to_string(),
|
|
590
|
+
Literal::Float(n) => ruby_float_inspect(*n),
|
|
591
|
+
Literal::Bool(b) => b.to_string(),
|
|
592
|
+
Literal::Nil => "nil".to_string(),
|
|
593
|
+
Literal::Symbol(s) => format!(":{s}"),
|
|
594
|
+
Literal::Hash(pairs) => format!("{{{}}}", pairs.iter().map(|(k, v)| format!("{k}: {}", literal_inspect(v))).collect::<Vec<_>>().join(", ")),
|
|
595
|
+
Literal::Array(items) => format!("[{}]", items.iter().map(literal_inspect).collect::<Vec<_>>().join(", ")),
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/// `Float#inspect`: always carries a decimal point.
|
|
600
|
+
fn ruby_float_inspect(n: f64) -> String {
|
|
601
|
+
let text = format!("{n}");
|
|
602
|
+
if text.contains('.') || text.contains('e') || text.contains('E') {
|
|
603
|
+
text
|
|
604
|
+
} else {
|
|
605
|
+
format!("{text}.0")
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
pub fn emit_query_condition_value(condition: &Condition) -> String {
|
|
610
|
+
match &condition.arg {
|
|
611
|
+
Some(arg) => format!("crate::kernel::QueryConditionValue::Arg({})", naming::ruby_inspect_string(arg)),
|
|
612
|
+
None => {
|
|
613
|
+
let lit = condition.literal.as_ref().unwrap();
|
|
614
|
+
// The skip check already proved a numeric literal targets a numeric field.
|
|
615
|
+
match lit {
|
|
616
|
+
Literal::Int(n) => format!("crate::kernel::QueryConditionValue::NumericLiteral({})", ruby_float_inspect(*n as f64)),
|
|
617
|
+
Literal::Float(n) => format!("crate::kernel::QueryConditionValue::NumericLiteral({})", ruby_float_inspect(*n)),
|
|
618
|
+
_ => format!("crate::kernel::QueryConditionValue::Literal({})", literal_inspect(lit)),
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
pub fn emit_query_condition(condition: &Condition) -> String {
|
|
625
|
+
let comparator_expr = format!("crate::kernel::query_comparators::QueryComparator::{}", query_comparator_variant(&condition.op));
|
|
626
|
+
format!("crate::kernel::QueryCondition {{ field: {}, comparator: {comparator_expr}, value: {} }},", naming::ruby_inspect_string(&condition.field), emit_query_condition_value(condition))
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
pub struct QueryDef {
|
|
630
|
+
pub verb: String,
|
|
631
|
+
pub aggregate: String,
|
|
632
|
+
pub arg_checks: Vec<String>,
|
|
633
|
+
pub conditions: Vec<Condition>,
|
|
634
|
+
pub reference_hop_conditions: Vec<HopCondition>,
|
|
635
|
+
pub order_by: Option<String>,
|
|
636
|
+
pub offset: Option<String>,
|
|
637
|
+
pub limit: Option<String>,
|
|
638
|
+
pub authorization: Option<String>,
|
|
639
|
+
/// True when the chapter's `provides "authorization", assignments:` names this query.
|
|
640
|
+
pub assignments: bool,
|
|
641
|
+
/// `Some` for a declared entity query — emitted into `ENTITY_QUERIES`.
|
|
642
|
+
pub entity: Option<EntityScope>,
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
pub fn provided_assignments(ir: &Json) -> Option<String> {
|
|
646
|
+
ir.get("provides")
|
|
647
|
+
.map(Json::each)
|
|
648
|
+
.unwrap_or(&[])
|
|
649
|
+
.iter()
|
|
650
|
+
.find(|row| {
|
|
651
|
+
row.get("capability").and_then(Json::as_str) == Some("authorization")
|
|
652
|
+
&& row.get("key").and_then(Json::as_str) == Some("assignments")
|
|
653
|
+
})
|
|
654
|
+
.and_then(|row| row.get("verb").and_then(Json::as_str))
|
|
655
|
+
.map(str::to_string)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
pub fn emit_authorization_assignments(query_defs: &[QueryDef]) -> String {
|
|
659
|
+
let value = match query_defs.iter().find(|q| q.assignments) {
|
|
660
|
+
Some(q) => format!("Some({})", naming::ruby_inspect_string(&q.verb)),
|
|
661
|
+
None => "None".to_string(),
|
|
662
|
+
};
|
|
663
|
+
format!(
|
|
664
|
+
"/// `provides \"authorization\", assignments:` — the query `kernel::check_role_via` reads; `None` when no chapter here declares one.\npub const AUTHORIZATION_ASSIGNMENTS: Option<&str> = {value};\n"
|
|
665
|
+
)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
pub struct EntityScope {
|
|
669
|
+
pub list_field: String,
|
|
670
|
+
pub parent_key: String,
|
|
671
|
+
pub identity_keys: Vec<String>,
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
pub fn emit_entity_query_table(entity_defs: &[&QueryDef]) -> String {
|
|
675
|
+
let rows: String = entity_defs.iter().map(|q| format!("{}\n", emit_entity_query_def(q))).collect();
|
|
676
|
+
format!(
|
|
677
|
+
"/// Declared entity queries (`Aggregate.Entity.Query`) — `kernel::named_query::run_entity`.\npub const ENTITY_QUERIES: &[crate::kernel::named_query::EntityQueryDef] = &[\n{rows}];\n"
|
|
678
|
+
)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
fn emit_entity_query_def(query_def: &QueryDef) -> String {
|
|
682
|
+
let entity = query_def.entity.as_ref().expect("an entity query def carries its scope");
|
|
683
|
+
let conditions = query_def.conditions.iter().map(|c| format!(" {}", emit_query_condition(c))).collect::<Vec<_>>().join("\n");
|
|
684
|
+
let keys = entity.identity_keys.iter().map(|key| naming::ruby_inspect_string(key)).collect::<Vec<_>>().join(", ");
|
|
685
|
+
let wrap = |value: &Option<String>| match value {
|
|
686
|
+
Some(v) => format!("Some({v})"),
|
|
687
|
+
None => "None".to_string(),
|
|
688
|
+
};
|
|
689
|
+
format!(
|
|
690
|
+
"crate::kernel::named_query::EntityQueryDef {{\n verb: {},\n aggregate: {},\n list_field: {},\n parent_key: {},\n identity_keys: &[{keys}],\n conditions: &[\n{conditions}\n ],\n order_by: {},\n offset: {},\n limit: {},\n}},",
|
|
691
|
+
naming::ruby_inspect_string(&query_def.verb),
|
|
692
|
+
naming::ruby_inspect_string(&query_def.aggregate),
|
|
693
|
+
naming::ruby_inspect_string(&entity.list_field),
|
|
694
|
+
naming::ruby_inspect_string(&entity.parent_key),
|
|
695
|
+
wrap(&query_def.order_by),
|
|
696
|
+
wrap(&query_def.offset),
|
|
697
|
+
wrap(&query_def.limit)
|
|
698
|
+
)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
pub fn entity_query_skip_reason(query: &Json, entity: &Json, holds_list: bool, value_objects_by_name: &HashMap<String, &Json>) -> Option<SkipReason> {
|
|
702
|
+
let entity_name = entity.get("name").map(Json::to_s).unwrap_or_default();
|
|
703
|
+
if !holds_list {
|
|
704
|
+
return Some(skip("entity_query", format!("{entity_name} is held in no list attribute on its aggregate — nothing to flatten")));
|
|
705
|
+
}
|
|
706
|
+
if query.get("authorization").is_some() {
|
|
707
|
+
return Some(skip("entity_query_authorization", "declares authorize — an entity query's tenant scope is not generated yet"));
|
|
708
|
+
}
|
|
709
|
+
query_skip_reason(query, entity, value_objects_by_name, &HashMap::new())
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
pub fn emit_query_def(query_def: &QueryDef) -> String {
|
|
713
|
+
let conditions = query_def.conditions.iter().map(|c| format!(" {}", emit_query_condition(c))).collect::<Vec<_>>().join("\n");
|
|
714
|
+
let reference_hop_conditions = query_def.reference_hop_conditions.iter().map(|h| format!(" {}", emit_reference_hop_condition(h))).collect::<Vec<_>>().join("\n");
|
|
715
|
+
let order_by = match &query_def.order_by {
|
|
716
|
+
Some(o) => format!("Some({o})"),
|
|
717
|
+
None => "None".to_string(),
|
|
718
|
+
};
|
|
719
|
+
let offset = match &query_def.offset {
|
|
720
|
+
Some(o) => format!("Some({o})"),
|
|
721
|
+
None => "None".to_string(),
|
|
722
|
+
};
|
|
723
|
+
let limit = match &query_def.limit {
|
|
724
|
+
Some(l) => format!("Some({l})"),
|
|
725
|
+
None => "None".to_string(),
|
|
726
|
+
};
|
|
727
|
+
let authorization = match &query_def.authorization {
|
|
728
|
+
Some(a) => format!("Some({a})"),
|
|
729
|
+
None => "None".to_string(),
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
format!(
|
|
733
|
+
"crate::kernel::QueryDef {{\n verb: {},\n aggregate: {},\n conditions: &[\n{conditions}\n ],\n reference_hop_conditions: &[\n{reference_hop_conditions}\n ],\n order_by: {order_by},\n offset: {offset},\n limit: {limit},\n authorization: {authorization},\n}},",
|
|
734
|
+
naming::ruby_inspect_string(&query_def.verb),
|
|
735
|
+
naming::ruby_inspect_string(&query_def.aggregate)
|
|
736
|
+
)
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const QUERY_TABLE_ROW_PLACEHOLDER: &str = "crate::kernel::QueryDef {\n verb: \"tmpl_verb\",\n aggregate: \"tmpl_aggregate\",\n conditions: &[\n crate::kernel::QueryCondition {\n field: \"tmpl_field\",\n comparator: crate::kernel::query_comparators::QueryComparator::Eq,\n value: crate::kernel::QueryConditionValue::Literal(\"tmpl_literal\"),\n },\n ],\n reference_hop_conditions: &[\n crate::kernel::read_model::ReferenceHopCondition {\n via_field: \"tmpl_via_field\",\n target_aggregate: \"tmpl_target_aggregate\",\n through: &[crate::kernel::read_model::HopStep { via_field: \"tmpl_via_field\", target_aggregate: \"tmpl_target_aggregate\" }],\n inner_field: \"tmpl_inner_field\",\n inner_comparator: crate::kernel::query_comparators::QueryComparator::Eq,\n inner_value: crate::kernel::QueryConditionValue::Literal(\"tmpl_literal\"),\n },\n ],\n order_by: Some(crate::kernel::query_ordering::OrderBy { field: \"tmpl_order_field\", descending: true, nulls: crate::kernel::query_ordering::NullsMode::Last }),\n offset: Some(crate::kernel::query_ordering::Offset::Literal(1)),\n limit: Some(crate::kernel::query_ordering::Limit::Literal(5)),\n authorization: Some(crate::kernel::named_query::TenantAuth { query_name: \"tmpl_query_name\", tenant_field: \"tmpl_tenant_field\", policy: \"tmpl_policy\" }),\n},";
|
|
740
|
+
|
|
741
|
+
pub fn emit_query_table(exemplar: &Exemplar, query_defs: &[QueryDef]) -> String {
|
|
742
|
+
let (entity_defs, aggregate_defs): (Vec<&QueryDef>, Vec<&QueryDef>) = query_defs.iter().partition(|q| q.entity.is_some());
|
|
743
|
+
let rows: Vec<String> = aggregate_defs.into_iter().map(emit_query_def).collect();
|
|
744
|
+
format!(
|
|
745
|
+
"{}\n{}{}",
|
|
746
|
+
exemplar.render("query_table", &[(QUERY_TABLE_ROW_PLACEHOLDER, rows.join("\n"))]),
|
|
747
|
+
emit_authorization_assignments(query_defs),
|
|
748
|
+
emit_entity_query_table(&entity_defs)
|
|
749
|
+
)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// One `if let` per value-object argument: built and invariant-checked, then dropped (ADR 0037).
|
|
753
|
+
pub fn query_arg_checks(query: &Json, mod_path: &str, value_objects_by_name: &HashMap<String, &Json>) -> Vec<String> {
|
|
754
|
+
query
|
|
755
|
+
.get("attributes")
|
|
756
|
+
.map(Json::each)
|
|
757
|
+
.unwrap_or(&[])
|
|
758
|
+
.iter()
|
|
759
|
+
.filter_map(|attr| {
|
|
760
|
+
if crate::attr::list(attr) || !matches!(attr.get("relationship"), None | Some(Json::Null)) {
|
|
761
|
+
return None;
|
|
762
|
+
}
|
|
763
|
+
let vo = value_objects_by_name.get(crate::attr::type_name(attr))?;
|
|
764
|
+
let key = naming::rust_field(crate::attr::name(attr));
|
|
765
|
+
let build = crate::json_codec::composite_from_json_expr(attr, value_objects_by_name, "x");
|
|
766
|
+
let check = if vo.get("closed_set").map(Json::as_bool).unwrap_or(false) { "" } else { ".check_invariants()?" };
|
|
767
|
+
Some(format!("if let Some(x) = args.get({}) {{ {mod_path}::{build}{check}; }}", naming::ruby_inspect_string(&key)))
|
|
768
|
+
})
|
|
769
|
+
.collect()
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// The gate `kernel/cli.rs` runs before `named_query::run`.
|
|
773
|
+
pub fn emit_query_arg_check_table(query_defs: &[QueryDef]) -> String {
|
|
774
|
+
let arms: Vec<String> = query_defs
|
|
775
|
+
.iter()
|
|
776
|
+
.filter(|q| !q.arg_checks.is_empty())
|
|
777
|
+
.map(|q| {
|
|
778
|
+
let lines = q.arg_checks.iter().map(|line| format!(" {line}")).collect::<Vec<_>>().join("\n");
|
|
779
|
+
format!(" {} => {{\n{lines}\n Ok(())\n }}", naming::ruby_inspect_string(&q.verb))
|
|
780
|
+
})
|
|
781
|
+
.collect();
|
|
782
|
+
format!(
|
|
783
|
+
"/// C3.7 for a named query's own arguments — `query_arg_checks`\n/// (rust/project/queries.rb) has the full story.\npub fn check_query_args(verb: &str, args: &crate::kernel::Json) -> Result<(), crate::kernel::Refusal> {{\n match verb {{\n{}\n _ => Ok(()),\n }}\n}}\n",
|
|
784
|
+
arms.join("\n")
|
|
785
|
+
)
|
|
786
|
+
}
|