hecks 0.2.0 → 1.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 +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 +427 -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 +101 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +179 -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 +145 -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 +88 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +438 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +17 -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 +80 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +306 -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 +54 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +125 -0
- data/lib/hecks/behaviors/expectations.rb +340 -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 +438 -0
- data/lib/hecks/bluebook/assembly/marks.rb +228 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +91 -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 +68 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -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 +78 -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 +1018 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +714 -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 +430 -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 +135 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -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 +173 -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 +228 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +117 -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 +123 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
- data/lib/hecks/bluebook/expression/projection.json +218 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +781 -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 +621 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -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 +542 -0
- data/lib/hecks/bluebook/model_check.rb +445 -0
- data/lib/hecks/bluebook/pattern_subset.rb +184 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +128 -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 +66 -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 +410 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +119 -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 +166 -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 +341 -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 +177 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +84 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +161 -0
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +119 -0
- data/lib/hecks/forms/record_table.rb +68 -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 +145 -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/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
- data/lib/hecks/fuzzing/properties.rb +1192 -0
- data/lib/hecks/fuzzing/replay.rb +668 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
- data/lib/hecks/fuzzing/value_generator.rb +200 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +324 -0
- data/lib/hecks/grammar/expression.bluebook +420 -0
- data/lib/hecks/grammar/expression_operators.json +1648 -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 +471 -0
- data/lib/hecks/language/bluebook/entity.bluebook +392 -0
- data/lib/hecks/language/bluebook/policy.bluebook +189 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +381 -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 +448 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -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 +333 -0
- data/lib/hecks/language/port.bluebook +120 -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 +174 -0
- data/lib/hecks/ports/access_control.port +9 -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 +8 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +6 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +7 -0
- data/lib/hecks/ports/authorization.rb +62 -0
- data/lib/hecks/ports/clock.port +5 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +5 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +5 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +5 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +5 -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 +138 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +23 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
- data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -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 +96 -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 +642 -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 +44 -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 +446 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +158 -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 +411 -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 +92 -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 +107 -0
- data/lib/hecks/query_specification/hop_path.rb +132 -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/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +66 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
- data/lib/hecks/runtime/command_interpreter.rb +497 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +189 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +245 -0
- data/lib/hecks/runtime/dispatcher.rb +301 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +299 -0
- data/lib/hecks/runtime/errors.rb +122 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +156 -0
- data/lib/hecks/runtime/instance.rb +175 -0
- data/lib/hecks/runtime/interpreting.rb +90 -0
- data/lib/hecks/runtime/loader.rb +184 -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 +286 -0
- data/lib/hecks/runtime/reaction_invocation.rb +253 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +341 -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 +126 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
- data/lib/hecks/runtime/registry/verification.rb +246 -0
- data/lib/hecks/runtime/registry.rb +279 -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 +503 -0
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -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 +130 -0
- data/lib/hecks/runtime/value/coercion.rb +565 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +107 -0
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +218 -0
- data/lib/hecks.rb +123 -6
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +412 -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,445 @@
|
|
|
1
|
+
require "set"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Bluebook
|
|
5
|
+
# Lightweight formal methods over the IR — the same family as TLA+/
|
|
6
|
+
# Alloy/P: every lifecycle is a declared finite state machine and
|
|
7
|
+
# every process manager a declared protocol, and BOTH are already
|
|
8
|
+
# data, not code, so they can be MODEL-CHECKED rather than merely
|
|
9
|
+
# executed. Static analysis only — no bluebook boots twice, no
|
|
10
|
+
# runtime is touched — over what meta_validator/judge.rb and the
|
|
11
|
+
# builders' own `validate!` methods leave uncovered (an undeclared
|
|
12
|
+
# transition target, a dispatch to nowhere, a compensation nothing
|
|
13
|
+
# can ever reach).
|
|
14
|
+
#
|
|
15
|
+
# THE RARE PROPERTY THIS RESTS ON: the model IS the implementation.
|
|
16
|
+
# A checker over TLA+ verifies a SPEC a human keeps in sync with
|
|
17
|
+
# code by hand ; this verifies the same IR the runtime dispatches
|
|
18
|
+
# against, so there is no second copy to drift.
|
|
19
|
+
module ModelCheck
|
|
20
|
+
Finding = Struct.new(:kind, :severity, :subject, :message, keyword_init: true) do
|
|
21
|
+
def to_s = "#{severity.to_s.upcase.ljust(7)} #{kind.to_s.ljust(20)} #{subject} — #{message}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# A FINDING SHIPPED, NOT SILENCED — the coverage-gate idiom, empty
|
|
25
|
+
# allowlists enforced BOTH directions (spec/model_check_spec.rb holds
|
|
26
|
+
# this exact table: an error the checker reports and this does not
|
|
27
|
+
# name is a regression, an entry the checker no longer reports is
|
|
28
|
+
# stale and must be deleted). bin/model_check reads this same
|
|
29
|
+
# constant, so the tool and the spec can never drift apart.
|
|
30
|
+
#
|
|
31
|
+
# "banking"/ExternalSettlement — found on the first real run:
|
|
32
|
+
# ExternalSettlement declares `ends_on "ExternalTransferSent"`, and
|
|
33
|
+
# ExternalTransfer.Send genuinely emits it — the event is real, and
|
|
34
|
+
# the AGGREGATE reaches "sent" (its own, separate lifecycle) — but
|
|
35
|
+
# the SAGA'S protocol has no `on "ExternalTransferSent"` handler, so
|
|
36
|
+
# its own `state "sent"` is unreachable through the chain the
|
|
37
|
+
# checker walks, and the saga's own bookkeeping (saga_log, ends_on)
|
|
38
|
+
# never closes it. Real domain activity is unaffected; the saga's
|
|
39
|
+
# OWN tracking of it is not. Left named rather than redesigning a
|
|
40
|
+
# corpus fixture that is not this checker's to redesign.
|
|
41
|
+
# S7, ADR 0025 — the ExternalSettlement finding this used to
|
|
42
|
+
# allowlist is GONE, not just quieted: its "sent" state was a
|
|
43
|
+
# `state "x"` line never named by any handler's own from:/to:, a
|
|
44
|
+
# pure declaration-drift artifact. States are DERIVED from the
|
|
45
|
+
# transitions that name them now (ProcessManagerBuilder#derived_
|
|
46
|
+
# states), so a state nothing ever transitions into or out of no
|
|
47
|
+
# longer exists to be unreachable — the finding this allowlisted
|
|
48
|
+
# cannot occur any more, by construction.
|
|
49
|
+
#
|
|
50
|
+
# "banking"/NotifyOnClosure, FlagKeyReturn — real, confirmed
|
|
51
|
+
# findings, not bugs to fix. `across "Notifications"` names a
|
|
52
|
+
# domain that does not exist anywhere in this repo — no
|
|
53
|
+
# Notifications bluebook, no hecksagon, nothing to `uses_framework`
|
|
54
|
+
# or `subscribe` to. This is deliberate: `spec/runtime/policy_spec.
|
|
55
|
+
# rb` (a test literally named "records a reaction it cannot
|
|
56
|
+
# deliver rather than swallowing it") and `lib/hecks/runtime/
|
|
57
|
+
# errors.rb`'s own `UnknownVerb` comment both treat "target domain
|
|
58
|
+
# not loaded" as the EXPECTED outcome for it — Notifications is
|
|
59
|
+
# used on purpose to exercise the undelivered-reaction runtime
|
|
60
|
+
# path, not left half-built. There is no real `subscribe` line to
|
|
61
|
+
# add (no event of Notifications' own to name) and no real domain
|
|
62
|
+
# to point `uses_framework` at.
|
|
63
|
+
ALLOWED_FINDINGS = {
|
|
64
|
+
"banking" => [
|
|
65
|
+
[:unacknowledged_relationship, "NotifyOnClosure"],
|
|
66
|
+
[:unknown_target_domain, "NotifyOnClosure"],
|
|
67
|
+
[:unacknowledged_relationship, "FlagKeyReturn"],
|
|
68
|
+
[:unknown_target_domain, "FlagKeyReturn"]
|
|
69
|
+
]
|
|
70
|
+
}.freeze
|
|
71
|
+
|
|
72
|
+
module_function
|
|
73
|
+
|
|
74
|
+
# `hecksagon:`/`known_domains:` — both optional, both `nil`-safe
|
|
75
|
+
# (every existing caller with no sibling hecksagon, or checking one
|
|
76
|
+
# domain in isolation, behaves exactly as before). `hecksagon` is
|
|
77
|
+
# THIS bluebook's own sibling wiring file, if the caller loaded one
|
|
78
|
+
# (see `emitted_events`'s own comment on why a caller that didn't
|
|
79
|
+
# simply finds none, correctly). `known_domains` is the caller's
|
|
80
|
+
# OWN corpus-wide view — every bluebook/hecksagon name it has
|
|
81
|
+
# booted anywhere, across every domain it has looked at, not just
|
|
82
|
+
# this one — used only to catch a typo'd `across`/`uses_framework`
|
|
83
|
+
# target; see `cross_domain_policy_findings`'s own comment for why
|
|
84
|
+
# this can only ever be a corpus-scoped heuristic, never a general
|
|
85
|
+
# correctness guarantee.
|
|
86
|
+
def call(bluebook, hecksagon: nil, known_domains: nil)
|
|
87
|
+
findings = []
|
|
88
|
+
bluebook.aggregates.each do |aggregate|
|
|
89
|
+
findings.concat(lifecycle_findings(aggregate, aggregate))
|
|
90
|
+
aggregate.entities.each { |entity| findings.concat(lifecycle_findings(aggregate, entity)) }
|
|
91
|
+
end
|
|
92
|
+
bluebook.process_managers.each { |pm| findings.concat(saga_findings(bluebook, pm)) }
|
|
93
|
+
bluebook.policies.each { |policy| findings.concat(policy_findings(bluebook, policy, hecksagon, known_domains)) }
|
|
94
|
+
findings
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# ── lifecycles (aggregate AND entity — a piece may declare one too) ──
|
|
98
|
+
|
|
99
|
+
def lifecycle_findings(aggregate, declaring)
|
|
100
|
+
lifecycle = declaring.lifecycle
|
|
101
|
+
return [] unless lifecycle
|
|
102
|
+
|
|
103
|
+
subject = declaring.equal?(aggregate) ? aggregate.hecks_name : "#{aggregate.hecks_name}::#{declaring.hecks_name}"
|
|
104
|
+
commands = Array(declaring.commands).map(&:hecks_name)
|
|
105
|
+
|
|
106
|
+
findings = []
|
|
107
|
+
findings.concat(unknown_transition_commands(lifecycle, commands, subject))
|
|
108
|
+
|
|
109
|
+
full = full_states(lifecycle)
|
|
110
|
+
reached = reachable_states(lifecycle)
|
|
111
|
+
|
|
112
|
+
(full - reached.to_a).each do |state|
|
|
113
|
+
findings << Finding.new(kind: :unreachable_state, severity: :error, subject: subject,
|
|
114
|
+
message: "#{state.inspect} is declared (in a transition's from: or target) " \
|
|
115
|
+
"but no path from #{lifecycle.default.inspect} ever reaches it")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
lifecycle.transitions.each do |command, transition|
|
|
119
|
+
next unless transition.constrained?
|
|
120
|
+
next if Array(transition.from).any? { |source| reached.include?(source) }
|
|
121
|
+
|
|
122
|
+
findings << Finding.new(kind: :dead_transition, severity: :error, subject: subject,
|
|
123
|
+
message: "#{command} from #{Array(transition.from).inspect} can never fire — " \
|
|
124
|
+
"none of those states is ever reached")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
any_unconstrained = lifecycle.transitions.any? { |_, t| !t.constrained? }
|
|
128
|
+
(reached - terminal_exempt(lifecycle)).each do |state|
|
|
129
|
+
next if any_unconstrained
|
|
130
|
+
next if lifecycle.transitions.any? { |_, t| t.constrained? && Array(t.from).include?(state) }
|
|
131
|
+
|
|
132
|
+
findings << Finding.new(kind: :stuck_state, severity: :warning, subject: subject,
|
|
133
|
+
message: "#{state.inspect} is reached but no transition ever leaves it — " \
|
|
134
|
+
"fine if that is meant to be terminal")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
findings
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def unknown_transition_commands(lifecycle, commands, subject)
|
|
141
|
+
lifecycle.transitions.filter_map do |command, _transition|
|
|
142
|
+
next if commands.include?(command)
|
|
143
|
+
|
|
144
|
+
Finding.new(kind: :unknown_command, severity: :error, subject: subject,
|
|
145
|
+
message: "a transition names #{command.inspect}, which this construct declares no command for")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# default, every declared target, and every declared from — a
|
|
150
|
+
# from-only state (declared nowhere as a target) is real and is
|
|
151
|
+
# exactly the hole `Lifecycle#states` leaves: it answers default
|
|
152
|
+
# plus targets only.
|
|
153
|
+
def full_states(lifecycle)
|
|
154
|
+
(
|
|
155
|
+
[lifecycle.default] +
|
|
156
|
+
lifecycle.transitions.map { |_, t| t.target } +
|
|
157
|
+
lifecycle.transitions.flat_map { |_, t| Array(t.from) }
|
|
158
|
+
).uniq
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Least fixpoint from the default state: an UNCONSTRAINED
|
|
162
|
+
# transition always fires, from wherever the machine is ; a
|
|
163
|
+
# constrained one fires once any of its named sources is reached.
|
|
164
|
+
def reachable_states(lifecycle)
|
|
165
|
+
reached = Set.new([lifecycle.default])
|
|
166
|
+
loop do
|
|
167
|
+
grown = false
|
|
168
|
+
lifecycle.transitions.each do |_, transition|
|
|
169
|
+
next if reached.include?(transition.target)
|
|
170
|
+
next if transition.constrained? && Array(transition.from).none? { |source| reached.include?(source) }
|
|
171
|
+
|
|
172
|
+
reached << transition.target
|
|
173
|
+
grown = true
|
|
174
|
+
end
|
|
175
|
+
break unless grown
|
|
176
|
+
end
|
|
177
|
+
reached
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# A state with no OUTGOING declared path at all is exempt from the
|
|
181
|
+
# stuck-state WARNING for a different reason than "it fires an
|
|
182
|
+
# unconstrained transition" — the default state of a lifecycle
|
|
183
|
+
# with only constrained transitions is legitimately allowed to sit
|
|
184
|
+
# forever, since nothing about *entering* it via default implies
|
|
185
|
+
# anything must eventually move it, unlike a state a transition
|
|
186
|
+
# explicitly delivered somewhere.
|
|
187
|
+
def terminal_exempt(_lifecycle) = []
|
|
188
|
+
|
|
189
|
+
# ── process managers / sagas ──────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
def saga_findings(bluebook, pm)
|
|
192
|
+
findings = []
|
|
193
|
+
emitted = emitted_events(bluebook)
|
|
194
|
+
verbs = verbs_of(bluebook)
|
|
195
|
+
|
|
196
|
+
[pm.starts_on, pm.ends_on].compact.each do |event|
|
|
197
|
+
next if emitted.include?(bare(event))
|
|
198
|
+
|
|
199
|
+
findings << Finding.new(kind: :deaf_trigger, severity: :error, subject: pm.name,
|
|
200
|
+
message: "starts_on/ends_on names #{event.inspect}, which no command in this " \
|
|
201
|
+
"domain emits")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
reached = pm_reachable_states(pm, emitted)
|
|
205
|
+
(Array(pm.states) - reached.to_a).each do |state|
|
|
206
|
+
findings << Finding.new(kind: :unreachable_pm_state, severity: :error, subject: pm.name,
|
|
207
|
+
message: "#{state.inspect} is declared but no handler chain from " \
|
|
208
|
+
"#{pm.states.first.inspect} ever reaches it")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
pm.handlers.each do |handler|
|
|
212
|
+
# The compensating leg answers REFUSED, a synthetic trigger no
|
|
213
|
+
# command ever emits by name (ProcessManager::REFUSED) — not
|
|
214
|
+
# a deaf handler, the one handler this domain's own events can
|
|
215
|
+
# never satisfy on purpose.
|
|
216
|
+
if handler.event_type != ProcessManager::REFUSED && !emitted.include?(bare(handler.event_type))
|
|
217
|
+
findings << Finding.new(kind: :deaf_handler, severity: :error, subject: pm.name,
|
|
218
|
+
message: "a handler answers #{handler.event_type.inspect}, which no command " \
|
|
219
|
+
"in this domain emits")
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
handler.dispatches.each do |dispatch|
|
|
223
|
+
# SAME-DOMAIN, same as `SagaInterpreter#qualified` — a dispatch
|
|
224
|
+
# naming no domain at all (the ordinary shape a bare command
|
|
225
|
+
# constant now produces, S6) means THIS one, and is compared
|
|
226
|
+
# against `verbs_of`'s own fully-qualified spelling qualified
|
|
227
|
+
# the identical way, not left bare to miss it on a technicality.
|
|
228
|
+
qualified = dispatch.command_name.include?("::") ? dispatch.command_name : "#{bluebook.name}::#{dispatch.command_name}"
|
|
229
|
+
next if verbs.include?(qualified)
|
|
230
|
+
|
|
231
|
+
findings << Finding.new(kind: :unknown_dispatch, severity: :error, subject: pm.name,
|
|
232
|
+
message: "dispatches #{dispatch.command_name.inspect}, which this domain " \
|
|
233
|
+
"declares no command at — cross-domain dispatch is out of this " \
|
|
234
|
+
"checker's scope, same as CommandRules#resolve_references")
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# A `compensates` DECLARED WITH NOWHERE TO EVER FIRE — the exact
|
|
238
|
+
# shape of the real bug this whole feature closes ("the
|
|
239
|
+
# reversal was written and never armed"), caught at build/
|
|
240
|
+
# model-check time instead of discovered in production. No
|
|
241
|
+
# handler anywhere answers REFUSED (`pm.saga?` false) means
|
|
242
|
+
# `SagaInterpreter#unwind` never runs for this process
|
|
243
|
+
# manager at all, so a declared `compensates` is structurally
|
|
244
|
+
# unreachable — not a warning about style, a dead declaration.
|
|
245
|
+
if !pm.saga? && handler.dispatches.any?(&:compensates)
|
|
246
|
+
handler.dispatches.select(&:compensates).each do |dispatch|
|
|
247
|
+
findings << Finding.new(kind: :unarmed_compensation, severity: :error, subject: pm.name,
|
|
248
|
+
message: "#{dispatch.command_name} compensates #{dispatch.compensates.command_name}, " \
|
|
249
|
+
"but no handler anywhere in this saga answers a refusal — the " \
|
|
250
|
+
"compensation is declared and can never fire")
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
if pm.saga? && !reached.include?(pm.saga.from_state)
|
|
256
|
+
findings << Finding.new(kind: :dead_compensation, severity: :error, subject: pm.name,
|
|
257
|
+
message: "the compensation leaves #{pm.saga.from_state.inspect}, which no " \
|
|
258
|
+
"handler chain ever reaches — a refusal here can never fire it")
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
findings
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# A handler edge is only usable in the closure if it can actually
|
|
265
|
+
# FIRE — REFUSED always can (it is a compensation trigger, not an
|
|
266
|
+
# event), and any other handler needs its event genuinely emitted.
|
|
267
|
+
# Without this, a deaf handler's declared from_state -> to_state
|
|
268
|
+
# pair reads as connected even though nothing can ever traverse
|
|
269
|
+
# it, which would hide exactly the states this walk exists to
|
|
270
|
+
# catch (a state only "reachable" through a handler that itself
|
|
271
|
+
# never fires).
|
|
272
|
+
def pm_reachable_states(pm, emitted)
|
|
273
|
+
return Set.new if Array(pm.states).empty?
|
|
274
|
+
|
|
275
|
+
reached = Set.new([pm.states.first])
|
|
276
|
+
loop do
|
|
277
|
+
grown = false
|
|
278
|
+
pm.handlers.each do |handler|
|
|
279
|
+
next unless handler.event_type == ProcessManager::REFUSED || emitted.include?(bare(handler.event_type))
|
|
280
|
+
next unless reached.include?(handler.from_state)
|
|
281
|
+
next if reached.include?(handler.to_state)
|
|
282
|
+
|
|
283
|
+
reached << handler.to_state
|
|
284
|
+
grown = true
|
|
285
|
+
end
|
|
286
|
+
break unless grown
|
|
287
|
+
end
|
|
288
|
+
reached
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# ── policies ───────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
def policy_findings(bluebook, policy, hecksagon, known_domains)
|
|
294
|
+
return cross_domain_policy_findings(policy, hecksagon, known_domains) if policy.target_domain
|
|
295
|
+
|
|
296
|
+
emitted = emitted_events(bluebook)
|
|
297
|
+
findings = []
|
|
298
|
+
|
|
299
|
+
# `policy.event_name` (Naming.unqualified) — NOT `bare`, which only
|
|
300
|
+
# strips a "::" domain qualifier. An AGGREGATE-scoped policy's
|
|
301
|
+
# `on_event` carries a "." aggregate qualifier instead (PolicyBuilder
|
|
302
|
+
# stores whatever was typed, verbatim — see `on "Account.
|
|
303
|
+
# AccountFrozen"`), and `bare` left it untouched, silently comparing
|
|
304
|
+
# "Account.AccountFrozen" against a list of bare emitted names that
|
|
305
|
+
# can never contain it. Latent until now : banking's one aggregate-
|
|
306
|
+
# scoped same-domain policy check would have caught it, but its only
|
|
307
|
+
# prior aggregate-scoped policy (ReviewOnFreeze) is also cross-domain
|
|
308
|
+
# (`across "Compliance"`), which exits this method one line above
|
|
309
|
+
# before the mismatch is ever reached.
|
|
310
|
+
unless emitted.include?(policy.event_name)
|
|
311
|
+
findings << Finding.new(kind: :deaf_policy, severity: :error, subject: policy.name,
|
|
312
|
+
message: "on #{policy.on_event.inspect}, which no command in this domain emits")
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# `trigger` is spelled "Aggregate.Command" (or "Entity.Command" one
|
|
316
|
+
# level down), completed to an FQN by PolicyInterpreter#deliver as
|
|
317
|
+
# "#{domain}::#{trigger_command}" — the same join `verbs_of` builds
|
|
318
|
+
# independently, so the two spellings have to be compared as FQNs,
|
|
319
|
+
# never as bare command names.
|
|
320
|
+
unless verbs_of(bluebook).include?("#{bluebook.name}::#{policy.trigger_command}")
|
|
321
|
+
findings << Finding.new(kind: :unknown_trigger, severity: :error, subject: policy.name,
|
|
322
|
+
message: "trigger #{policy.trigger_command.inspect} resolves to no command " \
|
|
323
|
+
"this domain declares")
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
findings
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# ── cross-domain policies (Context Mapping) ───────────────────────
|
|
330
|
+
#
|
|
331
|
+
# `uses_framework "X"` already IS a Shared Kernel relationship — it
|
|
332
|
+
# merges X's own bluebook into THIS registry, no boundary. A cross-
|
|
333
|
+
# domain `policy ... across: "X"` already IS a Customer/Supplier
|
|
334
|
+
# relationship — it dispatches into X over real cross-Lambda RPC in
|
|
335
|
+
# the Rust host (`rust/host/src/lambda_client.rs`). Neither is a new
|
|
336
|
+
# word; this makes the CHOICE between them checked instead of a
|
|
337
|
+
# prose comment nobody enforces (`examples/banking/bluebook/
|
|
338
|
+
# banking.hecksagon`'s own hand-written note explaining why
|
|
339
|
+
# Compliance is reached via `across`, never `uses_framework`).
|
|
340
|
+
#
|
|
341
|
+
# NO NEW KEYWORD ANYWHERE — ADR 0025 principle 1 ("one idea, one
|
|
342
|
+
# spelling") refuses a `relationship:`/`as:` argument that would
|
|
343
|
+
# just restate, as a string, the fact the chosen keyword (
|
|
344
|
+
# `uses_framework` vs `across`) already states completely. The
|
|
345
|
+
# DDD vocabulary (Shared Kernel, Customer/Supplier) lives here, in
|
|
346
|
+
# the finding's own name and this comment, and in prose docs — not
|
|
347
|
+
# in the grammar.
|
|
348
|
+
def cross_domain_policy_findings(policy, hecksagon, known_domains)
|
|
349
|
+
return [] unless hecksagon # no sibling hecksagon loaded — nothing to check a relationship against.
|
|
350
|
+
|
|
351
|
+
target = policy.target_domain
|
|
352
|
+
findings = []
|
|
353
|
+
|
|
354
|
+
if hecksagon.framework_members.include?(target)
|
|
355
|
+
# SHARED KERNEL AND CUSTOMER/SUPPLIER ARE MUTUALLY EXCLUSIVE
|
|
356
|
+
# CLAIMS about the SAME target — `uses_framework` means "X is
|
|
357
|
+
# loaded in-process, right here"; `across` means "X is a
|
|
358
|
+
# separate deployment, reached only by RPC." Declaring both is
|
|
359
|
+
# either a pointless RPC to a domain already local, or a
|
|
360
|
+
# `uses_framework` that isn't really doing what its name says.
|
|
361
|
+
findings << Finding.new(kind: :contradictory_relationship, severity: :error, subject: policy.name,
|
|
362
|
+
message: "across #{target.inspect} dispatches over RPC (Customer/Supplier), " \
|
|
363
|
+
"but this hecksagon also uses_framework #{target.inspect} (Shared " \
|
|
364
|
+
"Kernel) — #{target} is already loaded in-process here, so the two " \
|
|
365
|
+
"relationship declarations contradict each other for the same " \
|
|
366
|
+
"target domain")
|
|
367
|
+
elsif hecksagon.subscriptions.none? { |subscribed| Naming.qualifier(subscribed) == target }
|
|
368
|
+
# THIS IS WHAT FINALLY GIVES `subscribe` REAL TEETH — checked
|
|
369
|
+
# here, at model-check time, still never routed at runtime
|
|
370
|
+
# (nothing dispatches off a `subscribe` line; see hecksagon.md's
|
|
371
|
+
# own "checked, not routed" section). ADR 0025 names `subscribe`
|
|
372
|
+
# by number as failing the corpus-use bar; this is the real use.
|
|
373
|
+
findings << Finding.new(kind: :unacknowledged_relationship, severity: :error, subject: policy.name,
|
|
374
|
+
message: "across #{target.inspect} declares a Customer/Supplier " \
|
|
375
|
+
"relationship, but nothing in this hecksagon records the " \
|
|
376
|
+
"expectation — add subscribe \"#{target}.SomeEvent\" for what " \
|
|
377
|
+
"you expect back from it, or uses_framework #{target.inspect} to " \
|
|
378
|
+
"attach it in-process instead")
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# TYPO DETECTION, DELIBERATELY WEAKER — `known_domains` can only
|
|
382
|
+
# ever be a MONOREPO-SCOPED heuristic: a real external hecks
|
|
383
|
+
# consumer's own domain (this repo's own embryonaut/lifeadelics-
|
|
384
|
+
# shaped case) lives in a genuinely separate repository this
|
|
385
|
+
# corpus scan can never see, so a target this check cannot find
|
|
386
|
+
# is "unknown to THIS corpus," never proof of a typo. Two real,
|
|
387
|
+
# legitimate reasons a target is unresolvable — genuinely
|
|
388
|
+
# undefined by design (the corpus's own "Notifications," used
|
|
389
|
+
# deliberately to exercise the undelivered-reaction runtime path)
|
|
390
|
+
# and real-but-external (a separate repository) — both go in
|
|
391
|
+
# `ALLOWED_FINDINGS`, the same judged-exception mechanism this
|
|
392
|
+
# file already uses for ExternalSettlement, rather than a new
|
|
393
|
+
# keyword invented to declare "this one's fine."
|
|
394
|
+
if known_domains && !known_domains.include?(target)
|
|
395
|
+
findings << Finding.new(kind: :unknown_target_domain, severity: :error, subject: policy.name,
|
|
396
|
+
message: "across #{target.inspect} names a domain nowhere in the corpus " \
|
|
397
|
+
"this check has booted — a typo, or a real domain intentionally " \
|
|
398
|
+
"outside this corpus (undefined by design, or living in a " \
|
|
399
|
+
"separate repository) belongs in ALLOWED_FINDINGS, named and " \
|
|
400
|
+
"explained, not silently assumed correct")
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
findings
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# ── shared enumeration ────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
# A PORT OPERATION EMITS TOO — the primary/driving port an adapter
|
|
409
|
+
# outside the bluebook calls through (see hecksagon_builder.rb) is a
|
|
410
|
+
# second, real source of events, alongside a command's own `emits`.
|
|
411
|
+
# Ports attach to the aggregate/bluebook from the SIBLING `.hecksagon`
|
|
412
|
+
# file, not this one — a caller that boots only the `.bluebook` (as
|
|
413
|
+
# the fixtures under spec/fixtures/model_check/ do, having no
|
|
414
|
+
# hecksagon at all) simply finds none, which is correct : nothing
|
|
415
|
+
# can be deaf to an event that isn't even wired up yet.
|
|
416
|
+
def emitted_events(bluebook)
|
|
417
|
+
aggregate_emits = bluebook.aggregates.flat_map do |aggregate|
|
|
418
|
+
aggregate.commands.map(&:emits) +
|
|
419
|
+
aggregate.entities.flat_map { |entity| entity.commands.map(&:emits) } +
|
|
420
|
+
aggregate.ports.flat_map { |port| port.operations.map(&:emits) }
|
|
421
|
+
end
|
|
422
|
+
chapter_emits = bluebook.ports.flat_map { |port| port.operations.map(&:emits) }
|
|
423
|
+
|
|
424
|
+
(aggregate_emits + chapter_emits).flatten.uniq
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Fully-qualified, the same spelling DispatchSpec#command_name
|
|
428
|
+
# carries and fuzzing/sequence_generator/catalog.rb builds
|
|
429
|
+
# independently for the same reason: a saga dispatch and a fuzzer
|
|
430
|
+
# step both have to name a verb the same way the door does.
|
|
431
|
+
def verbs_of(bluebook)
|
|
432
|
+
bluebook.aggregates.flat_map do |aggregate|
|
|
433
|
+
verbs = aggregate.commands.map { |command| "#{bluebook.name}::#{aggregate.hecks_name}.#{command.hecks_name}" }
|
|
434
|
+
verbs + aggregate.entities.flat_map do |entity|
|
|
435
|
+
entity.commands.map { |command|
|
|
436
|
+
"#{bluebook.name}::#{aggregate.hecks_name}.#{entity.hecks_name}.#{command.hecks_name}"
|
|
437
|
+
}
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def bare(event) = event.to_s.split("::").last
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Bluebook
|
|
3
|
+
# WHICH REGEXES A BLUEBOOK MAY SAY.
|
|
4
|
+
#
|
|
5
|
+
# A `pattern:` is a fact about a value, carried in a bluebook — declared
|
|
6
|
+
# data, not Ruby code, so it must not lean on what any one engine happens
|
|
7
|
+
# to accept. Regex engines disagree in two different ways :
|
|
8
|
+
#
|
|
9
|
+
# ONLY A BACKTRACKING ENGINE CAN MATCH IT — lookahead, lookbehind,
|
|
10
|
+
# backreferences, atomic groups, possessive quantifiers. None of these
|
|
11
|
+
# can be matched in linear time, and linear-time engines refuse them
|
|
12
|
+
# outright. Refused here for the same reason.
|
|
13
|
+
#
|
|
14
|
+
# EVERY ENGINE PARSES IT AND THEY MEAN DIFFERENT THINGS — the dangerous
|
|
15
|
+
# half, because nothing errors. `\d` `\w` `\s` are ASCII in some engines
|
|
16
|
+
# and Unicode in others ; `[:digit:]` and friends flip the same way in
|
|
17
|
+
# the other direction. Both families are refused, and a domain spells
|
|
18
|
+
# the range it means.
|
|
19
|
+
#
|
|
20
|
+
# What remains — explicit ranges, alternation, quantifiers, anchors,
|
|
21
|
+
# groups — reads identically everywhere, with `^` and `$` as LINE anchors
|
|
22
|
+
# (Ruby's reading). The evidence is spec/corpus/fixtures/patterns.json.
|
|
23
|
+
module PatternSubset
|
|
24
|
+
Rejection = Struct.new(:construct, :reason)
|
|
25
|
+
|
|
26
|
+
REASONS = {
|
|
27
|
+
backreference:
|
|
28
|
+
"backreferences cannot be matched in linear time and portable " \
|
|
29
|
+
"engines refuse them ; a declared pattern may not depend on one",
|
|
30
|
+
named_backreference:
|
|
31
|
+
"a named backreference is still a backreference — it cannot be " \
|
|
32
|
+
"matched in linear time ; a declared pattern may not depend on one",
|
|
33
|
+
perl_class:
|
|
34
|
+
"engines read it in OPPOSITE directions : ASCII in some and " \
|
|
35
|
+
"Unicode in others, so an Arabic-Indic digit satisfies one and not " \
|
|
36
|
+
"the other. Spell the range you mean — [0-9], [A-Za-z0-9_], [ \t] " \
|
|
37
|
+
"— which every engine reads the same way",
|
|
38
|
+
posix_class:
|
|
39
|
+
"[:digit:] and friends flip between ASCII and Unicode across " \
|
|
40
|
+
"engines — the mirror of the perl classes, and wrong in the same " \
|
|
41
|
+
"way. Spell the range you mean",
|
|
42
|
+
lookahead:
|
|
43
|
+
"lookahead cannot be matched in linear time and portable engines " \
|
|
44
|
+
"refuse it ; a declared pattern may not depend on it",
|
|
45
|
+
lookbehind:
|
|
46
|
+
"lookbehind cannot be matched in linear time and portable engines " \
|
|
47
|
+
"refuse it ; a declared pattern may not depend on it",
|
|
48
|
+
atomic_group:
|
|
49
|
+
"an atomic group is a backtracking-engine control knob — " \
|
|
50
|
+
"linear-time engines reject `(?>` as a syntax error",
|
|
51
|
+
possessive:
|
|
52
|
+
"a possessive quantifier is a backtracking-engine control knob — " \
|
|
53
|
+
"linear-time engines reject it as a syntax error"
|
|
54
|
+
}.freeze
|
|
55
|
+
|
|
56
|
+
module_function
|
|
57
|
+
|
|
58
|
+
# nil when the pattern is admitted, a Rejection when it is not.
|
|
59
|
+
#
|
|
60
|
+
# A CHARACTER WALK, deliberately plain : the subset is defined by this
|
|
61
|
+
# walk, and a cleverer spelling would hide what it admits. An escaped
|
|
62
|
+
# construct is a LITERAL, not a violation — `\(\?=` is the three
|
|
63
|
+
# characters "(?=" and says nothing about lookahead — which is why this
|
|
64
|
+
# steps over each backslash pair rather than matching the pattern as a
|
|
65
|
+
# whole.
|
|
66
|
+
def validate(pattern)
|
|
67
|
+
chars = pattern.to_s.chars
|
|
68
|
+
index = 0
|
|
69
|
+
# A CHARACTER-CLASS INTERIOR IS A DIFFERENT ALPHABET : inside `[...]`,
|
|
70
|
+
# `*`, `+`, `?`, `(`, `?` are literal characters, not quantifiers or
|
|
71
|
+
# group syntax — `[*+]` means "a literal asterisk or plus". `]` is
|
|
72
|
+
# only the class's close when it isn't the first character after `[`
|
|
73
|
+
# or `[^` (where it is itself a literal, per POSIX bracket-expression
|
|
74
|
+
# rules).
|
|
75
|
+
in_class = false
|
|
76
|
+
class_start = nil
|
|
77
|
+
|
|
78
|
+
while index < chars.length
|
|
79
|
+
if chars[index] == "\\"
|
|
80
|
+
nxt = chars[index + 1]
|
|
81
|
+
return refuse(:backreference) if nxt&.match?(/[1-9]/)
|
|
82
|
+
return refuse(:named_backreference) if %w[k g].include?(nxt)
|
|
83
|
+
return refuse(:perl_class) if %w[d D w W s S].include?(nxt)
|
|
84
|
+
|
|
85
|
+
index += nxt ? 2 : 1
|
|
86
|
+
next
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if in_class
|
|
90
|
+
if chars[index] == "]" && index != class_start
|
|
91
|
+
in_class = false
|
|
92
|
+
index += 1
|
|
93
|
+
next
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
return refuse(:posix_class) if posix_class_at?(chars, index)
|
|
97
|
+
|
|
98
|
+
index += 1
|
|
99
|
+
next
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if chars[index] == "["
|
|
103
|
+
return refuse(:posix_class) if posix_class_at?(chars, index)
|
|
104
|
+
|
|
105
|
+
in_class = true
|
|
106
|
+
class_start = index + 1
|
|
107
|
+
class_start += 1 if chars[class_start] == "^"
|
|
108
|
+
index += 1
|
|
109
|
+
next
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if chars[index] == "(" && chars[index + 1] == "?"
|
|
113
|
+
return refuse(:lookahead) if %w[= !].include?(chars[index + 2])
|
|
114
|
+
return refuse(:lookbehind) if chars[index + 2] == "<" && %w[= !].include?(chars[index + 3])
|
|
115
|
+
return refuse(:atomic_group) if chars[index + 2] == ">"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
return refuse(:possessive) if possessive_at?(chars, index)
|
|
119
|
+
|
|
120
|
+
index += 1
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# SPELLED OUT, not derived from the key : these strings are the refusal
|
|
127
|
+
# a caller reads.
|
|
128
|
+
CONSTRUCTS = {
|
|
129
|
+
backreference: "backreference",
|
|
130
|
+
named_backreference: "named backreference",
|
|
131
|
+
perl_class: "perl character class",
|
|
132
|
+
posix_class: "posix bracket class",
|
|
133
|
+
lookahead: "lookahead",
|
|
134
|
+
lookbehind: "lookbehind",
|
|
135
|
+
atomic_group: "atomic group",
|
|
136
|
+
possessive: "possessive quantifier"
|
|
137
|
+
}.freeze
|
|
138
|
+
|
|
139
|
+
def refuse(key) = Rejection.new(CONSTRUCTS.fetch(key), REASONS.fetch(key))
|
|
140
|
+
|
|
141
|
+
def posix_class_at?(chars, index)
|
|
142
|
+
return false unless chars[index] == "[" && chars[index + 1] == ":"
|
|
143
|
+
|
|
144
|
+
cursor = index + 2
|
|
145
|
+
cursor += 1 while chars[cursor]&.match?(/[a-zA-Z]/)
|
|
146
|
+
chars[cursor] == ":" && chars[cursor + 1] == "]"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A possessive quantifier is `*+`, `++`, `?+`, or a bounded `{n}`/{n,m}`
|
|
150
|
+
# immediately followed by `+` — only checked OUTSIDE a character class,
|
|
151
|
+
# where `*`, `+`, `?`, `{`, `}` are quantifier syntax rather than
|
|
152
|
+
# literal characters.
|
|
153
|
+
def possessive_at?(chars, index)
|
|
154
|
+
return true if %w[* + ?].include?(chars[index]) && chars[index + 1] == "+"
|
|
155
|
+
return false unless chars[index] == "{"
|
|
156
|
+
|
|
157
|
+
len = bounded_quantifier_length(chars, index)
|
|
158
|
+
!len.nil? && chars[index + len] == "+"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Length of a `{n}` / `{n,}` / `{n,m}` bound starting at `index`, or nil
|
|
162
|
+
# if what's there isn't one.
|
|
163
|
+
def bounded_quantifier_length(chars, index)
|
|
164
|
+
cursor = index + 1
|
|
165
|
+
digit_seen = false
|
|
166
|
+
|
|
167
|
+
while chars[cursor]&.match?(/[0-9]/)
|
|
168
|
+
digit_seen = true
|
|
169
|
+
cursor += 1
|
|
170
|
+
end
|
|
171
|
+
return nil unless digit_seen
|
|
172
|
+
|
|
173
|
+
if chars[cursor] == ","
|
|
174
|
+
cursor += 1
|
|
175
|
+
cursor += 1 while chars[cursor]&.match?(/[0-9]/)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
return nil unless chars[cursor] == "}"
|
|
179
|
+
|
|
180
|
+
cursor - index + 1
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# GENERATED — projected from the language's own Policy aggregate.
|
|
2
|
+
# DO NOT EDIT: the holding half is rendered, and Behaviour::Policy
|
|
3
|
+
# is where anything hand-written belongs.
|
|
4
|
+
require_relative "behaviour/policy"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Bluebook
|
|
8
|
+
class Policy
|
|
9
|
+
include Hecks::IR
|
|
10
|
+
include Behaviour::Policy
|
|
11
|
+
|
|
12
|
+
emits_ir(
|
|
13
|
+
name: :name,
|
|
14
|
+
on_event: :on_event,
|
|
15
|
+
trigger_command: :trigger_command,
|
|
16
|
+
target_domain: :target_domain,
|
|
17
|
+
where: :where,
|
|
18
|
+
for_each: :for_each,
|
|
19
|
+
with_spec: -> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } }
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
attr_reader :name, :on_event, :trigger_command, :target_domain, :where, :for_each, :with_spec
|
|
23
|
+
|
|
24
|
+
# AGGREGATE, DECLARED AND DELIBERATELY OFF THE WIRE
|
|
25
|
+
# the wire format is a pinned contract, and it does not carry
|
|
26
|
+
# where a policy was written before the builder hoisted it
|
|
27
|
+
attr_accessor :aggregate
|
|
28
|
+
|
|
29
|
+
def initialize(name:, on_event: nil, trigger_command: nil, target_domain: nil, where: nil, for_each: nil, with_spec: [], aggregate: nil)
|
|
30
|
+
@name = name.to_s
|
|
31
|
+
@on_event = on_event
|
|
32
|
+
@trigger_command = trigger_command
|
|
33
|
+
@target_domain = target_domain
|
|
34
|
+
@where = where
|
|
35
|
+
@for_each = for_each
|
|
36
|
+
@with_spec = with_spec
|
|
37
|
+
@aggregate = aggregate&.to_s
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|