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
data/lib/hecks/naming.rb
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Naming
|
|
3
|
+
# WHAT SEPARATES THE PARTS OF A DERIVED IDENTITY.
|
|
4
|
+
#
|
|
5
|
+
# An identity of several parts is their JOIN, and the join has to be spelled the
|
|
6
|
+
# same everywhere or two readers name two different records off one declaration.
|
|
7
|
+
# It was spelled three ways at once here — "::" for an aggregate under a
|
|
8
|
+
# chapter, "." for everything under an aggregate, "#" for the three that keyed
|
|
9
|
+
# off position — and each was written where it happened to be needed. Once the
|
|
10
|
+
# runtime derived the same identity, the runtime made a FOURTH, and a reference
|
|
11
|
+
# that resolved by string comparison found nothing.
|
|
12
|
+
IDENTITY_JOIN = ":".freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# The parts of an identity, joined in declaration order.
|
|
17
|
+
def identity(parts) = Array(parts).join(IDENTITY_JOIN)
|
|
18
|
+
|
|
19
|
+
def demodulise(type)
|
|
20
|
+
type.to_s.split("::").last.to_s
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# snake_case -> PascalCase. The name a synthesised closed-set value object
|
|
24
|
+
# takes when an attribute declares one inline. The derivation is part of
|
|
25
|
+
# the IR contract: the same bluebook must always produce the same name.
|
|
26
|
+
def pascal(text)
|
|
27
|
+
text.to_s.split("_").map { |part| part.sub(/\A(.)/) { Regexp.last_match(1).upcase } }.join
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def snake(text)
|
|
31
|
+
text.to_s
|
|
32
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
33
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
34
|
+
.downcase
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The name a COLLECTION of something takes.
|
|
38
|
+
#
|
|
39
|
+
# There were two of these and one was wrong. A read model's gathered heads
|
|
40
|
+
# derived their name with a bare `"#{snake(target)}s"`, so the meta-domain's
|
|
41
|
+
# own whole-bluebook read model handed back `querys`, `entitys`, `policys`
|
|
42
|
+
# and `dispatchs` — and every check was green, because the checks compared
|
|
43
|
+
# the wrong rule against itself. Agreement is not correctness; it never was.
|
|
44
|
+
#
|
|
45
|
+
# So: one pluraliser, three rules, and every collection name flows through it.
|
|
46
|
+
def plural(text)
|
|
47
|
+
word = text.to_s
|
|
48
|
+
return "#{word[0..-2]}ies" if word.match?(/[^aeiou]y\z/)
|
|
49
|
+
return "#{word}es" if word.match?(/(s|x|z|ch|sh)\z/)
|
|
50
|
+
|
|
51
|
+
"#{word}s"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# `has_many`'s undo — the plural WRITTEN, back to the singular the target
|
|
55
|
+
# aggregate is actually named. Deliberately the crude half of a pair: `plural`
|
|
56
|
+
# above earns its precision (three suffix rules) because getting a COLLECTION
|
|
57
|
+
# name wrong reads as a typo forever ; this only ever recovers a name someone
|
|
58
|
+
# already wrote as a real aggregate, so "ies -> y, trailing s dropped" is the
|
|
59
|
+
# whole rule — enough for `has_many Invoices` to resolve to the aggregate
|
|
60
|
+
# actually named Invoice.
|
|
61
|
+
def singularize(text)
|
|
62
|
+
word = text.to_s
|
|
63
|
+
return "#{word[0..-4]}y" if word.length > 3 && word.end_with?("ies")
|
|
64
|
+
|
|
65
|
+
# `plural`'s OWN second rule adds "es" (not bare "s") after
|
|
66
|
+
# s/x/z/ch/sh — undone here the same way, or a word `plural`
|
|
67
|
+
# itself would have suffixed with "es" comes back missing its
|
|
68
|
+
# own trailing letter ("Boxes" -> "Boxe", not "Box") once this
|
|
69
|
+
# only ever knew how to drop a bare "s". Checked BEFORE the
|
|
70
|
+
# bare-"s" rule below: stripping "es" first and confirming what
|
|
71
|
+
# is left actually ends in one of those five shapes is what
|
|
72
|
+
# keeps an ordinary "-es" word (e.g. "Invoices" -> "Invoice")
|
|
73
|
+
# from also losing a letter it never doubled.
|
|
74
|
+
if word.length > 3 && word.end_with?("es") && word[0..-3].match?(/(s|x|z|ch|sh)\z/)
|
|
75
|
+
return word[0..-3]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
return word[0..-2] if word.length > 1 && word.end_with?("s")
|
|
79
|
+
|
|
80
|
+
word
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def reference_key(type)
|
|
84
|
+
snake(demodulise(type)).to_sym
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def split_dotted(dotted)
|
|
88
|
+
first, second = dotted.to_s.split(".", 2)
|
|
89
|
+
[first.to_s, second.to_s]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def qualifier(dotted)
|
|
93
|
+
text = dotted.to_s
|
|
94
|
+
text.include?(".") ? text.split(".", 2).first : nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def unqualified(dotted)
|
|
98
|
+
text = dotted.to_s
|
|
99
|
+
text.include?(".") ? text.split(".", 2).last : text
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def split_verb(verb)
|
|
103
|
+
path, command = verb.to_s.split(".", 2)
|
|
104
|
+
domain, aggregate = path.to_s.split("::", 2)
|
|
105
|
+
return nil unless domain && aggregate && command
|
|
106
|
+
|
|
107
|
+
[domain, aggregate, command]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# `trigger Account::Debit` / `dispatch Account::Debit` — a bare
|
|
111
|
+
# CONSTANT reference (`ConstShim`'s own `ScopedConstant`, S0b), not
|
|
112
|
+
# text (ADR 0025, "events and reactions" — command references become
|
|
113
|
+
# first-class). Ruby's `::` joins EVERY segment the same way a
|
|
114
|
+
# constant path always does, but a command's own `hecks_fqn` joins
|
|
115
|
+
# its aggregate with `.` (`Construct#hecks_separator`'s default,
|
|
116
|
+
# only an AGGREGATE overrides it to `::`) — so only the LAST `::`
|
|
117
|
+
# becomes a `.`; everything before it (the chapter, when a domain is
|
|
118
|
+
# spelled at all: `Banking::Account::Debit`) stays `::`-joined.
|
|
119
|
+
#
|
|
120
|
+
# A STRING PASSES THROUGH UNCHANGED, on purpose — legacy era text
|
|
121
|
+
# (S0a's own shadow-parsed spelling) already mixes `::` (domain) and
|
|
122
|
+
# `.` (command) correctly on its own, e.g. `"Banking::Account.Debit"`,
|
|
123
|
+
# and re-splitting that by content rather than by TYPE would corrupt
|
|
124
|
+
# it (its own last `::` sits between the domain and the aggregate,
|
|
125
|
+
# not the aggregate and the command). Only an actual constant object
|
|
126
|
+
# — never seen holding a `.` of its own — needs the rewrite at all.
|
|
127
|
+
def command_ref(value)
|
|
128
|
+
return value.to_s if value.is_a?(::String) || value.is_a?(::Symbol)
|
|
129
|
+
|
|
130
|
+
text = value.to_s
|
|
131
|
+
path, _, command = text.rpartition("::")
|
|
132
|
+
path.empty? ? text : "#{path}.#{command}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# `emits Account::AccountFrozen` / `on Account::AccountFrozen` — the
|
|
136
|
+
# event-side twin of `command_ref`, above (ADR 0025, S6 — "events
|
|
137
|
+
# first-class"). Identical transform (a bare `ScopedConstant`'s last
|
|
138
|
+
# `::` becomes `.`, a String passes through unchanged for legacy
|
|
139
|
+
# `shadow_parse` text and for corpus sites this pass didn't migrate —
|
|
140
|
+
# see that method's own comment for why both rules exist), given its
|
|
141
|
+
# own name because the two references mean different things even
|
|
142
|
+
# though the rewrite is byte-identical: an event name is not a
|
|
143
|
+
# command name that happens to share a format.
|
|
144
|
+
def event_ref(value) = command_ref(value)
|
|
145
|
+
|
|
146
|
+
# `transition Account::AccountDebited => "state"` / `starts_on
|
|
147
|
+
# Transfer::TransferRequested` / `ends_on Transfer::TransferSettled`
|
|
148
|
+
# — a process manager's OWN event references (ADR 0025, S6),
|
|
149
|
+
# DELIBERATELY NOT `event_ref` — found live, not assumed, wiring a
|
|
150
|
+
# real migrated corpus site into `bin/model_check` for the first
|
|
151
|
+
# time (2026-08-28): `SagaInterpreter#begin_saga`/`#advance_saga`
|
|
152
|
+
# match `pm.starts_on`/`pm.handler_for` against `event.name`, which
|
|
153
|
+
# `CommandRules::Emission#emit` stamps BARE — a command's own
|
|
154
|
+
# `emits AccountDebited` never carries its aggregate's name at all
|
|
155
|
+
# (unlike a policy's cross-aggregate `on`, matched instead by
|
|
156
|
+
# `Naming.demodulise(event.aggregate)` split apart from the bare
|
|
157
|
+
# name — `PolicyInterpreter#policies_for`). Handing a saga's own
|
|
158
|
+
# matcher the DOTTED `event_ref` form ("Account.AccountDebited")
|
|
159
|
+
# would silently name an event no command in the domain ever
|
|
160
|
+
# actually emits — caught by `bin/model_check`'s own `deaf_handler`/
|
|
161
|
+
# `deaf_trigger` findings the moment a real qualified corpus site
|
|
162
|
+
# existed to trip them, not by any unit test in isolation.
|
|
163
|
+
#
|
|
164
|
+
# A qualifier is still worth WRITING (`Account::`) — the same
|
|
165
|
+
# provenance a reader gets from `trigger Account::Debit` — it is
|
|
166
|
+
# only not worth KEEPING: `demodulise` drops everything but the
|
|
167
|
+
# final segment, so `Account::AccountDebited` and a bare
|
|
168
|
+
# `AccountDebited` resolve to the identical stored string. A String
|
|
169
|
+
# passes through unchanged either way, exactly like `command_ref`'s
|
|
170
|
+
# own legacy branch — this corpus never spelled one dotted to begin
|
|
171
|
+
# with, so there is nothing here to strip.
|
|
172
|
+
def event_name_ref(value) = demodulise(value)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require_relative "../runtime/registry"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Ports
|
|
5
|
+
# WHO CAN SIGN IN, AND WHAT ROLE DO THEY HOLD — the domain-specific
|
|
6
|
+
# half of sign-in `Ports::Authentication` deliberately stays out of
|
|
7
|
+
# (see that port's own header: it only ever talks to the external
|
|
8
|
+
# provider, never this registry's own data). Resolved the same way
|
|
9
|
+
# every other port here resolves its adapter: one adapter
|
|
10
|
+
# registry-wide answers this, since an app has no reason to want a
|
|
11
|
+
# different "who's allowed in" source per aggregate.
|
|
12
|
+
#
|
|
13
|
+
# Every domain shapes this differently — who its own "a person who
|
|
14
|
+
# can sign in" aggregate is, what admits them, what grants them
|
|
15
|
+
# access — so this port is pure delegation, same as every sibling
|
|
16
|
+
# port. The one piece of this that IS generic (a live Governance
|
|
17
|
+
# grant should win over whatever an aggregate's own role field
|
|
18
|
+
# says) lives on `Ports::Authorization#live_role_for` instead —
|
|
19
|
+
# a domain's adapter here calls that directly, rather than this
|
|
20
|
+
# port re-deriving Governance-query logic another port already owns.
|
|
21
|
+
module AccessControl
|
|
22
|
+
NAME = "access_control"
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def session_for_identity(registry, identity_id:)
|
|
27
|
+
adapter(registry).session_for_identity(registry, identity_id: identity_id)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def provision(registry, email:, issuer:, subject:)
|
|
31
|
+
adapter(registry).provision(registry, email: email, issuer: issuer, subject: subject)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def available_roles(registry)
|
|
35
|
+
adapter(registry).available_roles(registry)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def grant(registry, email:, role:)
|
|
39
|
+
adapter(registry).grant(registry, email: email, role: role)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def all_people(registry)
|
|
43
|
+
adapter(registry).all_people(registry)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def adapter(registry)
|
|
47
|
+
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
48
|
+
|
|
49
|
+
case implementations.size
|
|
50
|
+
when 1 then registry.adapter_class(implementations.first.name)
|
|
51
|
+
when 0
|
|
52
|
+
raise Runtime::WiringError,
|
|
53
|
+
"no adapter implements the #{NAME} port — nothing can answer who's allowed in"
|
|
54
|
+
else
|
|
55
|
+
raise Runtime::WiringError,
|
|
56
|
+
"#{implementations.size} adapters implement the #{NAME} port " \
|
|
57
|
+
"(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module Hecks
|
|
2
|
+
module Ports
|
|
3
|
+
module Agent
|
|
4
|
+
# WHERE A RAW HASH BECOMES A STRUCT — the one place, so every
|
|
5
|
+
# adapter (the real `claude_code` one, and any scripted double
|
|
6
|
+
# standing in for it in a spec) is held to the identical shape.
|
|
7
|
+
# An adapter's whole job ends at "here is what came back, already
|
|
8
|
+
# JSON"; whether that hash is USABLE is decided once, here, not
|
|
9
|
+
# re-decided per adapter.
|
|
10
|
+
#
|
|
11
|
+
# Every failure raises `ValidationError` — never lets a bare
|
|
12
|
+
# `NoMethodError` on nil, or a `KeyError`, reach a caller looking
|
|
13
|
+
# for an agent-shaped failure. `bin/interview` catches exactly one
|
|
14
|
+
# class for "the model answered badly," not a grab-bag of Ruby's
|
|
15
|
+
# own.
|
|
16
|
+
module Answers
|
|
17
|
+
VERB_PATTERN = /\A[A-Za-z]+::[A-Za-z]+\.[A-Za-z]+\z/
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def questions(raw)
|
|
22
|
+
rows(raw, "questions").map { |row| Question.new(text: text!(row, "text"), because: text!(row, "because")) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def proposals(raw)
|
|
26
|
+
rows(raw, "proposals").map do |row|
|
|
27
|
+
Proposal.new(verb: verb!(row), rationale: text!(row, "rationale"), arguments: arguments!(row))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def findings(raw)
|
|
32
|
+
rows(raw, "findings").map do |row|
|
|
33
|
+
Finding.new(kind: kind!(row), severity: severity!(row), subject: text!(row, "subject"),
|
|
34
|
+
message: text!(row, "message"))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def suggestions(raw)
|
|
39
|
+
rows(raw, "names").map do |row|
|
|
40
|
+
Suggestion.new(name: text!(row, "name"), because: text!(row, "because"),
|
|
41
|
+
rejected: Array(row["rejected"]).map(&:to_s))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# ── shared checks, each named for what it refuses ───────────────
|
|
46
|
+
|
|
47
|
+
def rows(raw, key)
|
|
48
|
+
raise ValidationError, "expected a Hash back, got #{raw.class}: #{raw.inspect}" unless raw.is_a?(Hash)
|
|
49
|
+
|
|
50
|
+
value = raw[key]
|
|
51
|
+
return value if value.is_a?(Array)
|
|
52
|
+
|
|
53
|
+
raise ValidationError, "no #{key.inspect} array in the answer: #{raw.inspect}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def text!(row, key)
|
|
57
|
+
value = row[key]
|
|
58
|
+
return value.to_s if value && !value.to_s.strip.empty?
|
|
59
|
+
|
|
60
|
+
raise ValidationError, "#{key.inspect} missing or blank in #{row.inspect}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# A VERB THE LANGUAGE COULD NOT EVEN PARSE IS AN ADAPTER FAULT,
|
|
64
|
+
# refused right here — the same `not_fully_qualified` shape the
|
|
65
|
+
# language's own grammar already refuses by, reused rather than
|
|
66
|
+
# reinvented. A verb naming a real category that turns out to
|
|
67
|
+
# describe the wrong fact is NOT this port's business: that one
|
|
68
|
+
# dispatches, and `Interview::Session#offer` is what says no.
|
|
69
|
+
def verb!(row)
|
|
70
|
+
verb = row["verb"].to_s
|
|
71
|
+
return verb if VERB_PATTERN.match?(verb)
|
|
72
|
+
|
|
73
|
+
raise ValidationError, "#{verb.inspect} is not a fully-qualified verb (Chapter::Aggregate.Command)"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def kind!(row)
|
|
77
|
+
kind = row["kind"].to_s.to_sym
|
|
78
|
+
return kind if CRITIQUE_KINDS.include?(kind)
|
|
79
|
+
|
|
80
|
+
raise ValidationError, "#{kind.inspect} is not a critique kind this port knows (#{CRITIQUE_KINDS.join(', ')})"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def severity!(row)
|
|
84
|
+
severity = row["severity"].to_s.to_sym
|
|
85
|
+
return severity if SEVERITIES.include?(severity)
|
|
86
|
+
|
|
87
|
+
raise ValidationError, "#{severity.inspect} is not a severity this port knows (#{SEVERITIES.join(', ')})"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# ROWS SHAPED EXACTLY AS `Interview::Proposal::Argument` — a
|
|
91
|
+
# reference's `field` is legitimately blank (a reference IS an
|
|
92
|
+
# id), so only `name` is required here.
|
|
93
|
+
def arguments!(row)
|
|
94
|
+
Array(row["arguments"]).map do |argument|
|
|
95
|
+
name = argument["name"].to_s
|
|
96
|
+
raise ValidationError, "argument row has no name: #{argument.inspect}" if name.empty?
|
|
97
|
+
|
|
98
|
+
{ name: name, field: argument["field"].to_s, value: argument["value"].to_s }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
require_relative "../runtime/registry"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Ports
|
|
5
|
+
# THE INTERVIEWER — whatever asks the next good question, reads a
|
|
6
|
+
# sentence back into proposed declarations, judges a model on taste
|
|
7
|
+
# rather than structure, or suggests a name. Resolved the singleton
|
|
8
|
+
# way every other port here is (`Ports::IdentityGeneration`'s own
|
|
9
|
+
# zero/one/many — one interviewer per process, never per-aggregate),
|
|
10
|
+
# because a second adapter answering this port is exactly as
|
|
11
|
+
# unchoosable as a second one minting identities.
|
|
12
|
+
#
|
|
13
|
+
# BUILT LAST, ON PURPOSE, AND STILL OPTIONAL. `.claude/skills/
|
|
14
|
+
# interview/SKILL.md` conducts a whole session with NONE of this —
|
|
15
|
+
# Claude Code reads `bin/interview state` itself, asks the human
|
|
16
|
+
# directly with real conversation context, and calls `bin/interview
|
|
17
|
+
# ask`/`propose`/`accept` itself. This port exists for what that
|
|
18
|
+
# cannot reach: a HEADLESS run (`bin/interview suggest`, no model in
|
|
19
|
+
# the room otherwise), a batch `critique` over an already-written
|
|
20
|
+
# chapter, and — the actual point of building it at all — a
|
|
21
|
+
# SCRIPTED double (`spec/fixtures/scripted_agent.rb`) that makes the
|
|
22
|
+
# whole loop testable with no real model involved.
|
|
23
|
+
#
|
|
24
|
+
# WHAT THIS PORT IS NOT. It does not judge whether a declaration is
|
|
25
|
+
# WELL-FORMED — that is the meta-domain's own job, answered by a real
|
|
26
|
+
# dispatch refusing or not (`Interview::Session#offer`). This port
|
|
27
|
+
# only ever produces SHAPE: a question, a proposed declaration, a
|
|
28
|
+
# judgement about taste, a name. A proposal naming a category the
|
|
29
|
+
# language does not declare is an ADAPTER fault, refused HERE, in
|
|
30
|
+
# `Answers`; a proposal naming a real category but describing the
|
|
31
|
+
# wrong domain fact is dispatched anyway, so the LANGUAGE gets to say
|
|
32
|
+
# why. That line is the whole design.
|
|
33
|
+
#
|
|
34
|
+
# PARSING BELONGS TO THIS FILE, NOT THE ADAPTER. An adapter answers
|
|
35
|
+
# with a plain, already-JSON-shaped Hash (a real model's parsed
|
|
36
|
+
# reply, or a spec double's own hand-built one) — never a Struct —
|
|
37
|
+
# so every adapter is validated identically here rather than trusting
|
|
38
|
+
# each one to refuse the same way. Two adapters that parsed
|
|
39
|
+
# differently would mean the SAME malformed answer passing through
|
|
40
|
+
# one and refusing through the other.
|
|
41
|
+
module Agent
|
|
42
|
+
NAME = "agent"
|
|
43
|
+
|
|
44
|
+
# The answer came back and could not be used — no JSON in it, a
|
|
45
|
+
# category the language does not declare, a severity outside the
|
|
46
|
+
# two that exist. A foreign failure (JSON::ParserError, a missing
|
|
47
|
+
# key) wrapped into something this port owns, the same shape
|
|
48
|
+
# `Ports::Authentication::ValidationError` already is, so no
|
|
49
|
+
# caller ever rescues a raw parser error leaking out of an
|
|
50
|
+
# adapter.
|
|
51
|
+
ValidationError = Class.new(StandardError)
|
|
52
|
+
|
|
53
|
+
# Nothing answered at all — the binary is missing, the subprocess
|
|
54
|
+
# died, the call timed out. Separate from ValidationError on
|
|
55
|
+
# purpose: unavailable means retry or fall back to asking the
|
|
56
|
+
# human directly; malformed means the PROMPT is wrong, not the
|
|
57
|
+
# transport, and retrying identically will not fix it.
|
|
58
|
+
Unavailable = Class.new(StandardError)
|
|
59
|
+
|
|
60
|
+
# ── what the four operations hand back ──────────────────────────
|
|
61
|
+
#
|
|
62
|
+
# Structs, not hashes, for the reason `Bluebook::ModelCheck::Finding`
|
|
63
|
+
# already is one: a `keyword_init` Struct raises on a key nobody
|
|
64
|
+
# declared, which turns "the model invented a field" into a
|
|
65
|
+
# failure at the boundary rather than a nil three calls in.
|
|
66
|
+
|
|
67
|
+
# ONE QUESTION. `because` is the reasoning quoted back so a human
|
|
68
|
+
# can see it, not decoration — the same "show your work" the
|
|
69
|
+
# language's own refusal messages already practice.
|
|
70
|
+
Question = Struct.new(:text, :because, keyword_init: true)
|
|
71
|
+
|
|
72
|
+
# ONE PROPOSED DECLARATION, already shaped as
|
|
73
|
+
# `Interview::Proposal`'s own `Argument` rows — `{name:, field:,
|
|
74
|
+
# value:}`, the exact triple `bin/interview propose --arg
|
|
75
|
+
# name:field:value` already takes and `Interview::Lowering`
|
|
76
|
+
# already knows how to lower. Nothing here decides HOW to address
|
|
77
|
+
# a record or whether a value is bare or wrapped — that was
|
|
78
|
+
# already the proposal-writer's job in `interview.bluebook`'s own
|
|
79
|
+
# design (see `Argument`'s own comment there); this only adds the
|
|
80
|
+
# PROSE reasoning a human typed does not carry on its own.
|
|
81
|
+
Proposal = Struct.new(:verb, :arguments, :rationale, keyword_init: true)
|
|
82
|
+
|
|
83
|
+
# CRITIQUE REUSES `Bluebook::ModelCheck::Finding`'S OWN SHAPE —
|
|
84
|
+
# same fields, same severities, so a mechanical finding
|
|
85
|
+
# (`Session#gaps`) and a judgement (this) print in one list and
|
|
86
|
+
# sort together. The KIND vocabulary is NOT shared — ModelCheck's
|
|
87
|
+
# eleven kinds are structural facts about a graph; these are
|
|
88
|
+
# opinions about a model, closed here on purpose (an open kind
|
|
89
|
+
# field is an open prompt, and an open prompt drifts).
|
|
90
|
+
CRITIQUE_KINDS = %i[
|
|
91
|
+
anemic_aggregate wrong_boundary crud_verb leaky_value_object
|
|
92
|
+
missing_lifecycle missing_invariant ubiquitous_language
|
|
93
|
+
overreaching_identity untold_rule
|
|
94
|
+
].freeze
|
|
95
|
+
SEVERITIES = %i[error warning].freeze
|
|
96
|
+
|
|
97
|
+
Finding = Struct.new(:kind, :severity, :subject, :message, keyword_init: true) do
|
|
98
|
+
def to_s = "#{severity.to_s.upcase.ljust(7)} #{kind.to_s.ljust(20)} #{subject} — #{message}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# ONE SUGGESTED NAME. `rejected` carries the near-misses and why
|
|
102
|
+
# they were passed over — free at the point the model is choosing
|
|
103
|
+
# anyway, and the part a human actually learns from.
|
|
104
|
+
Suggestion = Struct.new(:name, :because, :rejected, keyword_init: true)
|
|
105
|
+
|
|
106
|
+
module_function
|
|
107
|
+
|
|
108
|
+
# THE NEXT BEST QUESTION. `state` is the whole picture — normally
|
|
109
|
+
# `Interview::Session#declaration` plus `#gaps`, so an adapter
|
|
110
|
+
# needs no memory of its own between calls; that is what lets a
|
|
111
|
+
# headless run be a series of one-shot processes, same as
|
|
112
|
+
# `bin/interview` itself already is.
|
|
113
|
+
def ask(registry, state:, asked: [])
|
|
114
|
+
Answers.questions(adapter(registry).ask(state: state, asked: asked))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# PROSE -> PROPOSED DECLARATIONS. Returns `[]` when the sentence
|
|
118
|
+
# carried no declaration at all (a clarifying question back from
|
|
119
|
+
# the human, say) — a legitimate answer, not a failure.
|
|
120
|
+
def interpret(registry, prose:, state:)
|
|
121
|
+
Answers.proposals(adapter(registry).interpret(prose: prose, state: state))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# WHAT IS WRONG WITH THIS AS A MODEL — handed everything already
|
|
125
|
+
# known (the language's own refusals, `Session#gaps`'s mechanical
|
|
126
|
+
# findings) so it spends its judgement on what neither of those
|
|
127
|
+
# can see, rather than restating them.
|
|
128
|
+
def critique(registry, declared:, refusals: [], findings: [])
|
|
129
|
+
Answers.findings(adapter(registry).critique(declared: declared, refusals: refusals, findings: findings))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# VOCABULARY HELP. `near` is what the chapter already calls
|
|
133
|
+
# things, so a suggestion cannot collide with a name in use.
|
|
134
|
+
#
|
|
135
|
+
# NAMED `suggest_name`, NOT `name` — the plan's own word for this
|
|
136
|
+
# operation, but `Module#name` already exists and is load-bearing
|
|
137
|
+
# everywhere (backtraces, RSpec's own description building,
|
|
138
|
+
# `inspect`): a module-function called `name` SHADOWS it the
|
|
139
|
+
# instant it's defined, and `SomeModule.name` (no args) then
|
|
140
|
+
# raises `ArgumentError: missing keywords` the next time anything
|
|
141
|
+
# — not this file, something else entirely — asks the module its
|
|
142
|
+
# own name. Measured, not theoretical: this exact collision broke
|
|
143
|
+
# RSpec's own failure-message formatting the first time it was
|
|
144
|
+
# named `name` here.
|
|
145
|
+
def suggest_name(registry, meaning:, kind:, near: [])
|
|
146
|
+
Answers.suggestions(adapter(registry).suggest_name(meaning: meaning, kind: kind, near: near))
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def adapter(registry)
|
|
150
|
+
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
151
|
+
|
|
152
|
+
case implementations.size
|
|
153
|
+
when 1 then registry.adapter_class(implementations.first.name)
|
|
154
|
+
when 0
|
|
155
|
+
raise Runtime::WiringError,
|
|
156
|
+
"no adapter implements the #{NAME} port — nothing can conduct an interview"
|
|
157
|
+
else
|
|
158
|
+
raise Runtime::WiringError,
|
|
159
|
+
"#{implementations.size} adapters implement the #{NAME} port " \
|
|
160
|
+
"(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
require_relative "agent/answers"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require_relative "../runtime/registry"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Ports
|
|
5
|
+
# THE OIDC HANDSHAKE ITSELF — building the URL a browser goes to,
|
|
6
|
+
# and turning the code a provider sends back into a verified
|
|
7
|
+
# (issuer, subject, email) triple. Resolved the same way every
|
|
8
|
+
# other port here resolves its adapter: one adapter registry-wide
|
|
9
|
+
# answers this, not a per-aggregate binding, since a domain has no
|
|
10
|
+
# reason to want a different sign-in provider per aggregate.
|
|
11
|
+
#
|
|
12
|
+
# THE OTHER HALF of "who is this" — what a verified pair MEANS to
|
|
13
|
+
# this app, whether it resolves to an existing Identity — is
|
|
14
|
+
# `Ports::IdentityResolution`'s job, not this one's. This port only
|
|
15
|
+
# ever talks to the external provider; it never touches the
|
|
16
|
+
# registry's own Identity/Governance data (it takes `registry`
|
|
17
|
+
# purely to resolve which adapter answers it, same as every port
|
|
18
|
+
# here already does).
|
|
19
|
+
module Authentication
|
|
20
|
+
NAME = "authentication"
|
|
21
|
+
|
|
22
|
+
ValidationError = Class.new(StandardError)
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def authorization_url(registry)
|
|
27
|
+
adapter(registry).authorization_url
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def verify(registry, code:, state:, expected_state:)
|
|
31
|
+
adapter(registry).verify(code: code, state: state, expected_state: expected_state)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def adapter(registry)
|
|
35
|
+
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
36
|
+
|
|
37
|
+
case implementations.size
|
|
38
|
+
when 1 then registry.adapter_class(implementations.first.name)
|
|
39
|
+
when 0
|
|
40
|
+
raise Runtime::WiringError,
|
|
41
|
+
"no adapter implements the #{NAME} port — nothing can authenticate a sign-in"
|
|
42
|
+
else
|
|
43
|
+
raise Runtime::WiringError,
|
|
44
|
+
"#{implementations.size} adapters implement the #{NAME} port " \
|
|
45
|
+
"(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require_relative "../runtime/registry"
|
|
2
|
+
|
|
3
|
+
module Hecks
|
|
4
|
+
module Ports
|
|
5
|
+
# TWO YES/NO QUESTIONS AND ONE VALUE an application asks BEFORE
|
|
6
|
+
# binding a caller — resolved the same way `Ports::IdentityGeneration`
|
|
7
|
+
# resolves its own adapter: one adapter registry-wide answers this
|
|
8
|
+
# port, not a per-aggregate binding, since a domain has no reason to
|
|
9
|
+
# want a different authorization source per aggregate. What answers
|
|
10
|
+
# it is deliberately not named here — the governance-backed adapter
|
|
11
|
+
# is one implementation, not the only possible one, the same way
|
|
12
|
+
# `SequentialIdentity` and `SecureRandomIdentity` are two
|
|
13
|
+
# implementations of `identity_generation`.
|
|
14
|
+
#
|
|
15
|
+
# A caller decides what to DO with the answer — dispatch under that
|
|
16
|
+
# role, refuse, log, prefer it over some other fallback value — this
|
|
17
|
+
# only answers the question asked. Nothing here binds a
|
|
18
|
+
# `Runtime::Caller`.
|
|
19
|
+
#
|
|
20
|
+
# STALE AS OF THE `as_of`/`scope` split below, this used to also say
|
|
21
|
+
# nothing here is consulted by `CommandRules::Authorization` — it now
|
|
22
|
+
# is: `refuse_role_mismatch` calls `holds_role?` directly, once a
|
|
23
|
+
# caller binds an `actor_id` and the command's domain has Governance
|
|
24
|
+
# attached (see that rule's own header). `spec/act_as_spec.rb`
|
|
25
|
+
# remains the precedent for the OTHER shape — two separate
|
|
26
|
+
# registries, queried directly by name — for whenever Governance is
|
|
27
|
+
# not in the same boot as the caller; this port is the same
|
|
28
|
+
# questions asked through one adapter when it is.
|
|
29
|
+
module Authorization
|
|
30
|
+
NAME = "authorization"
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
def holds_role?(registry, actor_id:, role:, as_of: nil, scope: nil)
|
|
35
|
+
adapter(registry).holds_role?(registry, actor_id: actor_id, role: role, as_of: as_of, scope: scope)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def authorized_as?(registry, from_role:, to_role:)
|
|
39
|
+
adapter(registry).authorized_as?(registry, from_role: from_role, to_role: to_role)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def live_role_for(registry, actor_id:)
|
|
43
|
+
adapter(registry).live_role_for(registry, actor_id: actor_id)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def adapter(registry)
|
|
47
|
+
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
48
|
+
|
|
49
|
+
case implementations.size
|
|
50
|
+
when 1 then registry.adapter_class(implementations.first.name)
|
|
51
|
+
when 0
|
|
52
|
+
raise Runtime::WiringError,
|
|
53
|
+
"no adapter implements the #{NAME} port — nothing can answer a role check"
|
|
54
|
+
else
|
|
55
|
+
raise Runtime::WiringError,
|
|
56
|
+
"#{implementations.size} adapters implement the #{NAME} port " \
|
|
57
|
+
"(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|