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,632 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "facade/command_request"
|
|
5
|
+
require_relative "facade/json_door"
|
|
6
|
+
require_relative "naming"
|
|
7
|
+
require_relative "projector"
|
|
8
|
+
require_relative "runtime/errors"
|
|
9
|
+
|
|
10
|
+
module Hecks
|
|
11
|
+
# THE BUS, NOT A DOOR — `docs/hecks-survey-what-we-wish-we-had.md` and
|
|
12
|
+
# `docs/future-features.md` both name the sibling project's own
|
|
13
|
+
# "Storehouse" the single highest-priority gap this repo had: "no
|
|
14
|
+
# per-command tool... the bluebook IS the contract, the [door] just
|
|
15
|
+
# projects it." This module IS that bus, borrowing its name too — the
|
|
16
|
+
# dispatch/query/state/... surface below is a pure function of a
|
|
17
|
+
# `Runtime::Dispatcher` plus plain Ruby arguments, no IO, no protocol
|
|
18
|
+
# awareness. `bin/hecks_mcp_door` is ONE projection of it — MCP over
|
|
19
|
+
# stdio — not the whole thing; a plain CLI door, an HTTP door, a second
|
|
20
|
+
# transport of any shape, would sit beside it on the exact same bus,
|
|
21
|
+
# sharing the same audit log and caller-identity handling, without
|
|
22
|
+
# ever needing to speak MCP. (This module used to BE named `McpDoor`
|
|
23
|
+
# and live under `Facade` — that conflated the bus with its one built
|
|
24
|
+
# transport; this file is the rename, not a rewrite.)
|
|
25
|
+
#
|
|
26
|
+
# catalog — what aggregates a domain declares, and what each can do
|
|
27
|
+
# describe — one aggregate's full command/query/refusal contract
|
|
28
|
+
# validate — is this domain's wiring sound (deep: also model-checks it)
|
|
29
|
+
# domains — every domain directory under a root, discovered not typed
|
|
30
|
+
# dispatch — issue a command (dry_run: preview, steps: batch)
|
|
31
|
+
# query — ask a question
|
|
32
|
+
# state — read what is actually stored, no verb involved
|
|
33
|
+
# events — what HAPPENED, with payloads, to one record — not just its
|
|
34
|
+
# current state; events THIS BUS witnessed, sourced from
|
|
35
|
+
# its own audit log, not a full event-sourcing replay
|
|
36
|
+
# history — the full append-only journal, not just current state
|
|
37
|
+
# behaviors — run a domain's hand-curated `.behaviors` examples
|
|
38
|
+
# follow — tail this bus's own dispatch/query/state audit log
|
|
39
|
+
#
|
|
40
|
+
# `dispatch`/`query`/`state` EACH TAKE A `summary` — the survey's "every
|
|
41
|
+
# audit row carries human intent for free" — and `dispatch`/`query`
|
|
42
|
+
# additionally take an optional `source:` (`SOURCE_TAGS`, the survey's
|
|
43
|
+
# `SourceTag`: who is calling) AND an optional `role:`/`actor_id:` —
|
|
44
|
+
# a real caller identity, bound for the call's duration via `Hecks.
|
|
45
|
+
# as_caller`, checked against a `role`-gated command's own declared
|
|
46
|
+
# role (`CommandRules::Authorization`) rather than merely documented by
|
|
47
|
+
# `describe`. `dispatch` REQUIRES it for any command that declares a
|
|
48
|
+
# role — `require_caller_for_role_gated!` refuses an unbound dispatch
|
|
49
|
+
# against one rather than silently running it unchecked; `query`'s own
|
|
50
|
+
# authorization runs on a separate mechanism (`Runtime::TenantScope`)
|
|
51
|
+
# that `role:` does not gate. This is self-asserted identity, not
|
|
52
|
+
# authentication: the caller states its own `role`/`actor_id`, and this
|
|
53
|
+
# bus checks it consistently once stated — see `bin/hecks_mcp_door`'s
|
|
54
|
+
# header for what that does and does not guarantee. Every call through
|
|
55
|
+
# those three, plus a dry run, is appended to a per-domain JSONL audit
|
|
56
|
+
# log (`record!`) `follow` tails back — a record of what the caller
|
|
57
|
+
# SAID it was, not independently verified identity. `catalog`/
|
|
58
|
+
# `describe`/`validate`/`domains`/`history`/`behaviors`/`events` need
|
|
59
|
+
# neither — they change nothing and commit nothing to any log.
|
|
60
|
+
#
|
|
61
|
+
# A CALLER HANDS IN AN ALREADY-BOOTED `runtime`, the same division of
|
|
62
|
+
# labor `Facade::CliRunner` already keeps against `bin/run`: booting a
|
|
63
|
+
# domain from a path is IO the calling `bin/` script owns, this stays a
|
|
64
|
+
# pure function of a `Runtime::Dispatcher` plus plain Ruby arguments —
|
|
65
|
+
# no different from `CliRunner.call(runtime:, argv:)` itself.
|
|
66
|
+
# `validate` and `domains` are the two exceptions: `validate`'s whole
|
|
67
|
+
# job is to attempt the boot and report whether it survived, so it
|
|
68
|
+
# takes the domain PATH instead and boots it; `domains` has no
|
|
69
|
+
# domain to be handed one of yet, that's what it's answering.
|
|
70
|
+
#
|
|
71
|
+
# NO NEW VOCABULARY otherwise. Every method here composes doors that
|
|
72
|
+
# already exist — `Projector.call(:cli, ...)` for verb/question alias
|
|
73
|
+
# resolution (the identical table `CliRunner` itself resolves against),
|
|
74
|
+
# `Projector.call(:docs, ...)` for `describe`, `Facade::JsonDoor` for
|
|
75
|
+
# the JSON↔Runtime::Value boundary, `Registry#repository` for `state`/
|
|
76
|
+
# `history`, `Registry#verify!` (run by every boot) for `validate`,
|
|
77
|
+
# `Bluebook::ModelCheck` for `validate(deep: true)`, `Hecks::Behaviors`
|
|
78
|
+
# for `behaviors`, `Adapters::Folder#domain?` for `domains`,
|
|
79
|
+
# `Dispatcher#dry_run?` for `dispatch(dry_run: true)`. A second copy of
|
|
80
|
+
# any of these here would be the exact duplication `Facade::JsonDoor`'s
|
|
81
|
+
# own header already warns against.
|
|
82
|
+
module Storehouse
|
|
83
|
+
module_function
|
|
84
|
+
|
|
85
|
+
# THE SAME CLOSED SET `docs/hecks-survey-what-we-wish-we-had.md`'s
|
|
86
|
+
# `SourceTag` names — WHO dispatched, not WHAT. Optional: a caller
|
|
87
|
+
# that omits it gets `source: nil` recorded, honestly, rather than a
|
|
88
|
+
# guessed default.
|
|
89
|
+
SOURCE_TAGS = %w[process-manager operator hook sidequest-agent cascade daemon].freeze
|
|
90
|
+
|
|
91
|
+
# THE BUS'S OWN AUDIT TRAIL — a JSONL file per domain, one line per
|
|
92
|
+
# `dispatch`/`query`/`state`/dry-run call, `follow` tails it back.
|
|
93
|
+
# `tmp/`, not the domain's own directory: this is the BUS's record
|
|
94
|
+
# of what was asked of it, not part of the domain's own persisted
|
|
95
|
+
# state, and `tmp/` is already gitignored for exactly this kind of
|
|
96
|
+
# local, disposable-but-useful-while-it-lasts file.
|
|
97
|
+
LOG_ROOT = File.expand_path("../../tmp/storehouse", __dir__)
|
|
98
|
+
|
|
99
|
+
# THE ROOT EVERY `domain:`/`under:` MUST RESOLVE UNDER — the project
|
|
100
|
+
# directory by default, `HECKS_STOREHOUSE_ROOT` to widen or move it.
|
|
101
|
+
# `Hecks.boot` `Kernel.load`s the `.hecksagon`/`.bluebook`/`.world`
|
|
102
|
+
# files a domain path resolves to, and those are Ruby, not a data
|
|
103
|
+
# format — a caller-supplied path with no confinement at all is an
|
|
104
|
+
# unmarked door out of "a narrower, checked surface" (the README's
|
|
105
|
+
# own pitch for this bus) and into arbitrary code execution from any
|
|
106
|
+
# path on disk. This is a boundary a local convention was standing
|
|
107
|
+
# in for, not new behavior for a caller already confined to the
|
|
108
|
+
# project tree.
|
|
109
|
+
BOOT_ROOT = File.expand_path(ENV["HECKS_STOREHOUSE_ROOT"] || File.expand_path("../..", __dir__))
|
|
110
|
+
|
|
111
|
+
# REFUSED, NOT SILENTLY CLAMPED — a path outside `BOOT_ROOT` is either
|
|
112
|
+
# an honest mistake (a relative path typed against the wrong cwd) or
|
|
113
|
+
# the exact thing this check exists to catch, and both deserve the
|
|
114
|
+
# same clear refusal rather than a silent rewrite to something the
|
|
115
|
+
# caller didn't ask for.
|
|
116
|
+
def confine!(path, label)
|
|
117
|
+
resolved = File.expand_path(path.to_s, BOOT_ROOT)
|
|
118
|
+
return resolved if resolved == BOOT_ROOT || resolved.start_with?("#{BOOT_ROOT}#{File::SEPARATOR}")
|
|
119
|
+
|
|
120
|
+
raise Runtime::TypeMismatch,
|
|
121
|
+
"#{label}: #{path.inspect} resolves outside #{BOOT_ROOT} — this bus only boots domains under its " \
|
|
122
|
+
"own root (HECKS_STOREHOUSE_ROOT to widen it)"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# ── shared resolution helpers ────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
# THE ONE BLUEBOOK A DOMAIN DIRECTORY BOOTS. Every `bin/*` script that
|
|
128
|
+
# projects a whole-domain CLI or doc set makes this same assumption
|
|
129
|
+
# (`Facade::CliRunner#call`'s own `bluebook = runtime.registry.
|
|
130
|
+
# bluebooks.values.first`) — one `.hecksagon` names one chapter.
|
|
131
|
+
def bluebook_for(runtime)
|
|
132
|
+
runtime.registry.bluebooks.values.first or
|
|
133
|
+
raise Runtime::NotFound, "this boot loaded no bluebook"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def aggregate_ir!(bluebook, name)
|
|
137
|
+
bluebook.aggregate(name) or
|
|
138
|
+
raise Runtime::NotFound, "#{bluebook.name} declares no aggregate named #{name.inspect} — " \
|
|
139
|
+
"known: #{bluebook.aggregates.map(&:hecks_name).sort.join(', ')}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# THE SAME ALIAS TABLE `CliRunner` RESOLVES A TYPED WORD AGAINST — a
|
|
143
|
+
# short name when it's unambiguous, the qualified `Aggregate.Verb`
|
|
144
|
+
# form always. Shared here so `dispatch` and `query` (and their error
|
|
145
|
+
# messages) never drift from what a human typing `bin/run` sees.
|
|
146
|
+
def resolve!(cli, name, asking:)
|
|
147
|
+
pool = asking ? cli[:questions] : cli[:verbs]
|
|
148
|
+
key = cli[:names][asking ? :question : :command][name]
|
|
149
|
+
spec = pool[key]
|
|
150
|
+
return spec if spec
|
|
151
|
+
|
|
152
|
+
known = cli[:names][asking ? :question : :command].keys.sort.join(", ")
|
|
153
|
+
raise Runtime::NotFound, "no such #{asking ? 'query' : 'command'}: #{name.inspect} — known: #{known}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def require_summary!(summary)
|
|
157
|
+
return unless summary.nil? || summary.to_s.strip.empty?
|
|
158
|
+
|
|
159
|
+
raise Runtime::TypeMismatch,
|
|
160
|
+
"a one-line summary: is required on dispatch/query/state — it is what makes an audit row legible later"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def valid_source!(source)
|
|
164
|
+
return if source.nil? || SOURCE_TAGS.include?(source.to_s)
|
|
165
|
+
|
|
166
|
+
raise Runtime::TypeMismatch, "source: #{source.inspect} is not one of #{SOURCE_TAGS.join(', ')}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# `actor_id` NAMES WHO, `role` NAMES WHAT THEY HOLD — `Hecks.
|
|
170
|
+
# as_caller` requires the latter always, the former is additive
|
|
171
|
+
# (`Runtime::Caller::Current`'s own shape). An `actor_id` with no
|
|
172
|
+
# `role` would silently do nothing rather than bind a real caller,
|
|
173
|
+
# which is worse than refusing: a caller who thinks they've
|
|
174
|
+
# identified themselves and haven't deserves to be told.
|
|
175
|
+
def valid_caller!(role, actor_id)
|
|
176
|
+
return unless actor_id && role.nil?
|
|
177
|
+
|
|
178
|
+
raise Runtime::TypeMismatch, "actor_id: requires role: too — a caller names WHO through WHICH role they hold"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# BOUND FOR THE DURATION OF ONE CALL, THEN GONE — `Hecks.as_caller`
|
|
182
|
+
# is itself a `Thread.current`-scoped `ensure`-guarded block, so
|
|
183
|
+
# nothing here needs its own cleanup. `role: nil` yields unbound —
|
|
184
|
+
# for `query`, exactly as before: `CommandRules::Authorization#
|
|
185
|
+
# refuse_role_mismatch` is OPT-IN on the domain side (`return unless
|
|
186
|
+
# caller`), and query authorization runs on a wholly separate
|
|
187
|
+
# mechanism (`authorize policy, tenant: :field`, checked against an
|
|
188
|
+
# explicit `tenant:` argument — see `Runtime::TenantScope`), so
|
|
189
|
+
# binding a caller around a query has no effect on it TODAY; it is
|
|
190
|
+
# still accepted here, for symmetry and for the audit log, against
|
|
191
|
+
# the day a read model does check `Caller.current`. For `dispatch`,
|
|
192
|
+
# `require_caller_for_role_gated!` (below) now refuses BEFORE this
|
|
193
|
+
# is ever reached when the command declares a role and no caller is
|
|
194
|
+
# bound — so an unbound `dispatch` here means either the command
|
|
195
|
+
# declares no role at all, or a caller-side check let it through.
|
|
196
|
+
def with_caller(role, actor_id, &block)
|
|
197
|
+
return block.call if role.nil?
|
|
198
|
+
|
|
199
|
+
Hecks.as_caller(role: role, actor_id: actor_id, &block)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# THE FAIL-OPEN HALF `with_caller` ITSELF CANNOT CLOSE — ADR 0025's
|
|
203
|
+
# Governance RBAC work fixed WHAT a *bound* role is checked against
|
|
204
|
+
# (a live `Governance::RoleAssignment` lookup instead of a bare
|
|
205
|
+
# string match), but changed nothing about a caller who binds no
|
|
206
|
+
# role at all: `refuse_role_mismatch` `return`s immediately when
|
|
207
|
+
# `Caller.current` is nil, so a bus caller who simply omits `role:`
|
|
208
|
+
# sails past a role-gated command unchecked, not denied. That is a
|
|
209
|
+
# property of THIS BUS choosing to dispatch unbound, not of the
|
|
210
|
+
# domain rule — `bin/run`, the human CLI, has no such gap because a
|
|
211
|
+
# human always dispatches through a real `Hecks.as_caller` binding
|
|
212
|
+
# upstream of it. Refusing here, before `with_caller`/`dispatch` are
|
|
213
|
+
# ever reached, makes the bus keep the same promise: a command whose
|
|
214
|
+
# bluebook declares a role is not run through this bus without one.
|
|
215
|
+
def require_caller_for_role_gated!(spec, role)
|
|
216
|
+
return unless spec[:role_gated] && role.nil?
|
|
217
|
+
|
|
218
|
+
raise Runtime::Unauthorized,
|
|
219
|
+
"#{spec[:verb]} requires role: #{spec[:role].inspect} — this command is role-gated and no caller " \
|
|
220
|
+
"(role:/actor_id:) is bound; dispatching it unbound is refused, not silently unchecked"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# `dry_run?` (Runtime::Dispatcher) understands only the OLD flat
|
|
224
|
+
# legacy args shape — no to:/with: envelope, `route:` never passed
|
|
225
|
+
# (its own header explains why: built directly against
|
|
226
|
+
# CommandInterpreter/EntityInterpreter's pre-envelope contract,
|
|
227
|
+
# never updated because nothing else needed it to be — a real
|
|
228
|
+
# record of history, not a defect this bus should paper over
|
|
229
|
+
# silently). `Facade::CommandRequest`/`spec[:legacy_receiver]`
|
|
230
|
+
# already know how to NAME that same flat shape for every receiver
|
|
231
|
+
# kind (a bare id under one string key for :aggregate, a
|
|
232
|
+
# {aggregate:, entity:} pair of keys for :entity) — this is the one
|
|
233
|
+
# door back INTO it.
|
|
234
|
+
def flatten_legacy(envelope, receiver, legacy_receiver)
|
|
235
|
+
facts = envelope[:with] || {}
|
|
236
|
+
return facts unless envelope.key?(:to)
|
|
237
|
+
|
|
238
|
+
route = envelope[:to]
|
|
239
|
+
case receiver
|
|
240
|
+
when :aggregate then facts.merge(legacy_receiver.to_sym => route)
|
|
241
|
+
when :entity
|
|
242
|
+
facts.merge(legacy_receiver.fetch(:aggregate).to_sym => route[:aggregate],
|
|
243
|
+
legacy_receiver.fetch(:entity).to_sym => route[:entity])
|
|
244
|
+
else facts
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# ── the audit log `follow` reads back ───────────────────────────────
|
|
249
|
+
|
|
250
|
+
def log_path(domain_name)
|
|
251
|
+
File.join(LOG_ROOT, "#{domain_name.to_s.gsub(/[^A-Za-z0-9_-]/, '_')}.jsonl")
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# NEVER FAILS A REAL CALL BECAUSE ITS OWN AUDIT LOG COULDN'T BE
|
|
255
|
+
# WRITTEN — a full disk or a permissions problem is a `follow`
|
|
256
|
+
# feature going dark, not a reason to refuse the dispatch/query/
|
|
257
|
+
# state call that was actually asked for.
|
|
258
|
+
def record!(domain_name, tool:, summary:, source:, outcome:, verb: nil, role: nil, actor_id: nil)
|
|
259
|
+
return unless domain_name
|
|
260
|
+
|
|
261
|
+
entry = { time: Time.now.utc.iso8601, tool: tool, verb: verb, summary: summary, source: source,
|
|
262
|
+
role: role, actor_id: actor_id,
|
|
263
|
+
ok: outcome[:ok], id: outcome[:id], error: outcome[:error], events: outcome[:events] }.compact
|
|
264
|
+
FileUtils.mkdir_p(LOG_ROOT)
|
|
265
|
+
File.open(log_path(domain_name), "a") { |f| f.puts(JSON.generate(entry)) }
|
|
266
|
+
rescue StandardError
|
|
267
|
+
nil
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# ── the three that drive it ────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
# `dry_run: true` ANSWERS A DIFFERENT QUESTION than a real dispatch
|
|
273
|
+
# does — "would this succeed", not "here is what happened" — so a
|
|
274
|
+
# domain refusal is the legitimate, complete ANSWER (`ok: true,
|
|
275
|
+
# would_succeed: false`), not a failed call. A malformed request
|
|
276
|
+
# (unknown command, a bad args shape) is still a failed call
|
|
277
|
+
# (`ok: false`) either way — it never reached the domain to be asked.
|
|
278
|
+
def dispatch(runtime:, command:, summary:, args: {}, source: nil, dry_run: false, role: nil, actor_id: nil)
|
|
279
|
+
bluebook = bluebook_for(runtime)
|
|
280
|
+
tool = dry_run ? "dry_run" : "dispatch"
|
|
281
|
+
outcome = perform_dispatch(runtime, bluebook, command, summary, args, source, dry_run, role, actor_id)
|
|
282
|
+
|
|
283
|
+
record!(bluebook.name, tool: tool, verb: outcome[:verb], summary: summary, source: source,
|
|
284
|
+
outcome: outcome, role: role, actor_id: actor_id)
|
|
285
|
+
outcome.except(:verb)
|
|
286
|
+
rescue *refusal_classes => e
|
|
287
|
+
outcome = refused(e, summary: summary)
|
|
288
|
+
record!(bluebook&.name, tool: tool, summary: summary, source: source, outcome: outcome,
|
|
289
|
+
role: role, actor_id: actor_id)
|
|
290
|
+
outcome
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def perform_dispatch(runtime, bluebook, command, summary, args, source, dry_run, role, actor_id)
|
|
294
|
+
require_summary!(summary)
|
|
295
|
+
valid_source!(source)
|
|
296
|
+
valid_caller!(role, actor_id)
|
|
297
|
+
cli = Projector.call(:cli, bluebook: bluebook, options: { program: "mcp" })
|
|
298
|
+
spec = resolve!(cli, command, asking: false)
|
|
299
|
+
require_caller_for_role_gated!(spec, role)
|
|
300
|
+
envelope = Facade::CommandRequest.normalize(Facade::JsonDoor.deep_symbolize(args),
|
|
301
|
+
receiver: spec[:receiver],
|
|
302
|
+
legacy_receiver: spec[:legacy_receiver])
|
|
303
|
+
|
|
304
|
+
result = with_caller(role, actor_id) do
|
|
305
|
+
dry_run ? dry_run_outcome(runtime, spec, envelope, summary: summary) : real_dispatch(runtime, spec, envelope, summary)
|
|
306
|
+
end
|
|
307
|
+
result.merge(verb: spec[:verb])
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def real_dispatch(runtime, spec, envelope, summary)
|
|
311
|
+
result = runtime.dispatch(spec[:verb], **envelope)
|
|
312
|
+
ok(summary: summary,
|
|
313
|
+
id: result.id,
|
|
314
|
+
state: result.state.nil? ? nil : Facade::JsonDoor.materialize(result.state),
|
|
315
|
+
events: result.events.map { |event| { name: event.name, payload: Facade::JsonDoor.materialize(event.payload) } })
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def dry_run_outcome(runtime, spec, envelope, summary:)
|
|
319
|
+
flat = flatten_legacy(envelope, spec[:receiver], spec[:legacy_receiver])
|
|
320
|
+
runtime.dry_run?(spec[:verb], **flat)
|
|
321
|
+
ok(summary: summary, would_succeed: true)
|
|
322
|
+
rescue Runtime::WiringError, *Runtime::DOMAIN_REFUSALS => e
|
|
323
|
+
ok(summary: summary, would_succeed: false, error: e.message)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# ONE CALL, MANY STEPS — the survey's own `bin/run <domain> script`
|
|
327
|
+
# shape, so an agent issuing a known SEQUENCE of commands (open an
|
|
328
|
+
# account, then fund it) pays one round trip instead of N. Every step
|
|
329
|
+
# goes through `dispatch` itself — same resolution, same audit log
|
|
330
|
+
# line per step, same summary/source stamped on all of them since
|
|
331
|
+
# the batch is what the caller is declaring intent about, not each
|
|
332
|
+
# individual step. Runs every step regardless of an earlier one
|
|
333
|
+
# refusing — a later step naming a record an earlier step never
|
|
334
|
+
# created will refuse honestly on its own account, which is more
|
|
335
|
+
# informative than silently dropping the rest of the batch.
|
|
336
|
+
def dispatch_batch(runtime:, steps:, summary:, source: nil, role: nil, actor_id: nil)
|
|
337
|
+
require_summary!(summary)
|
|
338
|
+
results = Array(steps).map do |raw|
|
|
339
|
+
step = Facade::JsonDoor.deep_symbolize(raw)
|
|
340
|
+
dispatch(runtime: runtime, command: step[:command], args: step[:args] || {},
|
|
341
|
+
summary: summary, source: source, role: role, actor_id: actor_id)
|
|
342
|
+
end
|
|
343
|
+
{ ok: results.all? { |r| r[:ok] }, summary: summary, results: results }
|
|
344
|
+
rescue *refusal_classes => e
|
|
345
|
+
refused(e, summary: summary)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def query(runtime:, question:, summary:, args: {}, source: nil, role: nil, actor_id: nil)
|
|
349
|
+
bluebook = bluebook_for(runtime)
|
|
350
|
+
outcome = perform_query(bluebook, runtime, question, summary, args, source, role, actor_id)
|
|
351
|
+
|
|
352
|
+
record!(bluebook.name, tool: "query", verb: outcome[:verb], summary: summary, source: source,
|
|
353
|
+
outcome: outcome, role: role, actor_id: actor_id)
|
|
354
|
+
outcome.except(:verb)
|
|
355
|
+
rescue *refusal_classes => e
|
|
356
|
+
outcome = refused(e, summary: summary)
|
|
357
|
+
record!(bluebook&.name, tool: "query", summary: summary, source: source, outcome: outcome,
|
|
358
|
+
role: role, actor_id: actor_id)
|
|
359
|
+
outcome
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def perform_query(bluebook, runtime, question, summary, args, source, role, actor_id)
|
|
363
|
+
require_summary!(summary)
|
|
364
|
+
valid_source!(source)
|
|
365
|
+
valid_caller!(role, actor_id)
|
|
366
|
+
cli = Projector.call(:cli, bluebook: bluebook, options: { program: "mcp" })
|
|
367
|
+
spec = resolve!(cli, question, asking: true)
|
|
368
|
+
rows = with_caller(role, actor_id) { runtime.query(spec[:verb], **Facade::JsonDoor.deep_symbolize(args)) }
|
|
369
|
+
|
|
370
|
+
ok(summary: summary, rows: rows.map { |row| Facade::JsonDoor.materialize(row) }).merge(verb: spec[:verb])
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# WHAT IS ACTUALLY STORED — no verb, no interpretation, the repository
|
|
374
|
+
# itself. `id:` given answers one record (`NotFound` when it names
|
|
375
|
+
# nothing); omitted answers every record the aggregate currently
|
|
376
|
+
# holds. This is the difference `query` can't cover: a query answers a
|
|
377
|
+
# DECLARED question, and an aggregate that never declared "list
|
|
378
|
+
# everything" has no query this could reuse.
|
|
379
|
+
def state(runtime:, aggregate:, summary:, id: nil, source: nil)
|
|
380
|
+
bluebook = bluebook_for(runtime)
|
|
381
|
+
outcome = perform_state(runtime, bluebook, aggregate, summary, id)
|
|
382
|
+
|
|
383
|
+
record!(bluebook.name, tool: "state", summary: summary, source: source, outcome: outcome)
|
|
384
|
+
outcome
|
|
385
|
+
rescue *refusal_classes => e
|
|
386
|
+
outcome = refused(e, summary: summary)
|
|
387
|
+
record!(bluebook&.name, tool: "state", summary: summary, source: source, outcome: outcome)
|
|
388
|
+
outcome
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def perform_state(runtime, bluebook, aggregate, summary, id)
|
|
392
|
+
require_summary!(summary)
|
|
393
|
+
ir = aggregate_ir!(bluebook, aggregate)
|
|
394
|
+
repository = runtime.registry.repository(bluebook.name, ir)
|
|
395
|
+
|
|
396
|
+
if id
|
|
397
|
+
instance = repository.find(id) or
|
|
398
|
+
raise Runtime::NotFound, "no #{ir.hecks_name} found for id #{id.inspect}"
|
|
399
|
+
ok(summary: summary, record: Facade::JsonDoor.materialize(instance.to_h))
|
|
400
|
+
else
|
|
401
|
+
records = repository.all.map { |instance| Facade::JsonDoor.materialize(instance.to_h) }
|
|
402
|
+
ok(summary: summary, count: records.length, records: records)
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# ── the four zoom levels ─────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
# ZOOM LEVEL ZERO — every domain directory a root actually holds,
|
|
409
|
+
# discovered rather than typed from memory. Every other tool takes
|
|
410
|
+
# `domain:` as a directory it assumes the caller already knows; this
|
|
411
|
+
# is how a caller who doesn't finds out. `Adapters::Folder#domain?`
|
|
412
|
+
# is the same predicate `domain_root`/`nearest_domain` already walk
|
|
413
|
+
# up directories checking — a bare `.hecksagon` or one under
|
|
414
|
+
# `bluebook/`, the two real shapes this corpus uses.
|
|
415
|
+
def domains(under: "examples")
|
|
416
|
+
root = confine!(under, "under")
|
|
417
|
+
return ok(under: under, domains: []) unless Dir.exist?(root)
|
|
418
|
+
|
|
419
|
+
folder = Adapters::Folder.new
|
|
420
|
+
found = Dir.children(root).sort.select { |name| folder.domain?(File.join(root, name)) }
|
|
421
|
+
|
|
422
|
+
ok(under: under, domains: found.map { |name| File.join(under, name) })
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# ZOOM LEVEL ONE — every aggregate this domain declares, and every
|
|
426
|
+
# command/query name each answers to, snake_cased exactly as
|
|
427
|
+
# `dispatch`/`query` want it. Enough to pick a target; `describe` is
|
|
428
|
+
# the next level down for what one of them actually takes.
|
|
429
|
+
def catalog(runtime:)
|
|
430
|
+
bluebook = bluebook_for(runtime)
|
|
431
|
+
|
|
432
|
+
ok(domain: bluebook.name,
|
|
433
|
+
aggregates: bluebook.aggregates.map do |aggregate|
|
|
434
|
+
{ name: aggregate.hecks_name,
|
|
435
|
+
commands: aggregate.commands.map { |c| "#{Naming.snake(c.hecks_name)}!" }.sort,
|
|
436
|
+
queries: aggregate.queries.map { |q| Naming.snake(q.hecks_name) }.sort }
|
|
437
|
+
end)
|
|
438
|
+
rescue *refusal_classes => e
|
|
439
|
+
refused(e)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# ZOOM LEVEL TWO — the exact same usage document a human gets from
|
|
443
|
+
# `bin/docs <domain> [aggregate]` (`Projector::DocsProjector`, the
|
|
444
|
+
# identical projection `Surface::AggregateDoor#docs` calls one door
|
|
445
|
+
# over): every command's arguments, the states it may be issued
|
|
446
|
+
# from, and every way it can refuse. `aggregate:` omitted answers the
|
|
447
|
+
# whole chapter.
|
|
448
|
+
def describe(runtime:, aggregate: nil)
|
|
449
|
+
bluebook = bluebook_for(runtime)
|
|
450
|
+
options = aggregate ? { aggregate: aggregate_ir!(bluebook, aggregate).hecks_name } : {}
|
|
451
|
+
|
|
452
|
+
ok(domain: bluebook.name, docs: Projector.call(:docs, bluebook: bluebook, options: options))
|
|
453
|
+
rescue *refusal_classes => e
|
|
454
|
+
refused(e)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# ZOOM LEVEL THREE — is the wiring sound at all: every bind names a
|
|
458
|
+
# declared aggregate, every adapter satisfies the port it claims, the
|
|
459
|
+
# default adapter is usable. `Registry#verify!` (`runtime/registry/
|
|
460
|
+
# verification.rb`) is the one place this repo already answers that
|
|
461
|
+
# question, and `Runtime::Loader.boot` already calls it as the last
|
|
462
|
+
# step of every boot — so THIS is the one method here that boots for
|
|
463
|
+
# itself rather than taking a `runtime:` already in hand, because a
|
|
464
|
+
# runtime that successfully reached this line already answered the
|
|
465
|
+
# question. Given a domain PATH, not a booted runtime, deliberately:
|
|
466
|
+
# asking "is this valid" about a domain that failed to boot at all
|
|
467
|
+
# has to be askable without a runtime to hand it.
|
|
468
|
+
#
|
|
469
|
+
# `deep: true` GOES PAST WIRING INTO LOGIC — `Bluebook::ModelCheck`,
|
|
470
|
+
# the lightweight-formal-methods leg (dead lifecycle transitions, a
|
|
471
|
+
# saga state no handler chain reaches, a dispatch to nowhere). Opt-in
|
|
472
|
+
# and separate from the base check on purpose: a wiring defect is
|
|
473
|
+
# "this cannot run at all", a model-check finding is "this runs, but
|
|
474
|
+
# part of it can never fire" — two different questions, and the
|
|
475
|
+
# first is far cheaper to ask on every boot.
|
|
476
|
+
#
|
|
477
|
+
# ANY BOOT FAILURE ANSWERS THE QUESTION, not only `WiringError` — a
|
|
478
|
+
# domain path with no `.hecksagon`, a malformed bluebook, is just as
|
|
479
|
+
# much "not valid" as a real wiring mismatch, and this tool exists
|
|
480
|
+
# precisely so none of those ever cross a projection of this bus as
|
|
481
|
+
# a crash.
|
|
482
|
+
def validate(domain:, deep: false)
|
|
483
|
+
runtime = Hecks.boot(confine!(domain, "domain"), install_facade: false)
|
|
484
|
+
result = { ok: true, domain: domain, valid: true }
|
|
485
|
+
|
|
486
|
+
if deep
|
|
487
|
+
require_relative "bluebook/model_check"
|
|
488
|
+
findings = runtime.registry.bluebooks.values.flat_map { |bluebook| Bluebook::ModelCheck.call(bluebook) }
|
|
489
|
+
result[:findings] = findings.map { |f| { kind: f.kind, severity: f.severity, subject: f.subject, message: f.message } }
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
result
|
|
493
|
+
rescue StandardError => e
|
|
494
|
+
{ ok: false, domain: domain, valid: false, error: "#{e.class}: #{e.message}" }
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# ── beyond the zoom levels ──────────────────────────────────────────
|
|
498
|
+
|
|
499
|
+
# THE FULL WRITE HISTORY, not just the current head — `bin/history`'s
|
|
500
|
+
# own logic, unchanged: an append-only-backed aggregate's `entries`,
|
|
501
|
+
# every operation that ever touched it. An aggregate bound to a
|
|
502
|
+
# non-append-only adapter (Memory, Postgres proper) answers an empty
|
|
503
|
+
# list honestly rather than pretending to a history it never kept.
|
|
504
|
+
def history(runtime:)
|
|
505
|
+
bluebook = bluebook_for(runtime)
|
|
506
|
+
entries = bluebook.aggregates.each_with_object({}) do |aggregate, all|
|
|
507
|
+
repository = runtime.registry.repository(bluebook.name, aggregate)
|
|
508
|
+
all[aggregate.storage_name] = journal_entries(repository)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
ok(domain: bluebook.name, history: entries)
|
|
512
|
+
rescue *refusal_classes => e
|
|
513
|
+
refused(e)
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def journal_entries(repository)
|
|
517
|
+
return [] unless repository.is_a?(Ports::Persistence::AppendOnly)
|
|
518
|
+
|
|
519
|
+
repository.entries.map { |entry| { operation: entry.operation, id: entry.id, state: Facade::JsonDoor.materialize(entry.state) } }
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# `.behaviors` FILES, RUN AND REPORTED — hand-curated examples of how
|
|
523
|
+
# to use a domain, in domain vocabulary (`docs/guides/behaviors.md`),
|
|
524
|
+
# the survey's own "honest-refusal", generated-example-suite items.
|
|
525
|
+
# `Hecks::Behaviors` boots each test fresh through `Hecks.boot_files`
|
|
526
|
+
# itself — `target:` names a `.behaviors` file OR a directory to
|
|
527
|
+
# sweep, never a `runtime:`, the one other method here besides
|
|
528
|
+
# `validate` that takes a path instead.
|
|
529
|
+
def behaviors(target:)
|
|
530
|
+
require_relative "behaviors"
|
|
531
|
+
raise Runtime::NotFound, "no such file or directory: #{target.inspect}" unless target && File.exist?(target)
|
|
532
|
+
|
|
533
|
+
if File.directory?(target)
|
|
534
|
+
sweep = Hecks::Behaviors.run_all(target)
|
|
535
|
+
ok(target: target, files: sweep.files.map { |file| behaviors_file(file) }, counts: sweep.summary)
|
|
536
|
+
else
|
|
537
|
+
result = Hecks::Behaviors.run(target)
|
|
538
|
+
ok(target: target, files: [behaviors_file(result)], counts: Hecks::Behaviors.summarize([result]))
|
|
539
|
+
end
|
|
540
|
+
rescue *refusal_classes => e
|
|
541
|
+
refused(e)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def behaviors_file(result)
|
|
545
|
+
{ path: result.path,
|
|
546
|
+
parse_error: result.parse_error,
|
|
547
|
+
runs: Array(result.runs).map { |run| { description: run.description, status: run.status, message: run.message } } }
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# A LIVE TAIL WITHOUT A LIVE PROCESS — `bin/hecks_mcp_door` (its
|
|
551
|
+
# transport of MCP-over-stdio) answers one request at a time, no push
|
|
552
|
+
# channel to a client that only ever asks. This is the honest version
|
|
553
|
+
# of the survey's `storehouse follow` for that shape: not a
|
|
554
|
+
# subscription, a durable JSONL log every `dispatch`/`query`/`state`/
|
|
555
|
+
# dry-run call appends to (`record!`), tailed back here. Still real,
|
|
556
|
+
# still cross-process — the log outlives any one door's own process —
|
|
557
|
+
# just pull instead of push.
|
|
558
|
+
def follow(runtime:, limit: 20)
|
|
559
|
+
bluebook = bluebook_for(runtime)
|
|
560
|
+
entries = log_lines(bluebook.name).last([limit.to_i, 1].max)
|
|
561
|
+
|
|
562
|
+
ok(domain: bluebook.name, entries: entries)
|
|
563
|
+
rescue *refusal_classes => e
|
|
564
|
+
refused(e)
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def log_lines(domain_name)
|
|
568
|
+
path = log_path(domain_name)
|
|
569
|
+
return [] unless File.exist?(path)
|
|
570
|
+
|
|
571
|
+
File.readlines(path).map { |line| JSON.parse(line, symbolize_names: true) }
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# WHAT ACTUALLY HAPPENED, with payloads — distinct from `state`
|
|
575
|
+
# (what's stored NOW) and `history` (append-only operation
|
|
576
|
+
# SNAPSHOTS, no payload). NOT a domain-wide event-sourcing replay:
|
|
577
|
+
# "one boot per call" means `runtime.events` is always empty except
|
|
578
|
+
# during the very call that populated it, discarded the moment that
|
|
579
|
+
# call returns — there is no cross-call in-memory log to read here.
|
|
580
|
+
# So this reads the SAME durable audit log `follow` already tails
|
|
581
|
+
# (`record!` now stamps a successful dispatch's own announced
|
|
582
|
+
# events onto its log line), reshaped: `follow` answers "what was
|
|
583
|
+
# CALLED, in order, across every tool"; this answers "what HAPPENED
|
|
584
|
+
# to one aggregate/record" — events THIS BUS witnessed, which is
|
|
585
|
+
# every real dispatch ever routed through it, but no more than that.
|
|
586
|
+
# `aggregate:` narrows to one aggregate; `id:` (requires
|
|
587
|
+
# `aggregate:` — an id alone is not unique across aggregates)
|
|
588
|
+
# narrows to one record's own events.
|
|
589
|
+
def events(runtime:, aggregate: nil, id: nil, limit: nil)
|
|
590
|
+
raise Runtime::TypeMismatch, "id: requires aggregate: too — an id alone is not unique across aggregates" if id && !aggregate
|
|
591
|
+
|
|
592
|
+
bluebook = bluebook_for(runtime)
|
|
593
|
+
fqn = aggregate ? "#{bluebook.name}::#{aggregate_ir!(bluebook, aggregate).hecks_name}" : nil
|
|
594
|
+
|
|
595
|
+
found = log_lines(bluebook.name).filter_map do |entry|
|
|
596
|
+
next unless entry[:tool] == "dispatch" && entry[:ok] && entry[:events]
|
|
597
|
+
next if fqn && !entry[:verb].to_s.start_with?("#{fqn}.")
|
|
598
|
+
next if id && entry[:id] != id
|
|
599
|
+
|
|
600
|
+
entry[:events].map { |event| event.merge(time: entry[:time], verb: entry[:verb], id: entry[:id]) }
|
|
601
|
+
end.flatten(1)
|
|
602
|
+
|
|
603
|
+
found = found.last(limit.to_i) if limit
|
|
604
|
+
|
|
605
|
+
ok(domain: bluebook.name, events: found)
|
|
606
|
+
rescue *refusal_classes => e
|
|
607
|
+
refused(e)
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# ── shared shape ────────────────────────────────────────────────────
|
|
611
|
+
|
|
612
|
+
# `Runtime::WiringError` BELONGS HERE TOO, alongside the true domain
|
|
613
|
+
# refusals — not because it IS one (it's a structural defect, not a
|
|
614
|
+
# rule the caller broke), but because "this domain isn't wired to
|
|
615
|
+
# answer what you're asking" (a `role:`+`actor_id:` caller reaching
|
|
616
|
+
# an authorization port nothing implements, a dry run against a
|
|
617
|
+
# port verb) is exactly the shape this bus promises never crashes
|
|
618
|
+
# through it. `dry_run_outcome` already treats it this way locally;
|
|
619
|
+
# this makes every OTHER caller of `refusal_classes` do the same.
|
|
620
|
+
def refusal_classes = [Runtime::NotFound, Runtime::TypeMismatch, Runtime::WiringError, *Runtime::DOMAIN_REFUSALS]
|
|
621
|
+
|
|
622
|
+
def ok(**fields) = { ok: true }.merge(fields)
|
|
623
|
+
|
|
624
|
+
# AN HONEST REFUSAL, NOT A CRASH — the survey's own item #9: "an
|
|
625
|
+
# explicit, structured refusal a caller can act on" rather than a
|
|
626
|
+
# stack trace an agent has to parse to find the one line that
|
|
627
|
+
# mattered. The domain's own refusal text travels verbatim
|
|
628
|
+
# (`RefusalWording` already renders every one of these to be read),
|
|
629
|
+
# this only wraps it consistently.
|
|
630
|
+
def refused(error, summary: nil) = { ok: false, summary: summary, error: error.message }
|
|
631
|
+
end
|
|
632
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
# ISOLATED, ON PURPOSE — the gemspec reads this file directly rather
|
|
3
|
+
# than `require_relative "lib/hecks"` (the whole framework),
|
|
4
|
+
# specifically so evaluating the gemspec never triggers ANY of this
|
|
5
|
+
# gem's own dependencies (prism among them) before Bundler has even
|
|
6
|
+
# resolved what to install. A real, live chicken-and-egg bug caught
|
|
7
|
+
# deploying to a Ruby version that doesn't bundle prism for free
|
|
8
|
+
# (AWS Lambda's ruby3.2): the gemspec's own `require_relative
|
|
9
|
+
# "lib/hecks"` pulled in adapters/driven/prism.rb's
|
|
10
|
+
# unconditional `require "prism"`, which failed before Bundler ever
|
|
11
|
+
# read what the gemspec itself declared as a dependency — declaring
|
|
12
|
+
# it there, or even in the consuming Gemfile, never closed the gap,
|
|
13
|
+
# because gemspec evaluation happens before anything Bundler
|
|
14
|
+
# resolves is actually loadable yet.
|
|
15
|
+
VERSION = "1.0.0"
|
|
16
|
+
end
|