hecks 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +7 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +10 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
- data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
- data/lib/hecks/adapters/driven/heki/journal.rb +5 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
- data/lib/hecks/adapters/driven/heki.rb +13 -7
- data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
- data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
- data/lib/hecks/adapters/driven/lambda.rb +39 -33
- data/lib/hecks/adapters/driven/local_storage.rb +17 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
- data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
- data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
- data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
- data/lib/hecks/adapters/driven/postgres.rb +175 -28
- data/lib/hecks/adapters/driven/prism.rb +4 -4
- data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
- data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
- data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
- data/lib/hecks/adapters/driven/sqlite.rb +181 -21
- data/lib/hecks/adapters/driven.rb +4 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
- data/lib/hecks/behaviors/dsl.rb +2 -2
- data/lib/hecks/behaviors/expectations.rb +23 -23
- data/lib/hecks/behaviors/ir.rb +1 -1
- data/lib/hecks/behaviors/rspec.rb +1 -1
- data/lib/hecks/behaviors/runner.rb +2 -2
- data/lib/hecks/behaviors.rb +1 -1
- data/lib/hecks/bluebook/aggregate.rb +13 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
- data/lib/hecks/bluebook/assembly/build.rb +1 -1
- data/lib/hecks/bluebook/assembly/contract.rb +39 -16
- data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
- data/lib/hecks/bluebook/assembly/marks.rb +18 -18
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
- data/lib/hecks/bluebook/assembly.rb +9 -9
- data/lib/hecks/bluebook/attribute.rb +9 -9
- data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
- data/lib/hecks/bluebook/behaviour/command.rb +23 -23
- data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
- data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
- data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
- data/lib/hecks/bluebook/behaviour/query.rb +1 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +28 -9
- data/lib/hecks/bluebook/command.rb +12 -12
- data/lib/hecks/bluebook/domain_port.rb +9 -9
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
- data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
- data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
- data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
- data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
- data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
- data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
- data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
- data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
- data/lib/hecks/bluebook/entity.rb +11 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
- data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
- data/lib/hecks/bluebook/expression/resolver.rb +60 -62
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/lifecycle.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
- data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
- data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
- data/lib/hecks/bluebook/meta_validator.rb +70 -70
- data/lib/hecks/bluebook/model_check.rb +170 -84
- data/lib/hecks/bluebook/pattern_subset.rb +9 -9
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +14 -14
- data/lib/hecks/bluebook/project_register.rb +6 -6
- data/lib/hecks/bluebook/query.rb +4 -4
- data/lib/hecks/bluebook/read_model.rb +14 -14
- data/lib/hecks/bluebook/reference.rb +8 -8
- data/lib/hecks/bluebook/smoke_test.rb +19 -19
- data/lib/hecks/bluebook/synthesizer.rb +12 -12
- data/lib/hecks/bluebook/translation.rb +4 -4
- data/lib/hecks/bluebook/value_object.rb +6 -6
- data/lib/hecks/bluebook.rb +2 -2
- data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
- data/lib/hecks/codemod.rb +36 -35
- data/lib/hecks/construct.rb +6 -6
- data/lib/hecks/corpus.rb +317 -0
- data/lib/hecks/deprecation.rb +95 -0
- data/lib/hecks/doc/reference.rb +19 -19
- data/lib/hecks/embryonaut_bluebook.rb +11 -11
- data/lib/hecks/facade/cli_door.rb +69 -10
- data/lib/hecks/facade/cli_runner.rb +105 -24
- data/lib/hecks/facade/command_request.rb +23 -0
- data/lib/hecks/facade/handle.rb +79 -32
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
- data/lib/hecks/facade/surface/chapter.rb +26 -17
- data/lib/hecks/facade/surface.rb +16 -3
- data/lib/hecks/facade.rb +15 -4
- data/lib/hecks/forms/app.rb +46 -30
- data/lib/hecks/forms/command_form_renderer.rb +70 -9
- data/lib/hecks/forms/field_renderer.rb +142 -6
- data/lib/hecks/forms/field_shape.rb +180 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +2 -2
- data/lib/hecks/forms/query_form_renderer.rb +2 -2
- data/lib/hecks/forms/record_renderer.rb +2 -2
- data/lib/hecks/forms/record_table.rb +1 -1
- data/lib/hecks/forms/value_object_shape.rb +3 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +1 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework.rb +48 -17
- data/lib/hecks/freezer.rb +11 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
- data/lib/hecks/fuzzing/combination_miner.rb +119 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
- data/lib/hecks/fuzzing/differential.rb +158 -0
- data/lib/hecks/fuzzing/domain_generator.rb +694 -0
- data/lib/hecks/fuzzing/era_boundary.rb +37 -18
- data/lib/hecks/fuzzing/form_census.rb +35 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
- data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
- data/lib/hecks/fuzzing/properties/guards.rb +42 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
- data/lib/hecks/fuzzing/properties/querying.rb +17 -17
- data/lib/hecks/fuzzing/properties.rb +64 -40
- data/lib/hecks/fuzzing/qa_settings.rb +152 -0
- data/lib/hecks/fuzzing/replay.rb +114 -114
- data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
- data/lib/hecks/fuzzing/self_consistency.rb +141 -112
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
- data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
- data/lib/hecks/fuzzing/shrinker.rb +197 -0
- data/lib/hecks/fuzzing/structural_skips.rb +23 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
- data/lib/hecks/fuzzing/value_generator.rb +14 -14
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +10 -10
- data/lib/hecks/grammar.rb +7 -7
- data/lib/hecks/ir.rb +13 -13
- data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
- data/lib/hecks/language/bluebook/policy.bluebook +11 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +9 -9
- data/lib/hecks/naming.rb +25 -25
- data/lib/hecks/ports/access_control.rb +58 -2
- data/lib/hecks/ports/agent/answers.rb +83 -6
- data/lib/hecks/ports/agent.rb +119 -35
- data/lib/hecks/ports/authentication.rb +44 -4
- data/lib/hecks/ports/authorization.rb +53 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +24 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +18 -1
- data/lib/hecks/ports/loading.rb +4 -0
- data/lib/hecks/ports/persistence/append_only.rb +172 -8
- data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
- data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
- data/lib/hecks/ports/persistence/execution.rb +4 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
- data/lib/hecks/ports/persistence/plugin.rb +42 -4
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
- data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
- data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
- data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
- data/lib/hecks/ports/persistence/state_codec.rb +319 -0
- data/lib/hecks/ports/persistence.rb +36 -1
- data/lib/hecks/ports/projection.rb +61 -7
- data/lib/hecks/ports/query/in_memory.rb +3 -3
- data/lib/hecks/ports/query/ordering.rb +6 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/projections/bootstrap_table.rb +112 -0
- data/lib/hecks/projections/diagrams.rb +75 -75
- data/lib/hecks/projections/glossary/html.rb +3 -3
- data/lib/hecks/projections/glossary/markdown.rb +3 -3
- data/lib/hecks/projections/glossary/mermaid.rb +1 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +5 -5
- data/lib/hecks/projections/glossary.rb +15 -15
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +18 -17
- data/lib/hecks/projections/model.rb +25 -21
- data/lib/hecks/projections/oidc.rb +7 -7
- data/lib/hecks/projections/parser_table.rb +5 -5
- data/lib/hecks/projections/reference.rb +3 -3
- data/lib/hecks/projections/rust_vocabulary.rb +443 -0
- data/lib/hecks/projections/shape.rb +2 -2
- data/lib/hecks/projections/statements.rb +11 -11
- data/lib/hecks/projections/vocabulary.rb +9 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +29 -29
- data/lib/hecks/projector/docs_projector.rb +13 -13
- data/lib/hecks/projector/exporter.rb +42 -21
- data/lib/hecks/projector/ir_projector.rb +1 -1
- data/lib/hecks/projector/narrate_projector.rb +11 -11
- data/lib/hecks/projector/target.rb +13 -13
- data/lib/hecks/projector.rb +15 -15
- data/lib/hecks/query_ir.rb +47 -47
- data/lib/hecks/query_specification/common/comparators.rb +19 -3
- data/lib/hecks/query_specification/common/comparison.rb +109 -27
- data/lib/hecks/query_specification/common/dsl.rb +65 -9
- data/lib/hecks/query_specification/common/null_policy.rb +57 -13
- data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
- data/lib/hecks/query_specification/common/options.rb +25 -0
- data/lib/hecks/query_specification/field_path.rb +69 -15
- data/lib/hecks/query_specification/hop_path.rb +57 -20
- data/lib/hecks/query_specification/read_model/specification.rb +4 -0
- data/lib/hecks/rendering.rb +3 -3
- data/lib/hecks/router/namespace_installer.rb +3 -3
- data/lib/hecks/router.rb +1 -1
- data/lib/hecks/runtime/aggregate_lock.rb +11 -11
- data/lib/hecks/runtime/boot_gates.rb +3 -3
- data/lib/hecks/runtime/caller.rb +8 -8
- data/lib/hecks/runtime/capability_graph.rb +2 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
- data/lib/hecks/runtime/command_interpreter.rb +159 -102
- data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
- data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
- data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
- data/lib/hecks/runtime/command_rules/emission.rb +18 -1
- data/lib/hecks/runtime/command_rules/references.rb +35 -35
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +11 -11
- data/lib/hecks/runtime/dispatcher.rb +283 -90
- data/lib/hecks/runtime/entity_element.rb +151 -67
- data/lib/hecks/runtime/entity_interpreter.rb +108 -82
- data/lib/hecks/runtime/errors.rb +19 -19
- data/lib/hecks/runtime/event.rb +6 -6
- data/lib/hecks/runtime/identity.rb +22 -22
- data/lib/hecks/runtime/instance.rb +39 -14
- data/lib/hecks/runtime/interpreting.rb +12 -12
- data/lib/hecks/runtime/invocation.rb +276 -0
- data/lib/hecks/runtime/loader.rb +14 -14
- data/lib/hecks/runtime/outbox.rb +23 -23
- data/lib/hecks/runtime/policy_interpreter.rb +54 -54
- data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
- data/lib/hecks/runtime/query_interpreter.rb +55 -64
- data/lib/hecks/runtime/reaction_invocation.rb +20 -20
- data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
- data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
- data/lib/hecks/runtime/reference_hop.rb +6 -6
- data/lib/hecks/runtime/refusal_wording.rb +92 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
- data/lib/hecks/runtime/registry/verification.rb +36 -26
- data/lib/hecks/runtime/registry.rb +45 -28
- data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
- data/lib/hecks/runtime/routing.rb +10 -153
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
- data/lib/hecks/runtime/saga_interpreter.rb +64 -64
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +9 -9
- data/lib/hecks/runtime/tenant_scope.rb +5 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +318 -127
- data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
- data/lib/hecks/runtime/value.rb +21 -21
- data/lib/hecks/runtime.rb +7 -7
- data/lib/hecks/storehouse.rb +64 -64
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +13 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
- metadata +22 -2
data/lib/hecks/ports/agent.rb
CHANGED
|
@@ -2,41 +2,51 @@ require_relative "../runtime/registry"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Ports
|
|
5
|
-
#
|
|
5
|
+
# The interviewer — whatever asks the next good question, reads a
|
|
6
6
|
# sentence back into proposed declarations, judges a model on taste
|
|
7
|
-
# rather than structure, or suggests a name.
|
|
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.
|
|
7
|
+
# rather than structure, or suggests a name.
|
|
12
8
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
9
|
+
# ## Why a singleton port
|
|
10
|
+
#
|
|
11
|
+
# Resolved the same zero/one/many way every other port here is
|
|
12
|
+
# (`Ports::IdentityGeneration`'s own singleton adapter — one
|
|
13
|
+
# interviewer per process, never per-aggregate), because a second
|
|
14
|
+
# adapter answering this port is exactly as unchoosable as a second
|
|
15
|
+
# one minting identities.
|
|
16
|
+
#
|
|
17
|
+
# ## Why this exists
|
|
18
|
+
#
|
|
19
|
+
# Built last, on purpose, and still optional. `.claude/skills/
|
|
20
|
+
# interview/SKILL.md` conducts a whole session with none of this —
|
|
15
21
|
# Claude Code reads `bin/interview state` itself, asks the human
|
|
16
22
|
# directly with real conversation context, and calls `bin/interview
|
|
17
23
|
# ask`/`propose`/`accept` itself. This port exists for what that
|
|
18
|
-
# cannot reach: a
|
|
24
|
+
# cannot reach: a headless run (`bin/interview suggest`, no model in
|
|
19
25
|
# the room otherwise), a batch `critique` over an already-written
|
|
20
26
|
# chapter, and — the actual point of building it at all — a
|
|
21
|
-
#
|
|
27
|
+
# scripted double (`spec/fixtures/scripted_agent.rb`) that makes the
|
|
22
28
|
# whole loop testable with no real model involved.
|
|
23
29
|
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
30
|
+
# ## What this port is not
|
|
31
|
+
#
|
|
32
|
+
# It does not judge whether a declaration is well-formed — that is
|
|
33
|
+
# the meta-domain's own job, answered by a real dispatch refusing or
|
|
34
|
+
# not (`Interview::Session#offer`). This port only ever produces
|
|
35
|
+
# shape: a question, a proposed declaration, a judgement about
|
|
36
|
+
# taste, a name. A proposal naming a category the language does not
|
|
37
|
+
# declare is an adapter fault, refused here, in `Answers`; a
|
|
38
|
+
# proposal naming a real category but describing the wrong domain
|
|
39
|
+
# fact is dispatched anyway, so the language gets to say why. That
|
|
40
|
+
# line is the whole design.
|
|
33
41
|
#
|
|
34
|
-
#
|
|
42
|
+
# ## Where parsing happens
|
|
43
|
+
#
|
|
44
|
+
# Parsing belongs to this file, not the adapter. An adapter answers
|
|
35
45
|
# with a plain, already-JSON-shaped Hash (a real model's parsed
|
|
36
46
|
# reply, or a spec double's own hand-built one) — never a Struct —
|
|
37
47
|
# so every adapter is validated identically here rather than trusting
|
|
38
48
|
# each one to refuse the same way. Two adapters that parsed
|
|
39
|
-
# differently would mean the
|
|
49
|
+
# differently would mean the same malformed answer passing through
|
|
40
50
|
# one and refusing through the other.
|
|
41
51
|
module Agent
|
|
42
52
|
NAME = "agent".freeze
|
|
@@ -54,7 +64,7 @@ module Hecks
|
|
|
54
64
|
# Nothing answered at all — the binary is missing, the subprocess
|
|
55
65
|
# died, the call timed out. Separate from ValidationError on
|
|
56
66
|
# purpose: unavailable means retry or fall back to asking the
|
|
57
|
-
# human directly; malformed means the
|
|
67
|
+
# human directly; malformed means the prompt is wrong, not the
|
|
58
68
|
# transport, and retrying identically will not fix it.
|
|
59
69
|
class Unavailable < StandardError
|
|
60
70
|
end
|
|
@@ -66,26 +76,26 @@ module Hecks
|
|
|
66
76
|
# declared, which turns "the model invented a field" into a
|
|
67
77
|
# failure at the boundary rather than a nil three calls in.
|
|
68
78
|
|
|
69
|
-
#
|
|
79
|
+
# One question. `because` is the reasoning quoted back so a human
|
|
70
80
|
# can see it, not decoration — the same "show your work" the
|
|
71
81
|
# language's own refusal messages already practice.
|
|
72
82
|
Question = Struct.new(:text, :because, keyword_init: true)
|
|
73
83
|
|
|
74
|
-
#
|
|
84
|
+
# One proposed declaration, already shaped as
|
|
75
85
|
# `Interview::Proposal`'s own `Argument` rows — `{name:, field:,
|
|
76
86
|
# value:}`, the exact triple `bin/interview propose --arg
|
|
77
87
|
# name:field:value` already takes and `Interview::Lowering`
|
|
78
|
-
# already knows how to lower. Nothing here decides
|
|
88
|
+
# already knows how to lower. Nothing here decides how to address
|
|
79
89
|
# a record or whether a value is bare or wrapped — that was
|
|
80
90
|
# already the proposal-writer's job in `interview.bluebook`'s own
|
|
81
91
|
# design (see `Argument`'s own comment there); this only adds the
|
|
82
|
-
#
|
|
92
|
+
# prose reasoning a human typed does not carry on its own.
|
|
83
93
|
Proposal = Struct.new(:verb, :arguments, :rationale, keyword_init: true)
|
|
84
94
|
|
|
85
|
-
#
|
|
95
|
+
# Critique reuses `Bluebook::ModelCheck::Finding`'s own shape —
|
|
86
96
|
# same fields, same severities, so a mechanical finding
|
|
87
97
|
# (`Session#gaps`) and a judgement (this) print in one list and
|
|
88
|
-
# sort together. The
|
|
98
|
+
# sort together. The kind vocabulary is not shared — ModelCheck's
|
|
89
99
|
# eleven kinds are structural facts about a graph; these are
|
|
90
100
|
# opinions about a model, closed here on purpose (an open kind
|
|
91
101
|
# field is an open prompt, and an open prompt drifts).
|
|
@@ -100,54 +110,128 @@ module Hecks
|
|
|
100
110
|
def to_s = "#{severity.to_s.upcase.ljust(7)} #{kind.to_s.ljust(20)} #{subject} — #{message}"
|
|
101
111
|
end
|
|
102
112
|
|
|
103
|
-
#
|
|
113
|
+
# One suggested name. `rejected` carries the near-misses and why
|
|
104
114
|
# they were passed over — free at the point the model is choosing
|
|
105
115
|
# anyway, and the part a human actually learns from.
|
|
106
116
|
Suggestion = Struct.new(:name, :because, :rejected, keyword_init: true)
|
|
107
117
|
|
|
108
118
|
module_function
|
|
109
119
|
|
|
110
|
-
#
|
|
120
|
+
# Asks the adapter for the next best question and validates its raw answer into structs.
|
|
121
|
+
#
|
|
122
|
+
# `state` is the whole picture — normally
|
|
111
123
|
# `Interview::Session#declaration` plus `#gaps`, so an adapter
|
|
112
124
|
# needs no memory of its own between calls; that is what lets a
|
|
113
125
|
# headless run be a series of one-shot processes, same as
|
|
114
126
|
# `bin/interview` itself already is.
|
|
127
|
+
#
|
|
128
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
129
|
+
# @param state [Hash] the interview's whole current picture (declaration plus gaps),
|
|
130
|
+
# JSON-able because an adapter may serialise it into a prompt
|
|
131
|
+
# @param asked [Array<Object>] JSON-able record of the questions already asked, so the
|
|
132
|
+
# adapter does not repeat one; `[]` when nothing has been asked
|
|
133
|
+
# @return [Array<Ports::Agent::Question>] the questions the adapter answered with, in
|
|
134
|
+
# its order; `[]` if it offered none
|
|
135
|
+
# @raise [Ports::Agent::ValidationError] if the answer is not a Hash holding a
|
|
136
|
+
# `"questions"` array whose rows each carry a non-blank `"text"` and `"because"`
|
|
137
|
+
# @raise [Ports::Agent::Unavailable] if the adapter cannot answer at all (missing
|
|
138
|
+
# binary, failed subprocess, timeout, or an exhausted scripted queue)
|
|
139
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
140
|
+
# (see `adapter`)
|
|
115
141
|
def ask(registry, state:, asked: [])
|
|
116
142
|
Answers.questions(adapter(registry).ask(state: state, asked: asked))
|
|
117
143
|
end
|
|
118
144
|
|
|
119
|
-
#
|
|
145
|
+
# Turns a human's sentence into proposed declarations, validated into structs.
|
|
146
|
+
#
|
|
147
|
+
# Returns `[]` when the sentence
|
|
120
148
|
# carried no declaration at all (a clarifying question back from
|
|
121
149
|
# the human, say) — a legitimate answer, not a failure.
|
|
150
|
+
#
|
|
151
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
152
|
+
# @param prose [String] a human's plain-English sentence
|
|
153
|
+
# @param state [Hash] the interview's whole current picture, JSON-able because an
|
|
154
|
+
# adapter may serialise it into a prompt
|
|
155
|
+
# @return [Array<Ports::Agent::Proposal>] one proposal per declaration the sentence
|
|
156
|
+
# named; `[]` when it named none
|
|
157
|
+
# @raise [Ports::Agent::ValidationError] if the answer is not a Hash holding a
|
|
158
|
+
# `"proposals"` array, a row's `"verb"` is not `Chapter::Aggregate.Command`, its
|
|
159
|
+
# `"rationale"` is blank, or one of its argument rows has no `"name"`
|
|
160
|
+
# @raise [Ports::Agent::Unavailable] if the adapter cannot answer at all (missing
|
|
161
|
+
# binary, failed subprocess, timeout, or an exhausted scripted queue)
|
|
162
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
163
|
+
# (see `adapter`)
|
|
122
164
|
def interpret(registry, prose:, state:)
|
|
123
165
|
Answers.proposals(adapter(registry).interpret(prose: prose, state: state))
|
|
124
166
|
end
|
|
125
167
|
|
|
126
|
-
#
|
|
168
|
+
# Asks the adapter to judge a declared model on taste, validated into structs.
|
|
169
|
+
#
|
|
170
|
+
# What is wrong with this as a model — handed everything already
|
|
127
171
|
# known (the language's own refusals, `Session#gaps`'s mechanical
|
|
128
172
|
# findings) so it spends its judgement on what neither of those
|
|
129
173
|
# can see, rather than restating them.
|
|
174
|
+
#
|
|
175
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
176
|
+
# @param declared [Hash] the chapter as declared so far, JSON-able because an adapter
|
|
177
|
+
# may serialise it into a prompt
|
|
178
|
+
# @param refusals [Array<Object>] JSON-able refusals the language itself already
|
|
179
|
+
# raised; `[]` when there are none
|
|
180
|
+
# @param findings [Array<Object>] JSON-able mechanical findings `Session#gaps` already
|
|
181
|
+
# found; `[]` when there are none
|
|
182
|
+
# @return [Array<Ports::Agent::Finding>] the adapter's judgements; `[]` when it has
|
|
183
|
+
# nothing worth saying
|
|
184
|
+
# @raise [Ports::Agent::ValidationError] if the answer is not a Hash holding a
|
|
185
|
+
# `"findings"` array, a row's `"kind"` is outside `CRITIQUE_KINDS`, its `"severity"`
|
|
186
|
+
# is outside `SEVERITIES`, or its `"subject"` or `"message"` is blank
|
|
187
|
+
# @raise [Ports::Agent::Unavailable] if the adapter cannot answer at all (missing
|
|
188
|
+
# binary, failed subprocess, timeout, or an exhausted scripted queue)
|
|
189
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
190
|
+
# (see `adapter`)
|
|
130
191
|
def critique(registry, declared:, refusals: [], findings: [])
|
|
131
192
|
Answers.findings(adapter(registry).critique(declared: declared, refusals: refusals, findings: findings))
|
|
132
193
|
end
|
|
133
194
|
|
|
134
|
-
#
|
|
195
|
+
# Asks the adapter to suggest a name for a construct, validated into structs.
|
|
196
|
+
#
|
|
197
|
+
# Vocabulary help. `near` is what the chapter already calls
|
|
135
198
|
# things, so a suggestion cannot collide with a name in use.
|
|
136
199
|
#
|
|
137
|
-
#
|
|
200
|
+
# Named `suggest_name`, not `name` — the plan's own word for this
|
|
138
201
|
# operation, but `Module#name` already exists and is load-bearing
|
|
139
202
|
# everywhere (backtraces, RSpec's own description building,
|
|
140
|
-
# `inspect`): a module-function called `name`
|
|
203
|
+
# `inspect`): a module-function called `name` shadows it the
|
|
141
204
|
# instant it's defined, and `SomeModule.name` (no args) then
|
|
142
205
|
# raises `ArgumentError: missing keywords` the next time anything
|
|
143
206
|
# — not this file, something else entirely — asks the module its
|
|
144
207
|
# own name. Measured, not theoretical: this exact collision broke
|
|
145
208
|
# RSpec's own failure-message formatting the first time it was
|
|
146
209
|
# named `name` here.
|
|
210
|
+
#
|
|
211
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
212
|
+
# @param meaning [String] what the new name needs to mean
|
|
213
|
+
# @param kind [String] the kind of construct being named, such as `"event"`; an
|
|
214
|
+
# adapter interpolates it into its prompt
|
|
215
|
+
# @param near [Array<String>] names already in use in this chapter, which a suggestion
|
|
216
|
+
# must not collide with; `[]` when there are none
|
|
217
|
+
# @return [Array<Ports::Agent::Suggestion>] the suggested names, each with its
|
|
218
|
+
# `rejected` near-misses as Strings
|
|
219
|
+
# @raise [Ports::Agent::ValidationError] if the answer is not a Hash holding a
|
|
220
|
+
# `"names"` array whose rows each carry a non-blank `"name"` and `"because"`
|
|
221
|
+
# @raise [Ports::Agent::Unavailable] if the adapter cannot answer at all (missing
|
|
222
|
+
# binary, failed subprocess, timeout, or an exhausted scripted queue)
|
|
223
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
224
|
+
# (see `adapter`)
|
|
147
225
|
def suggest_name(registry, meaning:, kind:, near: [])
|
|
148
226
|
Answers.suggestions(adapter(registry).suggest_name(meaning: meaning, kind: kind, near: near))
|
|
149
227
|
end
|
|
150
228
|
|
|
229
|
+
# Finds the single adapter bound to this port, refusing an ambiguous wiring.
|
|
230
|
+
#
|
|
231
|
+
# @param registry [Runtime::Registry] the booted registry to search
|
|
232
|
+
# @return [Module] the adapter module or class implementing this port
|
|
233
|
+
# @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
|
|
234
|
+
# or the one that does has no Ruby implementation under `Hecks::Adapters`
|
|
151
235
|
def adapter(registry)
|
|
152
236
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
153
237
|
|
|
@@ -2,14 +2,14 @@ require_relative "../runtime/registry"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Ports
|
|
5
|
-
#
|
|
6
|
-
# and turning the code a provider sends back into a verified
|
|
7
|
-
# (issuer
|
|
5
|
+
# The OIDC handshake itself — building the URL a browser goes to,
|
|
6
|
+
# and turning the code a provider sends back into a Hash of verified
|
|
7
|
+
# claims (`issuer`, `subject`, `email`, `email_verified`). Resolved the same way every
|
|
8
8
|
# other port here resolves its adapter: one adapter registry-wide
|
|
9
9
|
# answers this, not a per-aggregate binding, since a domain has no
|
|
10
10
|
# reason to want a different sign-in provider per aggregate.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
12
|
+
# The other half of "who is this" — what a verified pair means to
|
|
13
13
|
# this app, whether it resolves to an existing Identity — is
|
|
14
14
|
# `Ports::IdentityResolution`'s job, not this one's. This port only
|
|
15
15
|
# ever talks to the external provider; it never touches the
|
|
@@ -19,19 +19,59 @@ module Hecks
|
|
|
19
19
|
module Authentication
|
|
20
20
|
NAME = "authentication".freeze
|
|
21
21
|
|
|
22
|
+
# A sign-in that could not be verified — a state mismatch, a failed code
|
|
23
|
+
# exchange, a missing or invalid ID token. An adapter wraps its own
|
|
24
|
+
# libraries' errors into this, so a caller rescues one class whichever
|
|
25
|
+
# provider answers the port.
|
|
22
26
|
class ValidationError < StandardError
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
module_function
|
|
26
30
|
|
|
31
|
+
# Builds the provider URL that starts a sign-in, with the CSRF state minted for it.
|
|
32
|
+
#
|
|
33
|
+
# The caller keeps the state (in a session, typically) and hands it back to `verify`
|
|
34
|
+
# as `expected_state`.
|
|
35
|
+
#
|
|
36
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
37
|
+
# @return [Array(String, String)] the URL to send the browser to, then the fresh
|
|
38
|
+
# `state` value embedded in it
|
|
39
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
40
|
+
# (see `adapter`)
|
|
41
|
+
# @raise [KeyError] if the adapter's required configuration is unset
|
|
42
|
+
# (`GoogleAuthentication` reads `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` and
|
|
43
|
+
# `GOOGLE_REDIRECT_URI` with no defaults)
|
|
27
44
|
def authorization_url(registry)
|
|
28
45
|
adapter(registry).authorization_url
|
|
29
46
|
end
|
|
30
47
|
|
|
48
|
+
# Exchanges the authorization code a provider sent back for a Hash of verified claims.
|
|
49
|
+
#
|
|
50
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
51
|
+
# @param code [String] the authorization code the provider sent back
|
|
52
|
+
# @param state [String, nil] the state parameter the provider returned; nil is refused
|
|
53
|
+
# @param expected_state [String, nil] the state `authorization_url` handed out before
|
|
54
|
+
# the redirect; nil is refused
|
|
55
|
+
# @return [Hash{Symbol => Object}] the verified claims: `issuer:` and `subject:`
|
|
56
|
+
# (String), `email:` (String, or nil if the token carries none) and
|
|
57
|
+
# `email_verified:` (Boolean) — never the raw token
|
|
58
|
+
# @raise [Ports::Authentication::ValidationError] if either state is nil or the two
|
|
59
|
+
# differ, the code exchange fails, the response has no ID token, or the ID token
|
|
60
|
+
# does not verify
|
|
61
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
62
|
+
# (see `adapter`)
|
|
63
|
+
# @raise [KeyError] if the adapter's required configuration is unset, or a verified
|
|
64
|
+
# token lacks an `iss` or `sub` claim
|
|
31
65
|
def verify(registry, code:, state:, expected_state:)
|
|
32
66
|
adapter(registry).verify(code: code, state: state, expected_state: expected_state)
|
|
33
67
|
end
|
|
34
68
|
|
|
69
|
+
# Finds the single adapter bound to this port, refusing an ambiguous wiring.
|
|
70
|
+
#
|
|
71
|
+
# @param registry [Runtime::Registry] the booted registry to search
|
|
72
|
+
# @return [Module] the adapter module or class implementing this port
|
|
73
|
+
# @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
|
|
74
|
+
# or the one that does has no Ruby implementation under `Hecks::Adapters`
|
|
35
75
|
def adapter(registry)
|
|
36
76
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
37
77
|
|
|
@@ -2,7 +2,7 @@ require_relative "../runtime/registry"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Ports
|
|
5
|
-
#
|
|
5
|
+
# Two yes/no questions and one value an application asks before
|
|
6
6
|
# binding a caller — resolved the same way `Ports::IdentityGeneration`
|
|
7
7
|
# resolves its own adapter: one adapter registry-wide answers this
|
|
8
8
|
# port, not a per-aggregate binding, since a domain has no reason to
|
|
@@ -12,37 +12,79 @@ module Hecks
|
|
|
12
12
|
# `SequentialIdentity` and `SecureRandomIdentity` are two
|
|
13
13
|
# implementations of `identity_generation`.
|
|
14
14
|
#
|
|
15
|
-
# A caller decides what to
|
|
15
|
+
# A caller decides what to do with the answer — dispatch under that
|
|
16
16
|
# role, refuse, log, prefer it over some other fallback value — this
|
|
17
17
|
# only answers the question asked. Nothing here binds a
|
|
18
18
|
# `Runtime::Caller`.
|
|
19
19
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# not in the same boot as the caller; this port is the same
|
|
28
|
-
# questions asked through one adapter when it is.
|
|
20
|
+
# `CommandRules::Authorization#refuse_role_mismatch` calls `holds_role?`
|
|
21
|
+
# directly, once a caller binds an `actor_id` and the command's domain
|
|
22
|
+
# has Governance attached (see that rule's own header). `spec/
|
|
23
|
+
# act_as_spec.rb` remains the precedent for the other shape — two
|
|
24
|
+
# separate registries, queried directly by name — for whenever
|
|
25
|
+
# Governance is not in the same boot as the caller; this port is the
|
|
26
|
+
# same questions asked through one adapter when it is.
|
|
29
27
|
module Authorization
|
|
30
28
|
NAME = "authorization".freeze
|
|
31
29
|
|
|
32
30
|
module_function
|
|
33
31
|
|
|
32
|
+
# Answers whether an actor holds a live (not ended) grant of a role.
|
|
33
|
+
#
|
|
34
|
+
# @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
|
|
35
|
+
# and handed on to it
|
|
36
|
+
# @param actor_id [String] the actor whose grants are checked
|
|
37
|
+
# @param role [String, Symbol] the role name to look for, compared as a String
|
|
38
|
+
# @param as_of [Integer, nil] Unix epoch seconds (from `Ports::Clock.now`); a grant whose
|
|
39
|
+
# `starts_at` is later, or does not parse as a time, does not count. nil skips the
|
|
40
|
+
# `starts_at` check entirely
|
|
41
|
+
# @param scope [String, nil] the scope the caller acts in; only a grant made for that
|
|
42
|
+
# scope counts. nil skips the scope check, so a grant in any scope counts
|
|
43
|
+
# @return [Boolean] true if at least one live grant of `role` to `actor_id` passes the
|
|
44
|
+
# `as_of` and `scope` checks
|
|
45
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
46
|
+
# (see `adapter`), or the governance-backed adapter finds no single loaded chapter
|
|
47
|
+
# providing `"authorization"`
|
|
34
48
|
def holds_role?(registry, actor_id:, role:, as_of: nil, scope: nil)
|
|
35
49
|
adapter(registry).holds_role?(registry, actor_id: actor_id, role: role, as_of: as_of, scope: scope)
|
|
36
50
|
end
|
|
37
51
|
|
|
52
|
+
# Answers whether one role may act as another.
|
|
53
|
+
#
|
|
54
|
+
# @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
|
|
55
|
+
# and handed on to it
|
|
56
|
+
# @param from_role [String, Symbol] the role the caller holds, compared as a String
|
|
57
|
+
# @param to_role [String, Symbol] the role the caller wants to act as, compared as a
|
|
58
|
+
# String
|
|
59
|
+
# @return [Boolean] true if a live (not ended) allowance lets `from_role` act as
|
|
60
|
+
# `to_role`
|
|
61
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
62
|
+
# (see `adapter`), or the governance-backed adapter finds no single loaded chapter
|
|
63
|
+
# providing `"authorization"`
|
|
38
64
|
def authorized_as?(registry, from_role:, to_role:)
|
|
39
65
|
adapter(registry).authorized_as?(registry, from_role: from_role, to_role: to_role)
|
|
40
66
|
end
|
|
41
67
|
|
|
68
|
+
# Looks up the role an actor holds right now, rather than checking a guessed one.
|
|
69
|
+
#
|
|
70
|
+
# @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
|
|
71
|
+
# and handed on to it
|
|
72
|
+
# @param actor_id [String] the actor to look up
|
|
73
|
+
# @return [String, nil] the role name of the actor's first live (not ended) grant, or
|
|
74
|
+
# nil if it has none; the caller supplies any fallback
|
|
75
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
76
|
+
# (see `adapter`), or the governance-backed adapter finds no single loaded chapter
|
|
77
|
+
# providing `"authorization"`
|
|
42
78
|
def live_role_for(registry, actor_id:)
|
|
43
79
|
adapter(registry).live_role_for(registry, actor_id: actor_id)
|
|
44
80
|
end
|
|
45
81
|
|
|
82
|
+
# Finds the single adapter bound to this port, refusing an ambiguous wiring.
|
|
83
|
+
#
|
|
84
|
+
# @param registry [Runtime::Registry] the booted registry to search
|
|
85
|
+
# @return [Module] the adapter module or class implementing this port
|
|
86
|
+
# @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
|
|
87
|
+
# or the one that does has no Ruby implementation under `Hecks::Adapters`
|
|
46
88
|
def adapter(registry)
|
|
47
89
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
48
90
|
|
data/lib/hecks/ports/clock.rb
CHANGED
|
@@ -2,47 +2,66 @@ require_relative "../runtime/registry"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Ports
|
|
5
|
-
#
|
|
5
|
+
# What time it is — the one fact a domain cannot derive and must not invent.
|
|
6
6
|
#
|
|
7
7
|
# Resolved exactly the way `Ports::IdentityGeneration` resolves its own
|
|
8
8
|
# adapter: one adapter registry-wide implements this, not a per-aggregate
|
|
9
9
|
# binding, because two aggregates in one running app have no real reason to
|
|
10
10
|
# disagree about the time.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
# this claim after fifteen minutes" — is untestable against the real clock:
|
|
14
|
-
# a spec for it would either sleep for fifteen minutes or never run at all.
|
|
15
|
-
# Bound to a fixed adapter, the same rule is three lines. That is the whole
|
|
16
|
-
# argument, and it is the same one `SequentialIdentity` makes next door.
|
|
12
|
+
# ## Why a port and not `Time.now`
|
|
17
13
|
#
|
|
18
|
-
# A
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# port is what lets a caller stop typing it — the door fills it in, the
|
|
24
|
-
# value is baked into the dispatch, and the recorded step carries the
|
|
25
|
-
# concrete number forever after.
|
|
14
|
+
# A staleness rule — "another agent may take this claim after fifteen
|
|
15
|
+
# minutes" — is untestable against the real clock: a spec for it would
|
|
16
|
+
# either sleep for fifteen minutes or never run at all. Bound to a fixed
|
|
17
|
+
# adapter, the same rule is three lines. That is the whole argument, and
|
|
18
|
+
# it is the same one `SequentialIdentity` makes next door.
|
|
26
19
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
20
|
+
# ## Why a predicate still can't ask the time
|
|
21
|
+
#
|
|
22
|
+
# The expression sublanguage has no clock and should not: a `given` that
|
|
23
|
+
# read the time would evaluate differently on two runs over the same
|
|
24
|
+
# record, and every replay, audit and fuzz oracle in this repository
|
|
25
|
+
# assumes it does not. So `now` remains an argument the predicate merely
|
|
26
|
+
# reads, and this port is what lets a caller stop typing it — the door
|
|
27
|
+
# fills it in, the value is baked into the dispatch, and the recorded
|
|
28
|
+
# step carries the concrete number forever after.
|
|
29
|
+
#
|
|
30
|
+
# ## Why the runtime never calls this directly
|
|
31
|
+
#
|
|
32
|
+
# `IdentityGeneration`'s own note works through the replay question for
|
|
33
|
+
# a minted uuid and lands on "the value gets baked into the caller's
|
|
34
|
+
# args at the first live dispatch". A clock consulted inside the
|
|
35
|
+
# interpreter would not have that property: a recorded corpus step
|
|
36
|
+
# replayed tomorrow would silently get tomorrow's time, and the
|
|
37
|
+
# fuzzer's oracle and the adapter-agreement gate both compare runs of
|
|
38
|
+
# exactly that shape. So the filling happens at the door, where a
|
|
39
|
+
# human or an agent is typing, and never on the dispatch path a replay
|
|
40
|
+
# uses.
|
|
35
41
|
module Clock
|
|
36
42
|
NAME = "clock".freeze
|
|
37
43
|
|
|
38
44
|
module_function
|
|
39
45
|
|
|
40
|
-
#
|
|
46
|
+
# Reads the current time from the bound clock adapter.
|
|
47
|
+
#
|
|
48
|
+
# Seconds, as an integer, because that is what the sublanguage can do
|
|
41
49
|
# arithmetic on. `claimed_at.value + window.value <= now.value` is
|
|
42
50
|
# addition and comparison — the only two things it has — and a Time
|
|
43
51
|
# object supports neither.
|
|
52
|
+
#
|
|
53
|
+
# @param registry [Runtime::Registry] the booted registry to resolve the adapter against
|
|
54
|
+
# @return [Integer] the current time, in Unix epoch seconds
|
|
55
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
56
|
+
# (see `adapter`)
|
|
44
57
|
def now(registry) = adapter(registry).now
|
|
45
58
|
|
|
59
|
+
# Finds the single adapter bound to this port, refusing an ambiguous wiring.
|
|
60
|
+
#
|
|
61
|
+
# @param registry [Runtime::Registry] the booted registry to search
|
|
62
|
+
# @return [Module] the adapter module or class implementing this port
|
|
63
|
+
# @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
|
|
64
|
+
# or the one that does has no Ruby implementation under `Hecks::Adapters`
|
|
46
65
|
def adapter(registry)
|
|
47
66
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
48
67
|
|
|
@@ -11,8 +11,24 @@ module Hecks
|
|
|
11
11
|
|
|
12
12
|
module_function
|
|
13
13
|
|
|
14
|
+
# Recovers a block's body as canonical source text, so a rule is carried as text.
|
|
15
|
+
#
|
|
16
|
+
# @param block [Proc] a block written in a bluebook file, such as a `given` predicate
|
|
17
|
+
# or an `identified_by` path
|
|
18
|
+
# @return [String, nil] the block body's source, normalised by
|
|
19
|
+
# `Bluebook::Expression::CanonicalForm` (whitespace collapsed, linked replacements
|
|
20
|
+
# applied); nil if the block's file cannot be read, no block starts on its line, or
|
|
21
|
+
# the block has an empty body
|
|
22
|
+
# @raise [Runtime::WiringError] if called outside a boot, or this port does not resolve
|
|
23
|
+
# to exactly one adapter (see `adapter`)
|
|
14
24
|
def canonical(block) = adapter.canonical(block)
|
|
15
25
|
|
|
26
|
+
# Finds the single adapter bound to this port in the registry currently booting.
|
|
27
|
+
#
|
|
28
|
+
# @return [Module] the adapter module or class implementing this port
|
|
29
|
+
# @raise [Runtime::WiringError] if `Hecks.current_registry` is nil (resolved outside a
|
|
30
|
+
# boot), if no adapter, or more than one, implements this port, or if the one that
|
|
31
|
+
# does has no Ruby implementation under `Hecks::Adapters`
|
|
16
32
|
def adapter
|
|
17
33
|
registry = Hecks.current_registry
|
|
18
34
|
unless registry
|
|
@@ -2,7 +2,7 @@ require_relative "../runtime/registry"
|
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
4
|
module Ports
|
|
5
|
-
#
|
|
5
|
+
# Which value a creating command's own identity field gets, when
|
|
6
6
|
# neither a slug-from-another-field nor a sequence-with-prefix
|
|
7
7
|
# mechanically answers it — a driving app's own console, say,
|
|
8
8
|
# falls back to this when a collection's identity strategy names
|
|
@@ -12,7 +12,7 @@ module Hecks
|
|
|
12
12
|
#
|
|
13
13
|
# Pure delegation, same as every sibling port (AccessControl,
|
|
14
14
|
# IdentityGeneration): an app's own domain declares what "port"
|
|
15
|
-
# actually
|
|
15
|
+
# actually means for it — mint a real UUID via
|
|
16
16
|
# Ports::IdentityGeneration, derive something else entirely — this
|
|
17
17
|
# port just resolves the one adapter that domain wired and calls
|
|
18
18
|
# it, the same "one adapter registry-wide, refuse if zero or many"
|
|
@@ -22,10 +22,32 @@ module Hecks
|
|
|
22
22
|
|
|
23
23
|
module_function
|
|
24
24
|
|
|
25
|
+
# Asks the domain's adapter which value a creating command's identity field gets.
|
|
26
|
+
#
|
|
27
|
+
# No adapter, spec double or caller of this port ships in this repository, so every
|
|
28
|
+
# shape below other than `registry` is adapter-defined: the port forwards it untouched.
|
|
29
|
+
#
|
|
30
|
+
# @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
|
|
31
|
+
# and handed on to it
|
|
32
|
+
# @param agg_name [Object] adapter-defined, forwarded unchanged; names the aggregate
|
|
33
|
+
# whose identity field is being assigned
|
|
34
|
+
# @param field_name [Object] adapter-defined, forwarded unchanged; names the identity
|
|
35
|
+
# field
|
|
36
|
+
# @param args [Object] adapter-defined, forwarded unchanged; the creating command's own
|
|
37
|
+
# arguments
|
|
38
|
+
# @return [Object] adapter-defined value to assign as the identity
|
|
39
|
+
# @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
|
|
40
|
+
# (see `adapter`)
|
|
25
41
|
def assign(registry, agg_name:, field_name:, args:)
|
|
26
42
|
adapter(registry).assign(registry, agg_name: agg_name, field_name: field_name, args: args)
|
|
27
43
|
end
|
|
28
44
|
|
|
45
|
+
# Finds the single adapter bound to this port, refusing an ambiguous wiring.
|
|
46
|
+
#
|
|
47
|
+
# @param registry [Runtime::Registry] the booted registry to search
|
|
48
|
+
# @return [Module] the adapter module or class implementing this port
|
|
49
|
+
# @raise [Runtime::WiringError] if no adapter, or more than one, implements this port,
|
|
50
|
+
# or the one that does has no Ruby implementation under `Hecks::Adapters`
|
|
29
51
|
def adapter(registry)
|
|
30
52
|
implementations = registry.adapters.values.select { |a| a.port == NAME }
|
|
31
53
|
|