hecks 0.2.0 → 0.3.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 +5 -5
- data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
- data/lib/hecks/adapters/driven/claude_code.rb +127 -0
- data/lib/hecks/adapters/driven/d1.adapter +12 -0
- data/lib/hecks/adapters/driven/d1.rb +403 -0
- data/lib/hecks/adapters/driven/folder.adapter +3 -0
- data/lib/hecks/adapters/driven/folder.rb +199 -0
- data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
- data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
- data/lib/hecks/adapters/driven/governance_authorization.rb +62 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +148 -0
- data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
- data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
- data/lib/hecks/adapters/driven/lambda.adapter +4 -0
- data/lib/hecks/adapters/driven/lambda.rb +131 -0
- data/lib/hecks/adapters/driven/memory.adapter +3 -0
- data/lib/hecks/adapters/driven/memory.rb +103 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
- data/lib/hecks/adapters/driven/postgres/codec.rb +80 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +391 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
- data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
- data/lib/hecks/adapters/driven/prism.adapter +3 -0
- data/lib/hecks/adapters/driven/prism.rb +80 -0
- data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
- data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +285 -0
- data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +14 -0
- data/lib/hecks/adapters/driven.rb +36 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +96 -0
- data/lib/hecks/behaviors/expectations.rb +280 -0
- data/lib/hecks/behaviors/ir.rb +31 -0
- data/lib/hecks/behaviors/rspec.rb +42 -0
- data/lib/hecks/behaviors/runner.rb +96 -0
- data/lib/hecks/behaviors.rb +25 -0
- data/lib/hecks/bluebook/aggregate.rb +108 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
- data/lib/hecks/bluebook/assembly/build.rb +48 -0
- data/lib/hecks/bluebook/assembly/contract.rb +112 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
- data/lib/hecks/bluebook/assembly/marks.rb +227 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +78 -0
- data/lib/hecks/bluebook/attribute.rb +96 -0
- data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
- data/lib/hecks/bluebook/behaviour/command.rb +116 -0
- data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
- data/lib/hecks/bluebook/behaviour/query.rb +10 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
- data/lib/hecks/bluebook/chapter.rb +68 -0
- data/lib/hecks/bluebook/command.rb +124 -0
- data/lib/hecks/bluebook/domain_port.rb +102 -0
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
- data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
- data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
- data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
- data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
- data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
- data/lib/hecks/bluebook/dsl.rb +45 -0
- data/lib/hecks/bluebook/entity.rb +103 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
- data/lib/hecks/bluebook/expression/projection.json +170 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +630 -0
- data/lib/hecks/bluebook/expression.rb +13 -0
- data/lib/hecks/bluebook/hexagon.rb +60 -0
- data/lib/hecks/bluebook/lifecycle.rb +42 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
- data/lib/hecks/bluebook/meta_validator.rb +452 -0
- data/lib/hecks/bluebook/model_check.rb +318 -0
- data/lib/hecks/bluebook/pattern_subset.rb +120 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +86 -0
- data/lib/hecks/bluebook/project_discovery.rb +30 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +82 -0
- data/lib/hecks/bluebook/query.rb +61 -0
- data/lib/hecks/bluebook/read_model.rb +109 -0
- data/lib/hecks/bluebook/reference.rb +74 -0
- data/lib/hecks/bluebook/smoke_test.rb +166 -0
- data/lib/hecks/bluebook/synthesizer.rb +95 -0
- data/lib/hecks/bluebook/translation.rb +92 -0
- data/lib/hecks/bluebook/value_object.rb +58 -0
- data/lib/hecks/bluebook.rb +74 -0
- data/lib/hecks/codemod.rb +342 -0
- data/lib/hecks/construct.rb +71 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
- data/lib/hecks/deploy/oidc.json +18 -0
- data/lib/hecks/doc/reference.rb +389 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +116 -0
- data/lib/hecks/facade/cli_runner.rb +190 -0
- data/lib/hecks/facade/command_request.rb +105 -0
- data/lib/hecks/facade/handle.rb +173 -0
- data/lib/hecks/facade/json_door.rb +154 -0
- data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
- data/lib/hecks/facade/surface/chapter.rb +107 -0
- data/lib/hecks/facade/surface.rb +48 -0
- data/lib/hecks/facade.rb +44 -0
- data/lib/hecks/forms/app.rb +300 -0
- data/lib/hecks/forms/command_form_renderer.rb +113 -0
- data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
- data/lib/hecks/forms/field_renderer.rb +170 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +53 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +132 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +115 -0
- data/lib/hecks/forms/record_table.rb +63 -0
- data/lib/hecks/forms/reference_options.rb +30 -0
- data/lib/hecks/forms/value_object_shape.rb +46 -0
- data/lib/hecks/forms.rb +54 -0
- data/lib/hecks/fqn.rb +94 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
- data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
- data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
- data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
- data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
- data/lib/hecks/framework/oidc.json +39 -0
- data/lib/hecks/framework.rb +90 -0
- data/lib/hecks/freezer.rb +67 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
- data/lib/hecks/fuzzing/properties.rb +1146 -0
- data/lib/hecks/fuzzing/replay.rb +635 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
- data/lib/hecks/fuzzing/value_generator.rb +177 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +291 -0
- data/lib/hecks/grammar/expression.bluebook +417 -0
- data/lib/hecks/grammar/expression_operators.json +1152 -0
- data/lib/hecks/grammar/grammar.hecksagon +20 -0
- data/lib/hecks/grammar/oidc.json +69 -0
- data/lib/hecks/grammar/translation.bluebook +203 -0
- data/lib/hecks/grammar.rb +178 -0
- data/lib/hecks/ir.rb +126 -0
- data/lib/hecks/language/adapter.bluebook +116 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
- data/lib/hecks/language/bluebook/command.bluebook +437 -0
- data/lib/hecks/language/bluebook/entity.bluebook +316 -0
- data/lib/hecks/language/bluebook/policy.bluebook +172 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
- data/lib/hecks/language/bluebook/projection.bluebook +267 -0
- data/lib/hecks/language/bluebook/query.bluebook +245 -0
- data/lib/hecks/language/bluebook/shape.bluebook +292 -0
- data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
- data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
- data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
- data/lib/hecks/language/oidc.json +313 -0
- data/lib/hecks/language/port.bluebook +92 -0
- data/lib/hecks/language/translation/translation.bluebook +110 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
- data/lib/hecks/language/world/wiring.bluebook +62 -0
- data/lib/hecks/language/world/world.bluebook +84 -0
- data/lib/hecks/literal.rb +125 -0
- data/lib/hecks/naming.rb +121 -0
- data/lib/hecks/ports/access_control.port +4 -0
- data/lib/hecks/ports/access_control.rb +62 -0
- data/lib/hecks/ports/agent/answers.rb +104 -0
- data/lib/hecks/ports/agent.port +4 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +4 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +4 -0
- data/lib/hecks/ports/authorization.rb +59 -0
- data/lib/hecks/ports/clock.port +4 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +4 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +4 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +4 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +4 -0
- data/lib/hecks/ports/identity_resolution.rb +40 -0
- data/lib/hecks/ports/loading.port +4 -0
- data/lib/hecks/ports/loading.rb +13 -0
- data/lib/hecks/ports/persistence/append_only.rb +102 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +18 -0
- data/lib/hecks/ports/persistence/lineage.rb +277 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
- data/lib/hecks/ports/persistence.port +4 -0
- data/lib/hecks/ports/persistence.rb +39 -0
- data/lib/hecks/ports/projection.port +4 -0
- data/lib/hecks/ports/projection.rb +76 -0
- data/lib/hecks/ports/query/in_memory.rb +60 -0
- data/lib/hecks/ports/query/ordering.rb +41 -0
- data/lib/hecks/ports/query.rb +36 -0
- data/lib/hecks/ports.rb +27 -0
- data/lib/hecks/projections/diagrams.rb +413 -0
- data/lib/hecks/projections/ir.rb +18 -0
- data/lib/hecks/projections/model/deviations.rb +98 -0
- data/lib/hecks/projections/model.rb +145 -0
- data/lib/hecks/projections/oidc.rb +110 -0
- data/lib/hecks/projections/parser_table.rb +159 -0
- data/lib/hecks/projections/reference.rb +38 -0
- data/lib/hecks/projections/shape.rb +31 -0
- data/lib/hecks/projections/statements.rb +110 -0
- data/lib/hecks/projections/vocabulary.rb +100 -0
- data/lib/hecks/projections.rb +32 -0
- data/lib/hecks/projector/cli_projector.rb +437 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +141 -0
- data/lib/hecks/projector/ir_projector.rb +18 -0
- data/lib/hecks/projector/narrate_projector.rb +243 -0
- data/lib/hecks/projector/target.rb +97 -0
- data/lib/hecks/projector.rb +186 -0
- data/lib/hecks/query_ir.rb +392 -0
- data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
- data/lib/hecks/query_specification/common/comparators.rb +25 -0
- data/lib/hecks/query_specification/common/comparison.rb +174 -0
- data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
- data/lib/hecks/query_specification/common/dsl.rb +58 -0
- data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
- data/lib/hecks/query_specification/common/null_policy.rb +80 -0
- data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
- data/lib/hecks/query_specification/common/options.rb +32 -0
- data/lib/hecks/query_specification/common/order_by.rb +9 -0
- data/lib/hecks/query_specification/common/specification.rb +10 -0
- data/lib/hecks/query_specification/common/where_clause.rb +9 -0
- data/lib/hecks/query_specification/field_path.rb +89 -0
- data/lib/hecks/query_specification/hop_path.rb +130 -0
- data/lib/hecks/query_specification/read_model/specification.rb +18 -0
- data/lib/hecks/query_specification.rb +14 -0
- data/lib/hecks/rendering.rb +48 -0
- data/lib/hecks/router/namespace_installer.rb +157 -0
- data/lib/hecks/router.rb +70 -0
- data/lib/hecks/runtime/caller.rb +50 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
- data/lib/hecks/runtime/command_interpreter.rb +413 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +181 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +231 -0
- data/lib/hecks/runtime/dispatcher.rb +286 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +228 -0
- data/lib/hecks/runtime/era_check.rb +136 -0
- data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/runtime/era_guard.rb +193 -0
- data/lib/hecks/runtime/era_tamper.rb +61 -0
- data/lib/hecks/runtime/errors.rb +100 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +128 -0
- data/lib/hecks/runtime/instance.rb +120 -0
- data/lib/hecks/runtime/interpreting.rb +69 -0
- data/lib/hecks/runtime/loader.rb +143 -0
- data/lib/hecks/runtime/policy_interpreter.rb +366 -0
- data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
- data/lib/hecks/runtime/query_interpreter.rb +254 -0
- data/lib/hecks/runtime/reaction_invocation.rb +245 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
- data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
- data/lib/hecks/runtime/reference_hop.rb +99 -0
- data/lib/hecks/runtime/refusal_wording.rb +124 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
- data/lib/hecks/runtime/registry/verification.rb +158 -0
- data/lib/hecks/runtime/registry.rb +218 -0
- data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
- data/lib/hecks/runtime/routing.rb +96 -0
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
- data/lib/hecks/runtime/saga_interpreter.rb +301 -0
- data/lib/hecks/runtime/storage_shape.rb +120 -0
- data/lib/hecks/runtime/tenant_check.rb +84 -0
- data/lib/hecks/runtime/tenant_scope.rb +55 -0
- data/lib/hecks/runtime/value/admission.rb +112 -0
- data/lib/hecks/runtime/value/coercion.rb +500 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +95 -0
- data/lib/hecks/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/translation/audit/layer_one.rb +37 -0
- data/lib/hecks/translation/audit/layer_two.rb +77 -0
- data/lib/hecks/translation/audit/unfed_report.rb +42 -0
- data/lib/hecks/translation/audit.rb +70 -0
- data/lib/hecks/translation/reattest.rb +72 -0
- data/lib/hecks/translation/rule_compiler.rb +120 -0
- data/lib/hecks/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/translation/scaffold.rb +30 -0
- data/lib/hecks/translation.rb +11 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +213 -0
- data/lib/hecks.rb +118 -6
- metadata +401 -222
- data/bin/hecks +0 -7
- data/bin/hecks-package +0 -65
- data/bin/hecks_console +0 -12
- data/bin/hecks_serverless +0 -6
- data/lib/cli/build.rb +0 -14
- data/lib/cli/command_runner.rb +0 -28
- data/lib/cli/console.rb +0 -10
- data/lib/cli/generate.rb +0 -37
- data/lib/cli/hecks-cli.rb +0 -27
- data/lib/cli/test.rb +0 -57
- data/lib/console/commands.rb +0 -8
- data/lib/console/hecks-console.rb +0 -1
- data/lib/packager/README.md +0 -0
- data/lib/packager/app_runner.rb +0 -21
- data/lib/packager/args.rb +0 -26
- data/lib/packager/compatibility/fixnum.rb +0 -6
- data/lib/packager/hecks.rb +0 -39
- data/lib/packager/query_runner.rb +0 -21
- data/lib/packager/resources/Dockerfile +0 -11
- data/lib/packager/resources/app_binary +0 -7
- data/lib/packager/resources/bundle_config +0 -3
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
- data/lib/serverless/Domain +0 -32
- data/lib/serverless/cli.rb +0 -75
- data/lib/serverless/resources/command_name.js +0 -5
- data/lib/serverless/resources/environment.js +0 -7
- data/lib/serverless/resources/handler.js.tt +0 -28
- data/lib/serverless/resources/run_binary.js +0 -22
- data/lib/serverless/resources/serverless.yml +0 -20
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
# A VENDORED, EXTERNAL BLUEBOOK — same shape as Framework (framework.rb),
|
|
3
|
+
# for members that don't ship inside hecks's own lib/ at all: a
|
|
4
|
+
# separate, independently-versioned package
|
|
5
|
+
# (github.com/chrisyoung/embryonaut_bluebooks) that a consuming project
|
|
6
|
+
# vendors into its OWN checkout, the same way a project already vendors
|
|
7
|
+
# hecks itself (bin/vendor_hecks, vendor/hecks/).
|
|
8
|
+
#
|
|
9
|
+
# RECOVERED, NOT REBUILT — this module and its `uses_embryonaut_bluebook`
|
|
10
|
+
# DSL word (hecksagon_builder.rb) were built on a prior commit of this
|
|
11
|
+
# repo (933d1dd), vendored out to a real consumer (lifeadelics/domain,
|
|
12
|
+
# for embryonaut_bluebooks/payments), and then lost from this repo's own
|
|
13
|
+
# reachable history — a hard reset or rebase left no branch containing
|
|
14
|
+
# that commit. The lifeadelics vendor snapshot (a `git archive` of that
|
|
15
|
+
# commit, committed into their repo) was the only surviving copy; this
|
|
16
|
+
# file is ported forward from it, checked against current `main`'s own
|
|
17
|
+
# conventions rather than copied wholesale, since the two trees had
|
|
18
|
+
# otherwise diverged for weeks in both directions.
|
|
19
|
+
#
|
|
20
|
+
# RESOLVED FROM THE CONSUMING REGISTRY'S OWN ROOT, not this gem's
|
|
21
|
+
# __dir__ — Framework::ROOT can be a fixed, `__dir__`-relative constant
|
|
22
|
+
# because framework members ship inside this gem; an embryonaut bluebook
|
|
23
|
+
# ships inside the CONSUMER's own checkout instead, at
|
|
24
|
+
# `<registry.root>/vendor/embryonaut_bluebooks/<name>/bluebook/`. There
|
|
25
|
+
# is no fixed answer until a registry (and its root) actually exists, so
|
|
26
|
+
# this resolves lazily, per call — the same reason `uses_framework`
|
|
27
|
+
# itself only runs at hecksagon-build time, when a real registry is
|
|
28
|
+
# current.
|
|
29
|
+
#
|
|
30
|
+
# EVERY `.bluebook` FILE IN THE PACKAGE, SORTED — not just one. Unlike a
|
|
31
|
+
# framework member (one file, named by its own stem), a vendored package
|
|
32
|
+
# can span several bluebook files that reopen the SAME `Hecks.bluebook`
|
|
33
|
+
# (embryonaut_bluebooks/payments/bluebook/{payment,payments,policies}
|
|
34
|
+
# .bluebook all reopen "Payments"). Load order matters — policies
|
|
35
|
+
# .bluebook names `Payment::Succeed` and needs the aggregate already
|
|
36
|
+
# built — and a plain alphabetical sort already gives the right order:
|
|
37
|
+
# payment < payments < policies, the same reason that package's own
|
|
38
|
+
# files are named to fall in that order in the first place.
|
|
39
|
+
#
|
|
40
|
+
# ONLY THE BLUEBOOK FILES — same restriction Framework draws, same
|
|
41
|
+
# reason: a `.hecksagon`/`.port`/`.adapter` is a WIRING decision
|
|
42
|
+
# (persistence, which processor adapter is bound) that belongs to
|
|
43
|
+
# whoever is deploying, never baked into the vendored package itself.
|
|
44
|
+
# embryonaut_bluebooks/payments ships its own mock `.hecksagon` for its
|
|
45
|
+
# own spec suite; a consumer declares its OWN separate
|
|
46
|
+
# `Hecks.hecksagon "Payments" do ... end` to bind real storage/adapters
|
|
47
|
+
# — see Framework's own comment for the fuller reasoning, identical here.
|
|
48
|
+
#
|
|
49
|
+
# IDEMPOTENT THE SAME WAY Framework.load! IS — checked against the
|
|
50
|
+
# bluebook this package actually declares (`Naming.pascal("payments")`
|
|
51
|
+
# => "Payments"), not a separate ledger. A vendored package's directory
|
|
52
|
+
# name and its declared `Hecks.bluebook` name are the one convention
|
|
53
|
+
# this reuses from Framework rather than reinventing.
|
|
54
|
+
module EmbryonautBluebook
|
|
55
|
+
def self.load!(name, registry: Hecks.current_registry)
|
|
56
|
+
unless registry&.root
|
|
57
|
+
raise Runtime::WiringError,
|
|
58
|
+
"uses_embryonaut_bluebook(#{name.inspect}) needs a registry with a root to vendor from"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
return if registry.bluebook(Naming.pascal(name.to_s))
|
|
62
|
+
|
|
63
|
+
dir = File.join(registry.root, "vendor", "embryonaut_bluebooks", name.to_s, "bluebook")
|
|
64
|
+
files = Dir.glob(File.join(dir, "*.bluebook"))
|
|
65
|
+
|
|
66
|
+
if files.empty?
|
|
67
|
+
raise Runtime::WiringError,
|
|
68
|
+
"no vendored embryonaut bluebook named #{name.inspect} at #{dir} — " \
|
|
69
|
+
"run bin/vendor_embryonaut_bluebooks #{name}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
files.each { |file| Kernel.load(file) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
require_relative "../runtime/errors"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Facade
|
|
5
|
+
# THE CLI DOOR — WHERE Facade MEETS A CALLER HOLDING FLAT STRINGS.
|
|
6
|
+
#
|
|
7
|
+
# `JsonDoor` beside this one translates for a caller holding parsed JSON:
|
|
8
|
+
# String keys, already-nested objects, real Integers. A command line has
|
|
9
|
+
# neither of those. It has `sequence.value=99` — one flat string, with the
|
|
10
|
+
# nesting spelled as a path and the type not spelled at all.
|
|
11
|
+
#
|
|
12
|
+
# So this does the two things that turns into: rebuild the nesting, and
|
|
13
|
+
# give every leaf the type the chapter declared for it.
|
|
14
|
+
#
|
|
15
|
+
# THE TYPE COMES FROM THE PROJECTION, NEVER FROM THE VALUE. A door that
|
|
16
|
+
# guessed — "99 looks like a number" — would send the Integer 99 for a
|
|
17
|
+
# version string of "99", and be wrong in a way nothing downstream could
|
|
18
|
+
# detect, because both are perfectly good arguments. `Projector::CliProjector`
|
|
19
|
+
# already read the declared field type out of the value object; this only
|
|
20
|
+
# applies it.
|
|
21
|
+
module CliDoor
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# `["reference.value=BUG#1", "sequence.value=99"]` against a projected
|
|
25
|
+
# verb spec -> `{ reference: { value: "BUG#1" }, sequence: { value: 99 } }`
|
|
26
|
+
def arguments(spec, pairs)
|
|
27
|
+
# Legacy options are accepted but not printed in help. This lets an
|
|
28
|
+
# existing id=... aggregate invocation cross the new receiver boundary
|
|
29
|
+
# while the projected surface teaches to=... exclusively.
|
|
30
|
+
options = (spec[:arguments] + Array(spec[:legacy_arguments])).to_h do |argument|
|
|
31
|
+
[argument[:path], argument]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
pairs.each_with_object({}) do |pair, args|
|
|
35
|
+
path, value = split(pair)
|
|
36
|
+
argument = options[path] || options[expand(path, options)] ||
|
|
37
|
+
raise(Runtime::NotFound, unknown(path, options.keys))
|
|
38
|
+
|
|
39
|
+
full = options.key?(path) ? path : expand(path, options)
|
|
40
|
+
next append(args, full.split("."), cast(value, argument[:type])) if argument[:list]
|
|
41
|
+
|
|
42
|
+
bury(args, full.split("."), cast(value, argument[:type]))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def split(pair)
|
|
47
|
+
name, value = pair.split("=", 2)
|
|
48
|
+
raise Runtime::NotFound, "#{pair.inspect} is not name=value" if value.nil?
|
|
49
|
+
|
|
50
|
+
[name, value]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# THE SHORT FORM, FOR THE COMMON CASE. Almost every value object in this
|
|
54
|
+
# corpus has exactly one field, so `reference=BUG#1` is unambiguous and
|
|
55
|
+
# is what anybody types. Expanded only when precisely one option starts
|
|
56
|
+
# with that prefix — two would be a guess, and a guess about which field
|
|
57
|
+
# a caller meant is worse than asking them to say.
|
|
58
|
+
def expand(path, options)
|
|
59
|
+
candidates = options.keys.select { |key| key.start_with?("#{path}.") }
|
|
60
|
+
candidates.length == 1 ? candidates.first : path
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def cast(value, type)
|
|
64
|
+
case type
|
|
65
|
+
when "Integer" then Integer(value)
|
|
66
|
+
when "Float" then Float(value)
|
|
67
|
+
when "Boolean" then %w[true yes 1].include?(value.downcase)
|
|
68
|
+
else value
|
|
69
|
+
end
|
|
70
|
+
rescue ArgumentError
|
|
71
|
+
raise Runtime::TypeMismatch, "#{value.inspect} is not #{type} — the chapter declares this field as #{type}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# A LIST GROWS RATHER THAN OVERWRITES, and getting this wrong is silent.
|
|
75
|
+
#
|
|
76
|
+
# `tags.value=framework tags.value=model-checker` used to reach `bury`
|
|
77
|
+
# twice and store the second one alone — no refusal, no warning, one tag
|
|
78
|
+
# simply gone. That is the failure the interview named first: not the
|
|
79
|
+
# loud kind, the kind where a value is forgotten and the caller has no
|
|
80
|
+
# way to notice.
|
|
81
|
+
#
|
|
82
|
+
# A LIST OF ONE IS STILL A LIST. `tags.value=flaky` produces
|
|
83
|
+
# `[{ value: "flaky" }]`, not `{ value: "flaky" }`, because the chapter
|
|
84
|
+
# declared a collection and a caller who sent one element did not
|
|
85
|
+
# thereby declare a different shape. The old behaviour handed a bare
|
|
86
|
+
# object to a `list_of` attribute, and everything downstream that walks
|
|
87
|
+
# it — a query's `contains`, a projection, the Postgres adapter's own
|
|
88
|
+
# array handling — is entitled to assume it can iterate.
|
|
89
|
+
#
|
|
90
|
+
# MULTI-FIELD ELEMENTS ARE NOT SUPPORTED HERE, deliberately. A flat
|
|
91
|
+
# command line has no way to say which `a.x=` goes with which `a.y=`,
|
|
92
|
+
# and inventing an index syntax would be a language nobody asked for.
|
|
93
|
+
# Every list in this corpus is a list of single-field value objects; a
|
|
94
|
+
# richer one is a job for `JsonDoor`, which has real nesting.
|
|
95
|
+
def append(hash, path, value)
|
|
96
|
+
*branches, leaf = path.map(&:to_sym)
|
|
97
|
+
holder = branches[0..-2].reduce(hash) { |node, key| node[key] ||= {} }
|
|
98
|
+
list = holder[branches.last] ||= []
|
|
99
|
+
|
|
100
|
+
list << { leaf => value }
|
|
101
|
+
hash
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def bury(hash, path, value)
|
|
105
|
+
*branches, leaf = path.map(&:to_sym)
|
|
106
|
+
target = branches.reduce(hash) { |node, key| node[key] ||= {} }
|
|
107
|
+
target[leaf] = value
|
|
108
|
+
hash
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def unknown(path, known)
|
|
112
|
+
"no argument #{path.inspect} — this verb takes #{known.sort.join(', ')}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "cli_door"
|
|
3
|
+
require_relative "command_request"
|
|
4
|
+
require_relative "json_door"
|
|
5
|
+
require_relative "../projector"
|
|
6
|
+
require_relative "../ports/clock"
|
|
7
|
+
|
|
8
|
+
module Hecks
|
|
9
|
+
module Facade
|
|
10
|
+
# THE RUNNER BEHIND A PROJECTED CLI.
|
|
11
|
+
#
|
|
12
|
+
# `Projector::CliProjector` answers what a domain's command line LOOKS
|
|
13
|
+
# like; this is the twenty lines that parse against it and dispatch. It
|
|
14
|
+
# lives in `lib/` rather than in a `bin/` because more than one front door
|
|
15
|
+
# wants it — `bin/run` for whichever domain you are standing in, `bin/qc`
|
|
16
|
+
# pinned to the QA ledger — and a second copy of the parse-and-dispatch
|
|
17
|
+
# would be the exact duplication the projection exists to avoid.
|
|
18
|
+
#
|
|
19
|
+
# NO IO. It answers `[text, status]` and never prints or exits, so a spec
|
|
20
|
+
# can call it without capturing streams or trapping SystemExit. The `bin/`
|
|
21
|
+
# scripts do the printing, the same division `Router` and `JsonDoor`
|
|
22
|
+
# already keep against HTTP.
|
|
23
|
+
module CliRunner
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# `[text, status]` — status 0 answered, 1 refused or misused.
|
|
27
|
+
def call(runtime:, argv:, program: "bin/run")
|
|
28
|
+
bluebook = runtime.registry.bluebooks.values.first
|
|
29
|
+
cli = Projector.call(:cli, bluebook: bluebook, options: { program: program })
|
|
30
|
+
|
|
31
|
+
name = argv.first
|
|
32
|
+
return [cli[:usage], 0] if name.nil? || %w[--help -h help].include?(name)
|
|
33
|
+
|
|
34
|
+
# `ask` PUTS A QUESTION IN ITS OWN NAMESPACE — a chapter may declare a
|
|
35
|
+
# command and a query of one name, and banking does.
|
|
36
|
+
asking = name == "ask"
|
|
37
|
+
argv = argv[1..] if asking
|
|
38
|
+
name = argv.first
|
|
39
|
+
return [cli[:usage], 1] if name.nil?
|
|
40
|
+
|
|
41
|
+
# RESOLVED THROUGH THE ALIAS MAP, so `pizzas create_pizza` and
|
|
42
|
+
# `pizzas order.create_pizza` reach the same verb — the aggregate is
|
|
43
|
+
# worth typing only when two of them declare the same word.
|
|
44
|
+
pool = asking ? cli[:questions] : cli[:verbs]
|
|
45
|
+
key = cli[:names][asking ? :question : :command][name]
|
|
46
|
+
spec = pool[key]
|
|
47
|
+
return [unknown(cli, name, asking, program), 1] unless spec
|
|
48
|
+
|
|
49
|
+
rest = argv[1..]
|
|
50
|
+
if rest.include?("--help")
|
|
51
|
+
help = Projector.call(:cli, bluebook: bluebook,
|
|
52
|
+
options: { program: program, verb: name, ask: asking })[:usage]
|
|
53
|
+
return [help, 0]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
dispatch(runtime, spec, name, rest, program, asking)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def dispatch(runtime, spec, name, rest, program, asking)
|
|
60
|
+
args = stamp_time(runtime, spec, CliDoor.arguments(spec, rest))
|
|
61
|
+
|
|
62
|
+
if spec[:kind] == :query
|
|
63
|
+
rows = runtime.query(spec[:verb], **args)
|
|
64
|
+
return [JSON.pretty_generate(rows.map { |row| JsonDoor.materialize(row) }), 0]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# THE ANSWER IS SCOPED TO WHAT WAS ASKED. `bin/run`'s step-list form
|
|
68
|
+
# reports the whole store because a corpus run is judged on all of it;
|
|
69
|
+
# somebody who issued one verb wants that verb's outcome, and against a
|
|
70
|
+
# Postgres-backed domain the full dump is every record there has been.
|
|
71
|
+
request = CommandRequest.normalize(args, receiver: spec[:receiver],
|
|
72
|
+
legacy_receiver: spec[:legacy_receiver])
|
|
73
|
+
handle = runtime.dispatch(spec[:verb], **request)
|
|
74
|
+
return [JSON.pretty_generate(answered(handle)), 0] if handle.state.nil?
|
|
75
|
+
|
|
76
|
+
[JSON.pretty_generate(id: handle.id,
|
|
77
|
+
state: JsonDoor.materialize(handle.state),
|
|
78
|
+
events: handle.events.map(&:name)), 0]
|
|
79
|
+
rescue Runtime::NotFound, Runtime::TypeMismatch => e
|
|
80
|
+
# A BAD ARGUMENT AND A MISSING RECORD BOTH LAND HERE, and both want the
|
|
81
|
+
# same next step: read what the verb actually takes.
|
|
82
|
+
["#{e.message}\n\n #{program} #{asking ? 'ask ' : ''}#{name} --help", 1]
|
|
83
|
+
rescue *Runtime::DOMAIN_REFUSALS => e
|
|
84
|
+
# THE REFUSAL IS THE PRODUCT — the chapter's own sentence, verbatim.
|
|
85
|
+
[e.message, 1]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# THE CLOCK, FILLED IN AT THE DOOR.
|
|
89
|
+
#
|
|
90
|
+
# A staleness rule needs the time, and the sublanguage cannot ask for it
|
|
91
|
+
# — a `given` that read the clock would judge the same record differently
|
|
92
|
+
# on two runs, and every replay, audit and fuzz oracle here assumes it
|
|
93
|
+
# does not. So `now` stays an ARGUMENT the predicate merely reads, and
|
|
94
|
+
# the question becomes who types it. Before this, the caller did:
|
|
95
|
+
#
|
|
96
|
+
# qa/quality_control target.claim id=QC held_by.value=me \
|
|
97
|
+
# now.value=$(date +%s) window.value=900
|
|
98
|
+
#
|
|
99
|
+
# which is a shell incantation in front of every claim, and one an agent
|
|
100
|
+
# gets wrong by pasting a stale number.
|
|
101
|
+
#
|
|
102
|
+
# AT THE DOOR, NOT IN THE RUNTIME, and the distinction is load-bearing.
|
|
103
|
+
# `Ports::IdentityGeneration` reasons the same question through for a
|
|
104
|
+
# minted uuid and lands on "the value is baked into the caller's args at
|
|
105
|
+
# the first live dispatch". A clock consulted INSIDE the interpreter
|
|
106
|
+
# would not have that property — a recorded corpus step replayed
|
|
107
|
+
# tomorrow would quietly get tomorrow's time, and the fuzzer's oracle and
|
|
108
|
+
# the adapter-agreement gate both compare runs of exactly that shape. Here
|
|
109
|
+
# it fills only what a person or an agent is typing, and `runtime.dispatch`
|
|
110
|
+
# is left alone.
|
|
111
|
+
#
|
|
112
|
+
# AN EXPLICIT VALUE ALWAYS WINS, so a spec or a caller reproducing a
|
|
113
|
+
# moment says so and is believed. This only supplies what was omitted.
|
|
114
|
+
#
|
|
115
|
+
# BY NAME, WHICH IS THE ONE UNCOMFORTABLE PART. `now` is a plausible
|
|
116
|
+
# domain word and nothing declares that it means the clock. It is
|
|
117
|
+
# tolerable because this is a convenience layer rather than semantics —
|
|
118
|
+
# the verb's own help says the argument exists, dispatch is unchanged,
|
|
119
|
+
# and passing it explicitly is always available. The honest version is a
|
|
120
|
+
# declaration in the chapter (`attribute :now, Instant, from: :clock`),
|
|
121
|
+
# which is a language change: DSL, IR, the self-hosted grammar and its
|
|
122
|
+
# goldens. Worth doing; not worth smuggling in here.
|
|
123
|
+
def stamp_time(runtime, spec, args)
|
|
124
|
+
return args unless spec[:arguments].any? { |argument| argument[:path].start_with?("now.") }
|
|
125
|
+
return args if args.key?(:now)
|
|
126
|
+
|
|
127
|
+
args.merge(now: { value: Ports::Clock.now(runtime.registry) })
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# A PORT OPERATION HAS NO STATE, AND ITS PAYLOAD IS THE ENTIRE POINT.
|
|
131
|
+
#
|
|
132
|
+
# A command answers with the record it changed, so naming the events is
|
|
133
|
+
# enough — the interesting part is in `state`. A port operation changes
|
|
134
|
+
# no record: it asked something outside and came back with what was
|
|
135
|
+
# said, and that lives ONLY in the event payload. Reporting names alone
|
|
136
|
+
# would print `SpecsCompleted` and drop the spec output on the floor.
|
|
137
|
+
#
|
|
138
|
+
# This is what makes a projected CLI usable as somebody's only door. An
|
|
139
|
+
# agent that may not shell out cannot run `rspec` and read the terminal;
|
|
140
|
+
# it asks the port and reads the answer, and if the answer is a bare
|
|
141
|
+
# event name then the door leads nowhere and it needs a shell after all.
|
|
142
|
+
#
|
|
143
|
+
# BOTH ENDINGS COME BACK THE SAME WAY, and the status stays 0 for both.
|
|
144
|
+
# A refusal here is not a misuse — `IssueStillOpen` and `SuiteFailed`
|
|
145
|
+
# are answers the caller asked for, correctly delivered. Exit 1 is for
|
|
146
|
+
# "you typed something wrong", and conflating the two would have a
|
|
147
|
+
# scripted agent treat a healthy no as a broken call.
|
|
148
|
+
def answered(handle)
|
|
149
|
+
# THE ID COMES OFF THE EVENT, because a port operation hydrates no
|
|
150
|
+
# instance and the handle's own `id` is nil by design. The event knows
|
|
151
|
+
# which record was asked about — it was stamped with it — and printing
|
|
152
|
+
# `null` beside a payload that plainly says `SW-TOOL` would read as a
|
|
153
|
+
# bug in something.
|
|
154
|
+
{ id: handle.id || handle.events.first&.id,
|
|
155
|
+
events: handle.events.map do |event|
|
|
156
|
+
{ name: event.name, payload: JsonDoor.materialize(event.payload) }
|
|
157
|
+
end }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# A NEAR MISS IS WORTH MORE THAN A LIST. Somebody who typed
|
|
161
|
+
# `bug.discovr` wants one line, not eighty-seven of them.
|
|
162
|
+
#
|
|
163
|
+
# RANKED BY SHARED PREFIX, not by substring. Substring was the first
|
|
164
|
+
# attempt and it finds nothing for the commonest typo of all — a dropped
|
|
165
|
+
# letter, `order.create_piza`, which is a substring of nothing. Prefix
|
|
166
|
+
# length survives an error anywhere after it, which is where errors are.
|
|
167
|
+
def unknown(cli, name, asking, program)
|
|
168
|
+
# BOTH SPELLINGS ARE CANDIDATES. A caller who typed the qualified
|
|
169
|
+
# form with a typo — `order.create_piza` — shares no prefix with the
|
|
170
|
+
# short name `create_pizza`, so pooling only one of them suggests
|
|
171
|
+
# nothing for half the mistakes anybody makes.
|
|
172
|
+
pool = cli[:names][asking ? :question : :command].keys
|
|
173
|
+
near = pool.map { |candidate| [shared_prefix(candidate, name), candidate] }
|
|
174
|
+
.select { |shared, _| shared >= [name.length / 2, 3].max }
|
|
175
|
+
.sort_by { |shared, candidate| [-shared, candidate] }
|
|
176
|
+
.map(&:last)
|
|
177
|
+
|
|
178
|
+
lines = ["no such #{asking ? 'question' : 'verb'}: #{name}"]
|
|
179
|
+
lines += ["", "did you mean:", *near.first(5).map { |candidate| " #{candidate}" }] unless near.empty?
|
|
180
|
+
lines += ["", " #{program}#{asking ? ' ask' : ''} for the full list"]
|
|
181
|
+
lines.join("\n")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def shared_prefix(one, other)
|
|
185
|
+
length = [one.length, other.length].min
|
|
186
|
+
(0...length).find { |index| one[index] != other[index] } || length
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require_relative "../runtime/errors"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Facade
|
|
5
|
+
# Turns an external command request into the dispatcher's receiver/payload
|
|
6
|
+
# envelope. Human-facing doors may accept their old flat id spelling at
|
|
7
|
+
# the edge, but every call leaving this boundary has one shape:
|
|
8
|
+
#
|
|
9
|
+
# aggregate command: { to: "record-id", with: { declared: "facts" } }
|
|
10
|
+
# entity command: { to: { aggregate: "...", entity: "..." },
|
|
11
|
+
# with: { declared: "facts" } }
|
|
12
|
+
#
|
|
13
|
+
# The command interpreter remains the authority on which facts are
|
|
14
|
+
# declared. This helper only prevents routing fields from leaking into the
|
|
15
|
+
# fact payload and gives every external door the same wire contract.
|
|
16
|
+
module CommandRequest
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def normalize(input, receiver:, legacy_receiver: nil)
|
|
20
|
+
request = symbolize(input)
|
|
21
|
+
unless request.is_a?(Hash)
|
|
22
|
+
raise Runtime::TypeMismatch, "a command request must be a hash"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
route, facts = split(request, receiver: receiver, legacy_receiver: legacy_receiver)
|
|
26
|
+
validate_route!(route, receiver)
|
|
27
|
+
|
|
28
|
+
envelope = { with: facts }
|
|
29
|
+
envelope[:to] = route if receiver
|
|
30
|
+
envelope
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def split(request, receiver:, legacy_receiver:)
|
|
34
|
+
if request.key?(:with)
|
|
35
|
+
loose = request.keys - [:to, :with]
|
|
36
|
+
unless loose.empty?
|
|
37
|
+
raise Runtime::TypeMismatch,
|
|
38
|
+
"an explicit command envelope takes routing in to: and facts in with:, not loose #{loose.sort.join(', ')}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
facts = request[:with]
|
|
42
|
+
raise Runtime::TypeMismatch, "with: must be a hash of command facts" unless facts.is_a?(Hash)
|
|
43
|
+
|
|
44
|
+
return [request[:to], facts]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
flat = request.dup
|
|
48
|
+
route = flat.delete(:to)
|
|
49
|
+
route = take_legacy_route(flat, receiver, legacy_receiver) if route.nil?
|
|
50
|
+
[route, flat]
|
|
51
|
+
end
|
|
52
|
+
private_class_method :split
|
|
53
|
+
|
|
54
|
+
def take_legacy_route(flat, receiver, legacy_receiver)
|
|
55
|
+
return unless legacy_receiver
|
|
56
|
+
|
|
57
|
+
if receiver == :aggregate
|
|
58
|
+
return flat.delete(legacy_receiver.to_sym)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
return unless receiver == :entity && legacy_receiver.is_a?(Hash)
|
|
62
|
+
|
|
63
|
+
aggregate_key = legacy_receiver.fetch(:aggregate).to_sym
|
|
64
|
+
entity_key = legacy_receiver.fetch(:entity).to_sym
|
|
65
|
+
return unless flat.key?(aggregate_key) || flat.key?(entity_key)
|
|
66
|
+
|
|
67
|
+
{ aggregate: flat.delete(aggregate_key), entity: flat.delete(entity_key) }
|
|
68
|
+
end
|
|
69
|
+
private_class_method :take_legacy_route
|
|
70
|
+
|
|
71
|
+
def validate_route!(route, receiver)
|
|
72
|
+
case receiver
|
|
73
|
+
when nil
|
|
74
|
+
raise Runtime::TypeMismatch, "this command does not take a receiver in to:" unless route.nil?
|
|
75
|
+
when :aggregate
|
|
76
|
+
if route.nil? || route.to_s.empty? || route.is_a?(Hash)
|
|
77
|
+
raise Runtime::TypeMismatch, "to: must name the receiving aggregate identity"
|
|
78
|
+
end
|
|
79
|
+
when :entity
|
|
80
|
+
unless route.is_a?(Hash)
|
|
81
|
+
raise Runtime::TypeMismatch, "to: for an entity command must contain aggregate: and entity:"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
extra = route.keys - [:aggregate, :entity]
|
|
85
|
+
missing = [:aggregate, :entity].select { |key| route[key].nil? || route[key].to_s.empty? }
|
|
86
|
+
unless extra.empty? && missing.empty?
|
|
87
|
+
raise Runtime::TypeMismatch, "to: for an entity command must contain only aggregate: and entity:"
|
|
88
|
+
end
|
|
89
|
+
else
|
|
90
|
+
raise ArgumentError, "unknown receiver kind #{receiver.inspect}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
private_class_method :validate_route!
|
|
94
|
+
|
|
95
|
+
def symbolize(value)
|
|
96
|
+
case value
|
|
97
|
+
when Hash then value.to_h { |key, nested| [key.to_sym, symbolize(nested)] }
|
|
98
|
+
when Array then value.map { |nested| symbolize(nested) }
|
|
99
|
+
else value
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
private_class_method :symbolize
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
require_relative "../naming"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Facade
|
|
5
|
+
# ONE record in hand — the object `Pizza.create_pizza(...)` and
|
|
6
|
+
# `Pizza.find(id)` give back.
|
|
7
|
+
#
|
|
8
|
+
# ONE SHARED CLASS, not one minted per aggregate. The old door subclassed
|
|
9
|
+
# `Hecks::Aggregate` per head and defined a reader per field ; this
|
|
10
|
+
# wraps the same `Runtime::Instance` state hash and answers readers and
|
|
11
|
+
# verbs through `method_missing`, closing over the dispatcher and the
|
|
12
|
+
# aggregate's IR — so a boot mints no classes at all, and two boots in
|
|
13
|
+
# one process each hand out handles bound to their own dispatcher.
|
|
14
|
+
#
|
|
15
|
+
# A non-creating verb is a method returning self, so commands chain :
|
|
16
|
+
#
|
|
17
|
+
# Pizza.create_pizza(...).add_topping(...).purchase(...)
|
|
18
|
+
class Handle
|
|
19
|
+
attr_reader :id
|
|
20
|
+
|
|
21
|
+
def initialize(dispatcher:, domain:, ir:, instance:)
|
|
22
|
+
@dispatcher = dispatcher
|
|
23
|
+
@domain = domain
|
|
24
|
+
@ir = ir
|
|
25
|
+
@id = instance.id
|
|
26
|
+
@state = instance.state
|
|
27
|
+
define_reference_accessors
|
|
28
|
+
define_verb_methods
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def [](key) = @state[key.to_sym]
|
|
32
|
+
|
|
33
|
+
# `id: @id` LAST, not first — an aggregate is free to declare its own
|
|
34
|
+
# attribute literally named `id` (BurningManPrep's `Item`, `attribute
|
|
35
|
+
# :id, ItemId`, is real corpus now: `identified_by :id` reads
|
|
36
|
+
# THAT attribute for identity). When it does, `@state[:id]` holds the
|
|
37
|
+
# full wrapped value object, not the bare identity string — merging
|
|
38
|
+
# `@state` on top of `{ id: @id }` let that wrapped VO silently
|
|
39
|
+
# clobber the correct bare `@id`, so every caller of `to_h` (the JSON
|
|
40
|
+
# door's own `/api/:coll` listing, in particular) got an object where
|
|
41
|
+
# a plain identity string belonged. `@id` merged LAST always wins,
|
|
42
|
+
# so `to_h[:id]` is always the true bare identity, regardless of
|
|
43
|
+
# whether the aggregate also happens to declare a same-named field.
|
|
44
|
+
def to_h = @state.merge(id: @id)
|
|
45
|
+
|
|
46
|
+
def fqn = "#{@domain}::#{@ir.hecks_name}"
|
|
47
|
+
|
|
48
|
+
def events
|
|
49
|
+
@dispatcher.events.select { |event| event.aggregate == fqn && event.id == @id }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def reload
|
|
53
|
+
stored = repository.find(@id)
|
|
54
|
+
@state = stored.state if stored
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Equality is (WHICH AGGREGATE, WHICH ID) — two handles to the same record
|
|
59
|
+
# are the same record, and a Pizza never equals an Account that happens to
|
|
60
|
+
# share an id. The old door said this with `other.is_a?(self.class)`,
|
|
61
|
+
# leaning on one class per aggregate ; the fqn says it in data.
|
|
62
|
+
def ==(other) = other.is_a?(Handle) && other.fqn == fqn && other.id == @id
|
|
63
|
+
alias eql? ==
|
|
64
|
+
def hash = [Handle, fqn, @id].hash
|
|
65
|
+
|
|
66
|
+
def inspect
|
|
67
|
+
fields = @state.map { |key, value| "#{key}=#{value.inspect}" }.join(" ")
|
|
68
|
+
"#<#{@ir.hecks_name} #{@id} #{fields}>"
|
|
69
|
+
end
|
|
70
|
+
alias to_s inspect
|
|
71
|
+
|
|
72
|
+
# A declared field not yet written arrives here too (nil, the way a
|
|
73
|
+
# defined reader answered). Verbs are NOT handled here — see
|
|
74
|
+
# `define_verb_methods` for why.
|
|
75
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
76
|
+
return @state[name] if @state.key?(name) || reader?(name)
|
|
77
|
+
|
|
78
|
+
super
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def respond_to_missing?(name, include_private = false)
|
|
82
|
+
@state.key?(name) || reader?(name) || super
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def repository = @dispatcher.registry.repository(@domain, @ir)
|
|
88
|
+
|
|
89
|
+
def reader?(name)
|
|
90
|
+
!@ir.attribute(name).nil? || @ir.lifecycle&.field&.to_sym == name
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# NON-CREATING VERBS ARE DEFINED, NOT DISPATCHED THROUGH method_missing.
|
|
94
|
+
#
|
|
95
|
+
# method_missing only runs once Ruby finds no REAL method already
|
|
96
|
+
# answering the name — and every object already answers `freeze` and
|
|
97
|
+
# `send` (Kernel/Object), among others. A verb whose snake-cased name
|
|
98
|
+
# collided with one of those — `Account::Freeze` -> `freeze`,
|
|
99
|
+
# `ExternalTransfer::Send` -> `send` in the banking corpus, both real —
|
|
100
|
+
# used to silently run the Kernel method instead of dispatching: no
|
|
101
|
+
# error, no refusal, the call just did the wrong thing. Defining a
|
|
102
|
+
# real singleton method per verb closed that; the `!` suffix (every
|
|
103
|
+
# command, door and Handle alike) closes it a second, permanent way —
|
|
104
|
+
# `freeze!`/`send!` name nothing Kernel/Object already answers to,
|
|
105
|
+
# so this exact class of collision cannot recur no matter what a
|
|
106
|
+
# future domain names a command.
|
|
107
|
+
def define_verb_methods
|
|
108
|
+
@ir.commands.reject(&:creates?).each do |command|
|
|
109
|
+
define_singleton_method("#{Naming.snake(command.hecks_name)}!") do |**args|
|
|
110
|
+
run(command, **args)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# ONE HEAD ADDRESSES THE SAME WAY AS SEVERAL. `@ir.identified_by` is only
|
|
116
|
+
# the single-head shorthand — nil the moment an identity is composite
|
|
117
|
+
# (`SafeDepositBox`'s `branch_code`/`box_number`) — so building the
|
|
118
|
+
# identity payload from `identity_heads` instead reads every head, one
|
|
119
|
+
# or many alike, straight out of state that already carries them.
|
|
120
|
+
def run(command, **args)
|
|
121
|
+
@state = @dispatcher.dispatch("#{fqn}.#{command.hecks_name}", to: @id, with: args).instance.state
|
|
122
|
+
self
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# THE OTHER HALF OF A CROSS-REFERENCE. `transfer.source` already reads
|
|
126
|
+
# the raw value — a plain reader, same as any other attribute, still
|
|
127
|
+
# needed by a `given`. This is the hydrated hop docs/rails-integration.md
|
|
128
|
+
# designed and marked "nothing built": `transfer.source_account`
|
|
129
|
+
# resolves it to the actual Account record, on demand — nothing loads
|
|
130
|
+
# until called, and this hop never triggers the next one. Plain
|
|
131
|
+
# chaining composes for free from here : `payment.disputed_by_customer.name`
|
|
132
|
+
# is two ordinary calls, each individually lazy, which is exactly why
|
|
133
|
+
# this is a named accessor per reference rather than a `through:`
|
|
134
|
+
# option — that shape was considered and rejected in the same design
|
|
135
|
+
# note for hiding how many lookups actually happened behind one call.
|
|
136
|
+
#
|
|
137
|
+
# Defined BEFORE verb methods, not after — on the vanishing chance a
|
|
138
|
+
# reference's own accessor name collided with a command's, the verb
|
|
139
|
+
# should win; `initialize` calls this first so `define_verb_methods`
|
|
140
|
+
# defines second and last.
|
|
141
|
+
# NO DERIVATION LEFT (ADR 0025, "References"): `reference_to`
|
|
142
|
+
# itself mints the bare attribute name now — `:account`, never
|
|
143
|
+
# `:account_id` — so the accessor is spelled exactly like the
|
|
144
|
+
# attribute it reads, with no `_id`-strip or `as:`-suffix rule to
|
|
145
|
+
# apply first. `piece.account` (a METHOD, defined here) and
|
|
146
|
+
# `piece[:account]` (`Handle#[]`, bracket access reading the raw
|
|
147
|
+
# id straight off `@instance`) never collide despite sharing a
|
|
148
|
+
# name — Ruby dispatches the two completely differently — which is
|
|
149
|
+
# what makes the OLD "studio_studio" double-suffix workaround
|
|
150
|
+
# (this method used to force a DIFFERENT name specifically to dodge
|
|
151
|
+
# that non-collision) unnecessary rather than merely simplified.
|
|
152
|
+
def define_reference_accessors
|
|
153
|
+
@ir.attributes.select(&:reference?).each do |attribute|
|
|
154
|
+
target = attribute.type.resolve
|
|
155
|
+
next unless target # cross-domain, or otherwise unresolvable — no accessor rather than a guess
|
|
156
|
+
|
|
157
|
+
domain = @domain
|
|
158
|
+
field = attribute.name
|
|
159
|
+
list = attribute.list?
|
|
160
|
+
target_fqn = "#{domain}::#{target.hecks_name}"
|
|
161
|
+
|
|
162
|
+
define_singleton_method(field) do
|
|
163
|
+
value = self[field]
|
|
164
|
+
door = Object.const_get(target_fqn)
|
|
165
|
+
next Array(value).map { |identity| door.find(identity) } if list
|
|
166
|
+
|
|
167
|
+
value && door.find(value)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|