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,149 @@
|
|
|
1
|
+
//! Cross-aggregate reference dereference for `given`/`ensures` clauses.
|
|
2
|
+
//! Rust counterpart of `CommandRules::References#dereference` (references.rb).
|
|
3
|
+
|
|
4
|
+
use super::{Field, Fielded, Value};
|
|
5
|
+
|
|
6
|
+
/// A declared `reference_to`/`belongs_to` field: stored name, clause name, target aggregate.
|
|
7
|
+
#[derive(Clone, Copy)]
|
|
8
|
+
pub struct ReferenceSpec {
|
|
9
|
+
pub field: &'static str,
|
|
10
|
+
pub as_name: &'static str,
|
|
11
|
+
pub target: &'static str,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// Per-aggregate reference specs keyed by fully qualified aggregate name.
|
|
15
|
+
pub type ReferenceTable = &'static [(&'static str, &'static [ReferenceSpec])];
|
|
16
|
+
|
|
17
|
+
/// Maximum hops followed; bounds recursion on cyclic references.
|
|
18
|
+
// A cycle (A -> B -> A) is representable in `ReferenceTable`, so recursion needs a floor.
|
|
19
|
+
pub const DEREFERENCE_DEPTH: usize = 4;
|
|
20
|
+
|
|
21
|
+
/// Fetches a record by aggregate name and id, type-erased behind `Fielded`.
|
|
22
|
+
// Reachable only through `store`: resolve into owned `DerefNode`s before the mutable borrow.
|
|
23
|
+
pub trait ReferenceLookup {
|
|
24
|
+
fn find_fielded(&self, target: &str, id: &str) -> Option<Box<dyn Fielded>>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn specs_for(table: ReferenceTable, target: &str) -> &'static [ReferenceSpec] {
|
|
28
|
+
table.iter().find(|(name, _)| *name == target).map(|(_, specs)| *specs).unwrap_or(&[])
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// A fetched record plus its dereferenced nested references.
|
|
32
|
+
pub struct DerefNode {
|
|
33
|
+
id: String,
|
|
34
|
+
base: Box<dyn Fielded>,
|
|
35
|
+
nested: Vec<(&'static str, DerefNode)>,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl Fielded for DerefNode {
|
|
39
|
+
fn field(&self, name: &str) -> Option<Field<'_>> {
|
|
40
|
+
// `nested` is checked first so a reference name beats a same-named field.
|
|
41
|
+
if let Some((_, node)) = self.nested.iter().find(|(n, _)| *n == name) {
|
|
42
|
+
return Some(Field::Nested(node));
|
|
43
|
+
}
|
|
44
|
+
self.base.field(name)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// `Value` has no map variant; equal ids mean the same reference.
|
|
48
|
+
fn as_scalar(&self) -> Option<Value> {
|
|
49
|
+
Some(Value::Str(self.id.clone()))
|
|
50
|
+
}
|
|
51
|
+
fn items(&self, name: &str) -> Option<Vec<Field<'_>>> {
|
|
52
|
+
self.base.items(name)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn deref_layer(lookup: &dyn ReferenceLookup, table: ReferenceTable, specs: &'static [ReferenceSpec], source: &dyn Fielded, depth: usize) -> Vec<(&'static str, DerefNode)> {
|
|
57
|
+
if depth == 0 {
|
|
58
|
+
return Vec::new();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
specs
|
|
62
|
+
.iter()
|
|
63
|
+
.filter_map(|spec| {
|
|
64
|
+
let Some(Field::Value(Value::Str(id))) = source.field(spec.field) else { return None };
|
|
65
|
+
// Absent or dangling ids are skipped, not refused: direct ones were already checked.
|
|
66
|
+
if id.is_empty() {
|
|
67
|
+
return None;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let base = lookup.find_fielded(spec.target, &id)?;
|
|
71
|
+
let nested = deref_layer(lookup, table, specs_for(table, spec.target), base.as_ref(), depth - 1);
|
|
72
|
+
Some((spec.as_name, DerefNode { id, base, nested }))
|
|
73
|
+
})
|
|
74
|
+
.collect()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Dereferences an aggregate/entity's stored reference fields, spread across top-level names.
|
|
78
|
+
pub fn owner_deref(lookup: &dyn ReferenceLookup, table: ReferenceTable, target: &'static str, id: &str) -> Vec<(&'static str, DerefNode)> {
|
|
79
|
+
let Some(base) = lookup.find_fielded(target, id) else { return Vec::new() };
|
|
80
|
+
deref_layer(lookup, table, specs_for(table, target), base.as_ref(), DEREFERENCE_DEPTH)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Wraps an entity command's parent aggregate as one dereferenced node.
|
|
84
|
+
pub fn parent_deref(lookup: &dyn ReferenceLookup, table: ReferenceTable, target: &'static str, id: &str) -> Option<DerefNode> {
|
|
85
|
+
let base = lookup.find_fielded(target, id)?;
|
|
86
|
+
let nested = deref_layer(lookup, table, specs_for(table, target), base.as_ref(), DEREFERENCE_DEPTH);
|
|
87
|
+
Some(DerefNode { id: id.to_string(), base, nested })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Dereferences a command's reference-typed arguments.
|
|
91
|
+
pub fn command_deref(lookup: &dyn ReferenceLookup, table: ReferenceTable, specs: &'static [ReferenceSpec], args: &dyn Fielded) -> Vec<(&'static str, DerefNode)> {
|
|
92
|
+
deref_layer(lookup, table, specs, args, DEREFERENCE_DEPTH)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// The `Fielded` view `given`/`ensures` read: `command_deref`, then `args`, then `owner_deref`.
|
|
96
|
+
pub struct WithReferences<'a> {
|
|
97
|
+
pub command_deref: &'a [(&'static str, DerefNode)],
|
|
98
|
+
pub args: &'a dyn Fielded,
|
|
99
|
+
pub owner_deref: &'a [(&'static str, DerefNode)],
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
impl<'a> Fielded for WithReferences<'a> {
|
|
103
|
+
fn field(&self, name: &str) -> Option<Field<'_>> {
|
|
104
|
+
// `command_deref` must win: an aliased reference shares its name with the raw id argument.
|
|
105
|
+
if let Some((_, node)) = self.command_deref.iter().find(|(n, _)| *n == name) {
|
|
106
|
+
return Some(Field::Nested(node));
|
|
107
|
+
}
|
|
108
|
+
if let Some(f) = self.args.field(name) {
|
|
109
|
+
return Some(f);
|
|
110
|
+
}
|
|
111
|
+
if let Some((_, node)) = self.owner_deref.iter().find(|(n, _)| *n == name) {
|
|
112
|
+
return Some(Field::Nested(node));
|
|
113
|
+
}
|
|
114
|
+
None
|
|
115
|
+
}
|
|
116
|
+
fn items(&self, name: &str) -> Option<Vec<Field<'_>>> {
|
|
117
|
+
self.args.items(name)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// A `projects` field: its name, the reference it follows, and the remote field it reads.
|
|
122
|
+
pub struct ProjectedFieldSpec {
|
|
123
|
+
pub field: &'static str,
|
|
124
|
+
pub reference: &'static str,
|
|
125
|
+
pub remote_field: &'static str,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Reads each projected field off the already-dereferenced references.
|
|
129
|
+
pub fn seeded_projections(with_references: &dyn Fielded, specs: &'static [ProjectedFieldSpec]) -> Vec<(&'static str, Option<String>)> {
|
|
130
|
+
specs
|
|
131
|
+
.iter()
|
|
132
|
+
.map(|spec| {
|
|
133
|
+
// Only `Option<String>` remote fields are seeded; other types would need a coercion.
|
|
134
|
+
let value = match with_references.field(spec.reference) {
|
|
135
|
+
Some(Field::Nested(node)) => match node.field(spec.remote_field) {
|
|
136
|
+
Some(Field::Value(Value::Str(s))) => Some(s),
|
|
137
|
+
_ => None,
|
|
138
|
+
},
|
|
139
|
+
_ => None,
|
|
140
|
+
};
|
|
141
|
+
(spec.field, value)
|
|
142
|
+
})
|
|
143
|
+
.collect()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Write side of `projects`: one generated match arm per projected field.
|
|
147
|
+
pub trait SetProjectedField {
|
|
148
|
+
fn set_projected_field(&mut self, name: &'static str, value: Option<String>);
|
|
149
|
+
}
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
//! Storage trait and in-memory implementation shared by every generated aggregate.
|
|
2
|
+
//! The adapter persistence contract is docs/implemented/guides/writing-an-adapter.md.
|
|
3
|
+
|
|
4
|
+
use std::cell::OnceCell;
|
|
5
|
+
use std::collections::BTreeMap;
|
|
6
|
+
|
|
7
|
+
pub trait Repository<T: Clone> {
|
|
8
|
+
fn find(&self, id: &str) -> Option<T>;
|
|
9
|
+
fn save(&mut self, id: &str, record: T);
|
|
10
|
+
fn all(&self) -> Vec<T>;
|
|
11
|
+
fn count(&self) -> usize;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// One held record plus its `to_json()` rendering, built the first time a scan asks for it.
|
|
15
|
+
///
|
|
16
|
+
/// A slot is only ever replaced whole by `save`, so the rendering can never outlive the record
|
|
17
|
+
/// it was built from.
|
|
18
|
+
#[derive(Clone)]
|
|
19
|
+
struct Slot<T> {
|
|
20
|
+
record: T,
|
|
21
|
+
json: OnceCell<super::Json>,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#[derive(Default, Clone)]
|
|
25
|
+
pub struct InMemoryRepository<T: Clone> {
|
|
26
|
+
records: BTreeMap<String, Slot<T>>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl<T: Clone> InMemoryRepository<T> {
|
|
30
|
+
pub fn new() -> Self {
|
|
31
|
+
Self { records: BTreeMap::new() }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// `(id, record)` pairs; `Repository::all` drops the id, which the CLI `instances` dump needs.
|
|
35
|
+
/// Inherent rather than on the trait: only the hand-written CLI calls it.
|
|
36
|
+
pub fn entries(&self) -> impl Iterator<Item = (&String, &T)> {
|
|
37
|
+
self.records.iter().map(|(id, slot)| (id, &slot.record))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// `(id, record.to_json())` pairs in id order, borrowed from a per-record cache.
|
|
41
|
+
///
|
|
42
|
+
/// `to_json` runs once per record per save, not once per scan, so a query over an unchanged
|
|
43
|
+
/// store allocates nothing until a row actually matches. `to_json` must be a pure function
|
|
44
|
+
/// of the record, as every generated `to_json` is.
|
|
45
|
+
pub fn json_entries<'a>(
|
|
46
|
+
&'a self,
|
|
47
|
+
to_json: impl Fn(&T) -> super::Json + 'a,
|
|
48
|
+
) -> impl Iterator<Item = (&'a String, &'a super::Json)> {
|
|
49
|
+
self.records.iter().map(move |(id, slot)| (id, slot.json.get_or_init(|| to_json(&slot.record))))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
impl<T: Clone> Repository<T> for InMemoryRepository<T> {
|
|
54
|
+
fn find(&self, id: &str) -> Option<T> {
|
|
55
|
+
self.records.get(id).map(|slot| slot.record.clone())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fn save(&mut self, id: &str, record: T) {
|
|
59
|
+
self.records.insert(id.to_string(), Slot { record, json: OnceCell::new() });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn all(&self) -> Vec<T> {
|
|
63
|
+
self.records.values().map(|slot| slot.record.clone()).collect()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn count(&self) -> usize {
|
|
67
|
+
self.records.len()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Refuses with `NotFound` when `value` names a record missing from `repo`; empty means no check.
|
|
72
|
+
///
|
|
73
|
+
/// Generic and hand-written because the check reads a repository other than the dispatching
|
|
74
|
+
/// command's own; the generated `dispatch_by_name` supplies the repo and attribute.
|
|
75
|
+
pub fn check_reference<T: Clone>(
|
|
76
|
+
repo: &impl Repository<T>,
|
|
77
|
+
value: &str,
|
|
78
|
+
target: &'static str,
|
|
79
|
+
heads: &'static str,
|
|
80
|
+
) -> Result<(), super::Refusal> {
|
|
81
|
+
if value.is_empty() || repo.find(value).is_some() {
|
|
82
|
+
return Ok(());
|
|
83
|
+
}
|
|
84
|
+
// Wording goes through `RefusalSite` so it cannot drift from the Ruby table.
|
|
85
|
+
Err(super::Refusal::NotFound(
|
|
86
|
+
super::refusal_wording::NotFoundReferenceTargetMissingArgs { target, heads, key: value }.render_args(),
|
|
87
|
+
))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Lets the CLI list one aggregate's `(id, to_json())` pairs from a runtime name such as
|
|
91
|
+
/// "Banking::Account", without knowing which domain is compiled in.
|
|
92
|
+
///
|
|
93
|
+
/// The default answers `None` for every name (unknown aggregate); generated stores override it.
|
|
94
|
+
pub trait AggregateScan {
|
|
95
|
+
fn scan(&self, aggregate: &str) -> Option<Vec<(String, super::Json)>> {
|
|
96
|
+
let _ = aggregate;
|
|
97
|
+
None
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Calls `visit` with each `(id, record.to_json())` of `aggregate` in id order, borrowing the
|
|
101
|
+
/// rows so a caller that keeps only some of them clones only those.
|
|
102
|
+
///
|
|
103
|
+
/// Answers `false` for an unknown aggregate, where `scan` answers `None`. The default walks
|
|
104
|
+
/// `scan`, so a store that overrides only `scan` still answers; generated stores override
|
|
105
|
+
/// this to read their repositories' cached renderings.
|
|
106
|
+
fn scan_each(&self, aggregate: &str, visit: &mut dyn FnMut(&str, &super::Json)) -> bool {
|
|
107
|
+
let Some(entries) = self.scan(aggregate) else { return false };
|
|
108
|
+
for (id, record) in &entries {
|
|
109
|
+
visit(id, record);
|
|
110
|
+
}
|
|
111
|
+
true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Keeps the entries whose dotted `field` satisfies `comparator` against `want`, sorted by id.
|
|
116
|
+
///
|
|
117
|
+
/// The id sort matches Ruby, which orders unordered queries by `record.id.to_s`; it does not
|
|
118
|
+
/// rely on `AggregateScan::scan` returning id order.
|
|
119
|
+
pub fn filter_entries(
|
|
120
|
+
entries: Vec<(String, super::Json)>,
|
|
121
|
+
field: &str,
|
|
122
|
+
comparator: super::query_comparators::QueryComparator,
|
|
123
|
+
want: &super::Json,
|
|
124
|
+
) -> Vec<(String, super::Json)> {
|
|
125
|
+
filter_entries_cross_domain(entries, field, comparator, want, &[])
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// `filter_entries` plus a `cross_domain` scan list, which `NoneInState` needs to look up other
|
|
129
|
+
/// aggregates. The plain function passes an empty list, so `NoneInState` is vacuously true there.
|
|
130
|
+
pub fn filter_entries_cross_domain(
|
|
131
|
+
entries: Vec<(String, super::Json)>,
|
|
132
|
+
field: &str,
|
|
133
|
+
comparator: super::query_comparators::QueryComparator,
|
|
134
|
+
want: &super::Json,
|
|
135
|
+
cross_domain: &[(&str, &dyn AggregateScan)],
|
|
136
|
+
) -> Vec<(String, super::Json)> {
|
|
137
|
+
let want = super::query_comparators::comparable(want);
|
|
138
|
+
let mut matched: Vec<(String, super::Json)> = entries
|
|
139
|
+
.into_iter()
|
|
140
|
+
.filter(|(_, record)| entry_matches(record, field, comparator, &want, cross_domain))
|
|
141
|
+
.collect();
|
|
142
|
+
matched.sort_by(|a, b| a.0.cmp(&b.0));
|
|
143
|
+
matched
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// True when `record`'s dotted `field` satisfies `comparator` against `want`, which the caller
|
|
147
|
+
/// has already reduced through `query_comparators::comparable`.
|
|
148
|
+
///
|
|
149
|
+
/// The per-record test `filter_entries_cross_domain` applies; `named_query::run` calls it on
|
|
150
|
+
/// borrowed rows so a record that fails is never cloned. A missing field reads as `Json::Null`.
|
|
151
|
+
pub fn entry_matches(
|
|
152
|
+
record: &super::Json,
|
|
153
|
+
field: &str,
|
|
154
|
+
comparator: super::query_comparators::QueryComparator,
|
|
155
|
+
want: &super::Json,
|
|
156
|
+
cross_domain: &[(&str, &dyn AggregateScan)],
|
|
157
|
+
) -> bool {
|
|
158
|
+
static NULL: super::Json = super::Json::Null;
|
|
159
|
+
let held = super::query_comparators::comparable_ref(record.dig(field).unwrap_or(&NULL));
|
|
160
|
+
if comparator == super::query_comparators::QueryComparator::NoneInState {
|
|
161
|
+
super::query_comparators::none_in_state_matches(cross_domain, held, want)
|
|
162
|
+
} else {
|
|
163
|
+
comparator.matches(held, want)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// Prepends the field `"id"` to a record's `to_json()` object, matching Ruby's query row shape.
|
|
168
|
+
///
|
|
169
|
+
/// Shared because read models nest this wrapping at every level, not only the top row.
|
|
170
|
+
pub fn row_json(id: String, record: super::Json) -> super::Json {
|
|
171
|
+
let super::Json::Object(mut fields) = record else { return record };
|
|
172
|
+
fields.insert(0, ("id".to_string(), super::Json::Str(id)));
|
|
173
|
+
super::Json::Object(fields)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// True when `actor_id` holds `role` through a live (`ends_at` unset) assignment.
|
|
177
|
+
///
|
|
178
|
+
/// Runs the chapter-declared `assignments` query through `named_query::run`; `None`, or a verb
|
|
179
|
+
/// missing from `queries`, holds no role. Ruby likewise ignores scope and `starts_at`.
|
|
180
|
+
pub fn holds_role_via(
|
|
181
|
+
store: &impl AggregateScan,
|
|
182
|
+
queries: &[super::QueryDef],
|
|
183
|
+
assignments: Option<&str>,
|
|
184
|
+
actor_id: &str,
|
|
185
|
+
role: &str,
|
|
186
|
+
) -> bool {
|
|
187
|
+
let Some(def) = assignments.and_then(|verb| super::named_query::find(queries, verb)) else {
|
|
188
|
+
return false;
|
|
189
|
+
};
|
|
190
|
+
let args = super::Json::Object(vec![("actor_id".to_string(), super::Json::Str(actor_id.to_string()))]);
|
|
191
|
+
let Ok(rows) = super::named_query::run(store, def, &args, None) else { return false };
|
|
192
|
+
rows.iter().any(|(_, record)| {
|
|
193
|
+
let matches_role = record.dig("role_name.value").and_then(super::Json::as_str) == Some(role);
|
|
194
|
+
let not_revoked = matches!(record.dig("ends_at"), None | Some(super::Json::Null));
|
|
195
|
+
matches_role && not_revoked
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// Refuses with `Unauthorized` when the caller's role does not satisfy `command_role`.
|
|
200
|
+
///
|
|
201
|
+
/// Unchecked unless both a caller and a command role are present. With `caller_actor_id` and an
|
|
202
|
+
/// authorization provider compiled in (`assignments` names a query in `queries`), the role must be
|
|
203
|
+
/// held through `holds_role_via`; otherwise the caller's stated role is compared as a string.
|
|
204
|
+
/// Governance's own commands get no exemption: the running Ruby checks them like any other,
|
|
205
|
+
/// despite the comment above `governance_attached?` saying otherwise.
|
|
206
|
+
/// Only the outermost dispatch carries a caller; reactions pass `None`, as in Ruby.
|
|
207
|
+
pub fn check_role_via(
|
|
208
|
+
command_role: Option<&str>,
|
|
209
|
+
command_name: &str,
|
|
210
|
+
caller_role: Option<&str>,
|
|
211
|
+
caller_actor_id: Option<&str>,
|
|
212
|
+
store: &impl AggregateScan,
|
|
213
|
+
queries: &[super::QueryDef],
|
|
214
|
+
assignments: Option<&str>,
|
|
215
|
+
) -> Result<(), super::Refusal> {
|
|
216
|
+
let (Some(caller), Some(role)) = (caller_role, command_role) else { return Ok(()) };
|
|
217
|
+
|
|
218
|
+
let attached = assignments.and_then(|verb| super::named_query::find(queries, verb)).is_some();
|
|
219
|
+
let authorized = match caller_actor_id {
|
|
220
|
+
Some(actor_id) if attached => holds_role_via(store, queries, assignments, actor_id, role),
|
|
221
|
+
_ => caller == role,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if authorized {
|
|
225
|
+
return Ok(());
|
|
226
|
+
}
|
|
227
|
+
// The refusal quotes the role the caller typed, not the one `holds_role_via` found (as Ruby).
|
|
228
|
+
Err(super::Refusal::Unauthorized(
|
|
229
|
+
super::refusal_wording::UnauthorizedRoleMismatchArgs { command: command_name, role, caller_role: caller }.render_args(),
|
|
230
|
+
))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// End-to-end pin of `filter_entries_cross_domain` against the scenario in
|
|
234
|
+
// spec/query_none_in_state_growth_spec.rb, which expects surviving ids `c2` and `nonexistent`.
|
|
235
|
+
#[cfg(test)]
|
|
236
|
+
mod filter_entries_none_in_state_tests {
|
|
237
|
+
use super::{filter_entries_cross_domain, AggregateScan};
|
|
238
|
+
use crate::kernel::query_comparators::QueryComparator;
|
|
239
|
+
use crate::kernel::Json;
|
|
240
|
+
|
|
241
|
+
struct FakeStore {
|
|
242
|
+
domain: &'static str,
|
|
243
|
+
aggregates: Vec<(&'static str, Vec<(String, Json)>)>,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
impl AggregateScan for FakeStore {
|
|
247
|
+
fn scan(&self, aggregate: &str) -> Option<Vec<(String, Json)>> {
|
|
248
|
+
let bare = aggregate.strip_prefix(&format!("{}::", self.domain))?;
|
|
249
|
+
self.aggregates.iter().find(|(name, _)| *name == bare).map(|(_, entries)| entries.clone())
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
fn claim_record(state: &str) -> Json {
|
|
254
|
+
Json::obj(vec![("state", Json::obj(vec![("value", Json::str(state))]))])
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
fn assignment_record(claim_id: &str) -> Json {
|
|
258
|
+
Json::obj(vec![("claim_id", Json::str(claim_id))])
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#[test]
|
|
262
|
+
fn matches_the_real_ruby_spec_s_own_expected_surviving_ids() {
|
|
263
|
+
let claims = FakeStore {
|
|
264
|
+
domain: "AntiJoinGrowth",
|
|
265
|
+
aggregates: vec![(
|
|
266
|
+
"Claim",
|
|
267
|
+
vec![
|
|
268
|
+
("c1".into(), claim_record("held")), // stays held -> excluded
|
|
269
|
+
("c2".into(), claim_record("released")), // no longer held -> kept
|
|
270
|
+
],
|
|
271
|
+
)],
|
|
272
|
+
};
|
|
273
|
+
let cross_domain: Vec<(&str, &dyn AggregateScan)> = vec![("AntiJoinGrowth", &claims)];
|
|
274
|
+
|
|
275
|
+
let assignments = vec![
|
|
276
|
+
("b1:c1".to_string(), assignment_record("c1")),
|
|
277
|
+
("b1:c2".to_string(), assignment_record("c2")),
|
|
278
|
+
// "nonexistent" -> no Claim record at all -> kept
|
|
279
|
+
("b1:c3".to_string(), assignment_record("nonexistent")),
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
let matched = filter_entries_cross_domain(
|
|
283
|
+
assignments,
|
|
284
|
+
"claim_id",
|
|
285
|
+
QueryComparator::NoneInState,
|
|
286
|
+
&Json::str("Claim:held"),
|
|
287
|
+
&cross_domain,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
let claim_ids: Vec<&str> =
|
|
291
|
+
matched.iter().map(|(_, record)| record.dig("claim_id").and_then(Json::as_str).unwrap()).collect();
|
|
292
|
+
assert_eq!(claim_ids, vec!["c2", "nonexistent"]);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#[cfg(test)]
|
|
297
|
+
mod check_role_actor_id_tests {
|
|
298
|
+
use super::{check_role_via, AggregateScan};
|
|
299
|
+
use crate::kernel::query_comparators::QueryComparator;
|
|
300
|
+
use crate::kernel::{Json, QueryCondition, QueryConditionValue, QueryDef, Refusal};
|
|
301
|
+
|
|
302
|
+
/// The assignments query a real Governance attachment declares.
|
|
303
|
+
const GOVERNANCE_ASSIGNMENTS: &str = "Governance::RoleAssignment.AssignmentsForActor";
|
|
304
|
+
|
|
305
|
+
fn check_role(
|
|
306
|
+
command_role: Option<&str>,
|
|
307
|
+
command_name: &str,
|
|
308
|
+
caller_role: Option<&str>,
|
|
309
|
+
caller_actor_id: Option<&str>,
|
|
310
|
+
store: &impl AggregateScan,
|
|
311
|
+
queries: &[QueryDef],
|
|
312
|
+
) -> Result<(), Refusal> {
|
|
313
|
+
check_role_via(command_role, command_name, caller_role, caller_actor_id, store, queries, Some(GOVERNANCE_ASSIGNMENTS))
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/// Mirrors the merged `Store` compiled for a domain using Governance: one `RoleAssignment`
|
|
317
|
+
/// aggregate scanned under the "Governance::RoleAssignment" prefix.
|
|
318
|
+
struct FakeStore {
|
|
319
|
+
role_assignments: Vec<(String, Json)>,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
impl AggregateScan for FakeStore {
|
|
323
|
+
fn scan(&self, aggregate: &str) -> Option<Vec<(String, Json)>> {
|
|
324
|
+
if aggregate == "Governance::RoleAssignment" {
|
|
325
|
+
Some(self.role_assignments.clone())
|
|
326
|
+
} else {
|
|
327
|
+
None
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/// The `AssignmentsForActor` `QueryDef` a merged `Store` compiles.
|
|
333
|
+
fn assignments_for_actor_query() -> QueryDef {
|
|
334
|
+
QueryDef {
|
|
335
|
+
verb: "Governance::RoleAssignment.AssignmentsForActor",
|
|
336
|
+
aggregate: "Governance::RoleAssignment",
|
|
337
|
+
conditions: &[QueryCondition { field: "actor_id", comparator: QueryComparator::Eq, value: QueryConditionValue::Arg("actor_id") }],
|
|
338
|
+
reference_hop_conditions: &[],
|
|
339
|
+
order_by: None,
|
|
340
|
+
offset: None,
|
|
341
|
+
limit: None,
|
|
342
|
+
authorization: None,
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
fn role_assignment(actor_id: &str, role_name: &str, ends_at: Option<&str>) -> Json {
|
|
347
|
+
Json::obj(vec![
|
|
348
|
+
("actor_id", Json::obj(vec![("value", Json::str(actor_id.to_string()))])),
|
|
349
|
+
("role_name", Json::obj(vec![("value", Json::str(role_name.to_string()))])),
|
|
350
|
+
("scope", Json::obj(vec![("value", Json::str("kitchen".to_string()))])),
|
|
351
|
+
("starts_at", Json::obj(vec![("value", Json::str("2026-01-01".to_string()))])),
|
|
352
|
+
("ends_at", match ends_at {
|
|
353
|
+
Some(ts) => Json::obj(vec![("value", Json::str(ts.to_string()))]),
|
|
354
|
+
None => Json::Null,
|
|
355
|
+
}),
|
|
356
|
+
])
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// An identified actor with no grant is refused even when the typed role matches.
|
|
360
|
+
#[test]
|
|
361
|
+
fn refuses_an_identified_caller_with_no_matching_grant_even_though_the_typed_role_matches() {
|
|
362
|
+
let store = FakeStore { role_assignments: vec![] };
|
|
363
|
+
let queries = [assignments_for_actor_query()];
|
|
364
|
+
|
|
365
|
+
let result = check_role(Some("Chef"), "Prepare", Some("Chef"), Some("attacker"), &store, &queries);
|
|
366
|
+
|
|
367
|
+
assert!(result.is_err(), "an actor with no real grant must be refused even though it typed the right role");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// A live (non-revoked) matching grant is accepted.
|
|
371
|
+
#[test]
|
|
372
|
+
fn accepts_an_identified_caller_with_a_live_matching_grant() {
|
|
373
|
+
let store = FakeStore { role_assignments: vec![("ra1".to_string(), role_assignment("u1", "Chef", None))] };
|
|
374
|
+
let queries = [assignments_for_actor_query()];
|
|
375
|
+
|
|
376
|
+
let result = check_role(Some("Chef"), "Prepare", Some("Chef"), Some("u1"), &store, &queries);
|
|
377
|
+
|
|
378
|
+
assert!(result.is_ok(), "a live matching grant must be accepted: {result:?}");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// A revoked grant (`ends_at` set) is refused.
|
|
382
|
+
#[test]
|
|
383
|
+
fn refuses_a_revoked_grant() {
|
|
384
|
+
let store = FakeStore { role_assignments: vec![("ra1".to_string(), role_assignment("u1", "Chef", Some("2026-06-01")))] };
|
|
385
|
+
let queries = [assignments_for_actor_query()];
|
|
386
|
+
|
|
387
|
+
let result = check_role(Some("Chef"), "Prepare", Some("Chef"), Some("u1"), &store, &queries);
|
|
388
|
+
|
|
389
|
+
assert!(result.is_err(), "a revoked grant must not authorize: {result:?}");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// A caller with only `role:` keeps the plain string comparison, whatever grants exist.
|
|
393
|
+
#[test]
|
|
394
|
+
fn a_bare_role_only_caller_is_unaffected_by_any_real_grant_data() {
|
|
395
|
+
let store = FakeStore { role_assignments: vec![("ra1".to_string(), role_assignment("u1", "Customer", None))] };
|
|
396
|
+
let queries = [assignments_for_actor_query()];
|
|
397
|
+
|
|
398
|
+
// Matching string, no actor_id: accepted.
|
|
399
|
+
let matches = check_role(Some("Chef"), "Prepare", Some("Chef"), None, &store, &queries);
|
|
400
|
+
assert!(matches.is_ok(), "a bare matching role string must still be accepted unchanged: {matches:?}");
|
|
401
|
+
|
|
402
|
+
// Mismatched string, no actor_id: refused.
|
|
403
|
+
let mismatches = check_role(Some("Chef"), "Prepare", Some("Customer"), None, &store, &queries);
|
|
404
|
+
assert!(mismatches.is_err(), "a bare mismatched role string must still refuse unchanged");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Without an `AssignmentsForActor` query, a bound `actor_id` falls back to the string check.
|
|
408
|
+
#[test]
|
|
409
|
+
fn falls_back_to_the_string_check_when_this_domain_never_attached_governance() {
|
|
410
|
+
let store = FakeStore { role_assignments: vec![] };
|
|
411
|
+
let queries: [QueryDef; 0] = [];
|
|
412
|
+
|
|
413
|
+
let matches = check_role(Some("Chef"), "Prepare", Some("Chef"), Some("whoever"), &store, &queries);
|
|
414
|
+
assert!(matches.is_ok(), "no AssignmentsForActor query compiled in -> string fallback, matching role -> accepted: {matches:?}");
|
|
415
|
+
|
|
416
|
+
let mismatches = check_role(Some("Chef"), "Prepare", Some("Customer"), Some("whoever"), &store, &queries);
|
|
417
|
+
assert!(mismatches.is_err(), "no AssignmentsForActor query compiled in -> string fallback, mismatched role -> refused");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// The declared provider verb is honoured, not the Governance query name...
|
|
421
|
+
#[test]
|
|
422
|
+
fn check_role_via_reads_whichever_assignments_query_the_chapter_declared() {
|
|
423
|
+
let declared = QueryDef { verb: "Access::Grant.HeldBy", ..assignments_for_actor_query() };
|
|
424
|
+
let queries = [declared];
|
|
425
|
+
|
|
426
|
+
let granted = FakeStore { role_assignments: vec![("ra1".to_string(), role_assignment("u1", "Chef", None))] };
|
|
427
|
+
let accepted = check_role_via(Some("Chef"), "Prepare", Some("Chef"), Some("u1"), &granted, &queries, Some("Access::Grant.HeldBy"));
|
|
428
|
+
assert!(accepted.is_ok(), "a live grant under the declared query must be accepted: {accepted:?}");
|
|
429
|
+
|
|
430
|
+
let ungranted = FakeStore { role_assignments: vec![] };
|
|
431
|
+
let refused = check_role_via(Some("Chef"), "Prepare", Some("Chef"), Some("u1"), &ungranted, &queries, Some("Access::Grant.HeldBy"));
|
|
432
|
+
assert!(refused.is_err(), "no grant under the declared query must refuse even though the typed role matches");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ...and with no declared provider, a Governance-named query is ignored.
|
|
436
|
+
#[test]
|
|
437
|
+
fn check_role_via_ignores_a_governance_named_query_nobody_declared() {
|
|
438
|
+
let store = FakeStore { role_assignments: vec![] };
|
|
439
|
+
let queries = [assignments_for_actor_query()];
|
|
440
|
+
|
|
441
|
+
let result = check_role_via(Some("Chef"), "Prepare", Some("Chef"), Some("whoever"), &store, &queries, None);
|
|
442
|
+
assert!(result.is_ok(), "no declared provider -> string fallback, matching role -> accepted: {result:?}");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
#[cfg(test)]
|
|
447
|
+
mod json_cache_tests {
|
|
448
|
+
use super::{InMemoryRepository, Repository};
|
|
449
|
+
use crate::kernel::Json;
|
|
450
|
+
|
|
451
|
+
fn render(n: &i64) -> Json {
|
|
452
|
+
Json::obj(vec![("n", Json::int(*n))])
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// A save replaces the whole slot, so a scan after it never sees the old rendering.
|
|
456
|
+
#[test]
|
|
457
|
+
fn json_entries_follow_a_resave_and_stay_in_id_order() {
|
|
458
|
+
let mut repo: InMemoryRepository<i64> = InMemoryRepository::new();
|
|
459
|
+
repo.save("b", 2);
|
|
460
|
+
repo.save("a", 1);
|
|
461
|
+
let first: Vec<(String, Json)> = repo.json_entries(render).map(|(id, json)| (id.clone(), json.clone())).collect();
|
|
462
|
+
assert_eq!(first, vec![("a".to_string(), render(&1)), ("b".to_string(), render(&2))]);
|
|
463
|
+
|
|
464
|
+
repo.save("a", 10);
|
|
465
|
+
let second: Vec<Json> = repo.json_entries(render).map(|(_, json)| json.clone()).collect();
|
|
466
|
+
assert_eq!(second, vec![render(&10), render(&2)]);
|
|
467
|
+
}
|
|
468
|
+
}
|