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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: badbf552a5137cc39f791c4a398fe7d33423ff6d15c82cb018d1ef7c464af754
|
|
4
|
+
data.tar.gz: 5bf8d933f52178d2870d19dd0e33b03cacd060b848b54340c4940edbe94e008e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9327ee7366dacb9dcc90fc155999e5ff055efefc8e840ff4ddb08a7042adfcedbe5396c7547713562bebe1db618017b6851421f48d0ac300ab4339cf674a4cca
|
|
7
|
+
data.tar.gz: 97e1aa8115377269a23b29609e88370f86cfaec8dee9f79889232755dea7a55921d7e3db2fa6c3af26ca4f2afaca56a034fd7a6b5b8413ad00e45aef9c24c0d9
|
|
@@ -6,16 +6,16 @@ require_relative "../../ports/agent"
|
|
|
6
6
|
|
|
7
7
|
module Hecks
|
|
8
8
|
module Adapters
|
|
9
|
-
#
|
|
9
|
+
# **The real `agent` fulfillment** — shells out to the `claude` CLI
|
|
10
10
|
# itself, `claude -p --output-format json`, one process per call.
|
|
11
11
|
# `Ports::Agent`'s own scripted double (`spec/fixtures/scripted_
|
|
12
12
|
# agent.{adapter,rb}`) is the deterministic sibling every spec binds
|
|
13
13
|
# instead, the same relationship `SecureRandomIdentity` already has
|
|
14
14
|
# to `SequentialIdentity`.
|
|
15
15
|
#
|
|
16
|
-
#
|
|
16
|
+
# **This file owns transport only** — spawning the process, unwrapping
|
|
17
17
|
# the CLI's own JSON envelope (`{"result": "..."}`) down to the
|
|
18
|
-
# model's raw text, and parsing
|
|
18
|
+
# model's raw text, and parsing that text as JSON. It hands back a
|
|
19
19
|
# plain Hash. Whether that Hash has the keys a caller asked for, and
|
|
20
20
|
# whether its values are within the closed vocabularies this port
|
|
21
21
|
# recognizes (a critique's `kind`, a proposal's `verb` pattern) is
|
|
@@ -40,7 +40,7 @@ module Hecks
|
|
|
40
40
|
|
|
41
41
|
module_function
|
|
42
42
|
|
|
43
|
-
#
|
|
43
|
+
# **The next best question**. `state` is whatever
|
|
44
44
|
# `Interview::Session#declaration`/`#gaps` produced — passed
|
|
45
45
|
# through as JSON, not reformatted, so this adapter never
|
|
46
46
|
# re-derives what the session already knows.
|
|
@@ -53,7 +53,7 @@ module Hecks
|
|
|
53
53
|
)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
#
|
|
56
|
+
# Prose -> proposed declarations.
|
|
57
57
|
def interpret(prose:, state:)
|
|
58
58
|
call(
|
|
59
59
|
system: SYSTEM_PREFIX + "Given the domain model so far and a sentence the human just said, " \
|
|
@@ -66,7 +66,7 @@ module Hecks
|
|
|
66
66
|
)
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
#
|
|
69
|
+
# What is wrong with this as a model — closed to the same kind
|
|
70
70
|
# vocabulary `Ports::Agent::CRITIQUE_KINDS` declares, spelled out
|
|
71
71
|
# here too since the system prompt is the only place the model
|
|
72
72
|
# itself ever sees that list.
|
|
@@ -82,7 +82,7 @@ module Hecks
|
|
|
82
82
|
)
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
#
|
|
85
|
+
# **Vocabulary help**. Named `suggest_name`, not `name` — see
|
|
86
86
|
# `Ports::Agent#suggest_name`'s own comment for why `name` is
|
|
87
87
|
# never a safe module-function name here.
|
|
88
88
|
def suggest_name(meaning:, kind:, near:)
|
|
@@ -16,15 +16,15 @@ require_relative "../../runtime/instance"
|
|
|
16
16
|
module Hecks
|
|
17
17
|
module Adapters
|
|
18
18
|
# Cloudflare D1 — SQLite, managed, reached over its REST API rather
|
|
19
|
-
# than a local file. D1
|
|
20
|
-
# reuses Sqlite::SchemaBuilder and Sqlite::Codec
|
|
19
|
+
# than a local file. D1 is SQLite, dialect and all, so this file
|
|
20
|
+
# reuses Sqlite::SchemaBuilder and Sqlite::Codec unchanged (the DDL
|
|
21
21
|
# and the column encode/decode) and SqlQueryBuilder's dialect hooks
|
|
22
22
|
# are copied near-verbatim from sqlite.rb — the only real difference
|
|
23
23
|
# is the transport (D1::Connection, an HTTP call per query, vs a
|
|
24
24
|
# persistent local sqlite3 handle). See sqlite.rb's own header
|
|
25
25
|
# comment: "this file supplies only SQLite's dialect" — true here too.
|
|
26
26
|
class D1
|
|
27
|
-
#
|
|
27
|
+
# **The transport** — mirrors just the slice of SQLite3::Database's own
|
|
28
28
|
# interface (execute/get_first_row/get_first_value, rows as
|
|
29
29
|
# column-name-keyed hashes) that Sqlite::SchemaBuilder, Sqlite::Codec,
|
|
30
30
|
# and this file's own methods already assume. One stateless HTTP call
|
|
@@ -33,20 +33,41 @@ module Hecks
|
|
|
33
33
|
class Connection
|
|
34
34
|
ENDPOINT = "https://api.cloudflare.com/client/v4".freeze
|
|
35
35
|
|
|
36
|
+
# @param account_id [String] the Cloudflare account that owns the database
|
|
37
|
+
# @param database_id [String] the D1 database's id
|
|
38
|
+
# @param api_token [String] a Cloudflare API token, sent as a bearer token on every
|
|
39
|
+
# request
|
|
36
40
|
def initialize(account_id:, database_id:, api_token:)
|
|
37
41
|
@uri = URI("#{ENDPOINT}/accounts/#{account_id}/d1/database/#{database_id}/query")
|
|
38
42
|
@api_token = api_token
|
|
39
43
|
end
|
|
40
44
|
|
|
45
|
+
# Runs one statement in its own HTTP request and returns its rows.
|
|
46
|
+
#
|
|
47
|
+
# @param sql [String] the statement, with `?` placeholders
|
|
48
|
+
# @param binds [Array<Object>] one value per placeholder, in order
|
|
49
|
+
# @return [Array<Hash{String => Object}>] the result rows keyed by column name; `[]`
|
|
50
|
+
# for a statement that returns none
|
|
51
|
+
# @raise [Runtime::WiringError] if D1 reports the query failed or answers with a body
|
|
52
|
+
# that is not JSON
|
|
41
53
|
def execute(sql, binds = [])
|
|
42
54
|
response_results({ sql: sql, params: binds }).first.fetch("results", [])
|
|
43
55
|
end
|
|
44
56
|
|
|
57
|
+
# Runs several statements as one transaction in a single HTTP request.
|
|
58
|
+
#
|
|
45
59
|
# D1 batches are SQL transactions: statements execute in order and a
|
|
46
60
|
# failure rolls the entire sequence back. Keep the tuple-shaped local
|
|
47
61
|
# seam small so adapter code and focused fakes do not need to know the
|
|
48
62
|
# REST request envelope.
|
|
49
63
|
# https://developers.cloudflare.com/d1/worker-api/d1-database/#batch
|
|
64
|
+
#
|
|
65
|
+
# @param statements [Array<Array(String, Array)>] `[sql, binds]` pairs in execution
|
|
66
|
+
# order; nil binds mean none
|
|
67
|
+
# @return [Array<Array<Hash{String => Object}>>] each statement's result rows, in
|
|
68
|
+
# the order given
|
|
69
|
+
# @raise [Runtime::WiringError] if D1 reports the request or any one statement failed,
|
|
70
|
+
# or answers with a body that is not JSON
|
|
50
71
|
def batch(statements)
|
|
51
72
|
payload = {
|
|
52
73
|
batch: statements.map do |sql, binds|
|
|
@@ -83,10 +104,10 @@ module Hecks
|
|
|
83
104
|
raise Runtime::WiringError, "D1 query failed: non-JSON response (HTTP #{response.code}): #{response.body}"
|
|
84
105
|
end
|
|
85
106
|
|
|
86
|
-
# `messages` is the whole-response :errors fallback, tried
|
|
107
|
+
# `messages` is the whole-response :errors fallback, tried last —
|
|
87
108
|
# a per-statement `failed["error"]`/`failed["message"]` (checked
|
|
88
109
|
# by key presence, not truthiness, so an explicit `nil` still
|
|
89
|
-
# counts as "the key was there") is always more specific to
|
|
110
|
+
# counts as "the key was there") is always more specific to which
|
|
90
111
|
# statement failed, when either is present.
|
|
91
112
|
def failed_statement_detail(failed, messages)
|
|
92
113
|
detail =
|
|
@@ -102,10 +123,26 @@ module Hecks
|
|
|
102
123
|
|
|
103
124
|
public
|
|
104
125
|
|
|
126
|
+
# Runs one statement and keeps only its first row, as `SQLite3::Database` does.
|
|
127
|
+
#
|
|
128
|
+
# @param sql [String] the statement, with `?` placeholders
|
|
129
|
+
# @param binds [Array<Object>] one value per placeholder, in order
|
|
130
|
+
# @return [Hash{String => Object}, nil] the first row keyed by column name, or nil when
|
|
131
|
+
# the statement returned none
|
|
132
|
+
# @raise [Runtime::WiringError] if D1 reports the query failed or answers with a body
|
|
133
|
+
# that is not JSON
|
|
105
134
|
def get_first_row(sql, binds = [])
|
|
106
135
|
execute(sql, binds).first
|
|
107
136
|
end
|
|
108
137
|
|
|
138
|
+
# Runs one statement and keeps only the first column of its first row.
|
|
139
|
+
#
|
|
140
|
+
# @param sql [String] the statement, with `?` placeholders
|
|
141
|
+
# @param binds [Array<Object>] one value per placeholder, in order
|
|
142
|
+
# @return [Object, nil] the value as D1's JSON carries it, or nil when the statement
|
|
143
|
+
# returned no row
|
|
144
|
+
# @raise [Runtime::WiringError] if D1 reports the query failed or answers with a body
|
|
145
|
+
# that is not JSON
|
|
109
146
|
def get_first_value(sql, binds = [])
|
|
110
147
|
get_first_row(sql, binds)&.values&.first
|
|
111
148
|
end
|
|
@@ -119,8 +156,22 @@ module Hecks
|
|
|
119
156
|
|
|
120
157
|
attr_reader :aggregate
|
|
121
158
|
|
|
159
|
+
# Names the optional persistence capabilities `Ports::Persistence::AppendOnly` may rely on.
|
|
160
|
+
#
|
|
161
|
+
# @return [Array<Symbol>] `[:atomic_put]`
|
|
122
162
|
def persistence_capabilities = [:atomic_put]
|
|
123
163
|
|
|
164
|
+
# Checks the credentials are declared and creates the aggregate, journal, event and
|
|
165
|
+
# saga tables if absent, one HTTP request per statement.
|
|
166
|
+
#
|
|
167
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose table this adapter owns
|
|
168
|
+
# @param settings [Hash{Symbol, String => Object}] world settings for the binding:
|
|
169
|
+
# `account_id`, `database_id` and `api_token` (all required) and `domain` (scopes saga
|
|
170
|
+
# rows, default the aggregate's name), each read under a Symbol or a String key
|
|
171
|
+
# @param root [String, nil] project root directory; accepted for the shared adapter
|
|
172
|
+
# constructor shape and ignored
|
|
173
|
+
# @raise [Runtime::WiringError] if a required setting is missing or empty, or D1 rejects
|
|
174
|
+
# a table-creation statement
|
|
124
175
|
def initialize(aggregate:, settings: {}, root: nil)
|
|
125
176
|
@aggregate = aggregate
|
|
126
177
|
|
|
@@ -132,7 +183,7 @@ module Hecks
|
|
|
132
183
|
end
|
|
133
184
|
|
|
134
185
|
@db = Connection.new(account_id: account_id, database_id: database_id, api_token: api_token)
|
|
135
|
-
#
|
|
186
|
+
# The optional saga-persistence capability's own scoping column
|
|
136
187
|
# (§2/§4) — D1's domain isolation is by whole-database identity
|
|
137
188
|
# (one D1 database per domain in practice), so unlike Sqlite's
|
|
138
189
|
# own per-aggregate-file default, there's no "which file does
|
|
@@ -159,8 +210,16 @@ module Hecks
|
|
|
159
210
|
create_saga_table!
|
|
160
211
|
end
|
|
161
212
|
|
|
213
|
+
# Names the aggregate's table; the journal table is keyed off it.
|
|
214
|
+
#
|
|
215
|
+
# @return [String] the aggregate's snake_case storage name, unquoted
|
|
162
216
|
def table = @aggregate.storage_name
|
|
163
217
|
|
|
218
|
+
# Reads the current row for one aggregate identity.
|
|
219
|
+
#
|
|
220
|
+
# @param id [String, Object] the aggregate identity, bound as `id.to_s`
|
|
221
|
+
# @return [Runtime::Instance, nil] the decoded record, or nil when no row has that id
|
|
222
|
+
# @raise [Runtime::WiringError] if D1 rejects the statement
|
|
164
223
|
def find(id)
|
|
165
224
|
row = @db.get_first_row("SELECT * FROM #{quoted_table} WHERE id = ?", [id.to_s])
|
|
166
225
|
return nil unless row
|
|
@@ -168,9 +227,19 @@ module Hecks
|
|
|
168
227
|
Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
|
|
169
228
|
end
|
|
170
229
|
|
|
171
|
-
#
|
|
230
|
+
# Lists every stored record, ordered by id unless an ordering attribute is given.
|
|
231
|
+
#
|
|
232
|
+
# order_by is a runtime value — see postgres.rb's own all for the
|
|
172
233
|
# full reasoning; whitelisted the identical way, same order_clause
|
|
173
234
|
# Sqlite's own all reuses (D1 speaks the identical dialect).
|
|
235
|
+
#
|
|
236
|
+
# @param order_by [String, Symbol, nil] attribute (or dotted value-object path) to sort
|
|
237
|
+
# by, with id as the tie-break; nil orders by id alone
|
|
238
|
+
# @param direction [Symbol, String] `:asc` or `:desc`, case-insensitive; anything else
|
|
239
|
+
# sorts ascending
|
|
240
|
+
# @return [Array<Runtime::Instance>] the decoded records, `[]` when the table is empty
|
|
241
|
+
# @raise [Runtime::WiringError] if `order_by` names no attribute of the aggregate, or D1
|
|
242
|
+
# rejects the statement
|
|
174
243
|
def all(order_by: nil, direction: :asc)
|
|
175
244
|
order_sql = "ORDER BY id"
|
|
176
245
|
if order_by
|
|
@@ -189,21 +258,37 @@ module Hecks
|
|
|
189
258
|
end
|
|
190
259
|
end
|
|
191
260
|
|
|
261
|
+
# Counts the rows in the aggregate's table, deleted records excluded.
|
|
262
|
+
#
|
|
263
|
+
# @return [Integer] number of current records
|
|
264
|
+
# @raise [Runtime::WiringError] if D1 rejects the statement
|
|
192
265
|
def count = @db.get_first_value("SELECT COUNT(*) FROM #{quoted_table}").to_i
|
|
193
266
|
|
|
267
|
+
# Inserts one journal row in its own HTTP request.
|
|
268
|
+
#
|
|
269
|
+
# @param entry [Ports::Persistence::Entry] the save or delete to journal; `state` is
|
|
270
|
+
# encoded through the state codec and `mirrors` stored as JSON, or NULL when nil
|
|
271
|
+
# @return [Ports::Persistence::Entry] the same `entry`
|
|
272
|
+
# @raise [Runtime::WiringError] if D1 rejects the insert
|
|
194
273
|
def append(entry)
|
|
195
274
|
@db.execute(
|
|
196
275
|
"INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES (?, ?, ?, ?)",
|
|
197
|
-
# `mirrors` (unlike `state`) is a
|
|
276
|
+
# `mirrors` (unlike `state`) is a nullable column — an absent
|
|
198
277
|
# mirrors hash must bind a real SQL NULL, not the four-character
|
|
199
278
|
# JSON text `"null"` (`JSON.generate(nil)`), or a future `IS NULL`
|
|
200
279
|
# check against it would never match. Same guard `postgres_era.rb`
|
|
201
280
|
# already uses for its own journal's `mirrors` column.
|
|
202
|
-
[entry.id, entry.operation,
|
|
281
|
+
[entry.id, entry.operation, state_json(entry.state), entry.mirrors && JSON.generate(entry.mirrors)]
|
|
203
282
|
)
|
|
204
283
|
entry
|
|
205
284
|
end
|
|
206
285
|
|
|
286
|
+
# Replaces or deletes the aggregate's row for one journal entry.
|
|
287
|
+
#
|
|
288
|
+
# @param entry [Ports::Persistence::Entry] the save or delete to materialize
|
|
289
|
+
# @return [Runtime::Instance, Array] for a save, a new instance over the entry's state;
|
|
290
|
+
# for a delete, the `DELETE` statement's empty result rows
|
|
291
|
+
# @raise [Runtime::WiringError] if D1 rejects the statement
|
|
207
292
|
def project(entry)
|
|
208
293
|
return @db.execute("DELETE FROM #{quoted_table} WHERE id = ?", [entry.id]) if entry.delete?
|
|
209
294
|
|
|
@@ -219,59 +304,89 @@ module Hecks
|
|
|
219
304
|
instance
|
|
220
305
|
end
|
|
221
306
|
|
|
307
|
+
# Reads the whole journal back in append order, for `AppendOnly#recover!` to replay.
|
|
308
|
+
#
|
|
309
|
+
# @return [Array<Ports::Persistence::Entry>] every journalled entry, state decoded
|
|
310
|
+
# through the state codec and `mirrors` parsed with String keys (nil when none were
|
|
311
|
+
# stored); a NULL `operation` reads as `"save"`; `[]` when nothing has been appended
|
|
312
|
+
# @raise [Runtime::WiringError] if D1 rejects the statement
|
|
313
|
+
# @raise [JSON::ParserError] if a stored `state` or `mirrors` value is not valid JSON
|
|
222
314
|
def entries
|
|
223
315
|
@db.execute("SELECT aggregate_id, operation, state, mirrors FROM #{quoted_entry_table} ORDER BY sequence").map do |row|
|
|
224
316
|
state = JSON.parse(row["state"])
|
|
225
317
|
Ports::Persistence::Entry.new(
|
|
226
318
|
operation: row["operation"] || "save",
|
|
227
319
|
id: row["aggregate_id"],
|
|
228
|
-
state: state
|
|
320
|
+
state: Ports::Persistence::StateCodec.decode(@aggregate, state),
|
|
229
321
|
mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
|
|
230
322
|
)
|
|
231
323
|
end
|
|
232
324
|
end
|
|
233
325
|
|
|
326
|
+
# Deletes every row of the aggregate's table and its journal; events and saga rows
|
|
327
|
+
# are left in place.
|
|
328
|
+
#
|
|
329
|
+
# @return [Adapters::D1] self
|
|
330
|
+
# @raise [Runtime::WiringError] if D1 rejects a statement
|
|
234
331
|
def reset!
|
|
235
332
|
@db.execute("DELETE FROM #{quoted_table}")
|
|
236
333
|
@db.execute("DELETE FROM #{quoted_entry_table}")
|
|
237
334
|
self
|
|
238
335
|
end
|
|
239
336
|
|
|
337
|
+
# Journals and then replaces an instance's current state, as two separate HTTP
|
|
338
|
+
# requests rather than one batch.
|
|
339
|
+
#
|
|
340
|
+
# @param instance [Runtime::Instance] the instance to store
|
|
341
|
+
# @return [Runtime::Instance] a new instance over a shallow copy of the saved state
|
|
342
|
+
# @raise [Runtime::WiringError] if D1 rejects either statement; a journal row already
|
|
343
|
+
# inserted stays
|
|
240
344
|
def save(instance)
|
|
241
345
|
entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
|
|
242
346
|
append(entry)
|
|
243
347
|
project(entry)
|
|
244
348
|
end
|
|
245
349
|
|
|
350
|
+
# Stores an entry and reports whether it inserted, replaced or conflicted, all in one
|
|
351
|
+
# batched HTTP request.
|
|
352
|
+
#
|
|
246
353
|
# Classification, durable journal append and current-state projection
|
|
247
354
|
# are one D1 batch transaction and therefore one HTTP request. The first
|
|
248
355
|
# statement supplies the outcome from database state inside that same
|
|
249
356
|
# transaction; the runtime performs no preliminary find.
|
|
250
357
|
#
|
|
251
|
-
# `insert_only
|
|
252
|
-
# out whether the row
|
|
358
|
+
# Under `insert_only:`, spending a separate, earlier round trip finding
|
|
359
|
+
# out whether the row exists before building the batch would be a real
|
|
253
360
|
# TOCTOU gap (two concurrent creates at the same identity could both
|
|
254
361
|
# pass that check before either wrote). D1's batch has no conditional
|
|
255
|
-
#
|
|
362
|
+
# branch of its own, true, but it does not need one: a batch's own
|
|
256
363
|
# statements already execute in order, atomically, as one transaction
|
|
257
364
|
# (Connection#batch's own comment) — the exact guarantee the single-
|
|
258
365
|
# connection adapters' own `@db.transaction do ... end` gets locally.
|
|
259
|
-
# So the existence check
|
|
366
|
+
# So the existence check sits inside the batch as its own first
|
|
260
367
|
# statement, and the two writes are individually gated with `WHERE NOT
|
|
261
368
|
# EXISTS (...)` against that same table, evaluated in the same
|
|
262
369
|
# transaction — a row that already existed makes both writes into
|
|
263
370
|
# real, zero-row no-ops rather than skipping them from the Ruby side,
|
|
264
|
-
# matching Sqlite#atomic_put's `next` (skip append
|
|
371
|
+
# matching Sqlite#atomic_put's `next` (skip append and project both,
|
|
265
372
|
# together) with no second HTTP call and no gap for another writer to
|
|
266
373
|
# land in between the check and the write.
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
#
|
|
374
|
+
#
|
|
375
|
+
# Three SQL statements, built here and batched together as one
|
|
376
|
+
# transaction below — see the paragraph above on the real TOCTOU gap
|
|
377
|
+
# this exact shape closes (the existence check inside the
|
|
270
378
|
# batch, not run as a separate earlier round trip). Splitting the
|
|
271
379
|
# per-statement builders out would still need columns/values/slots/
|
|
272
380
|
# not_exists/quoted_table threaded into each, and would separate
|
|
273
|
-
# three pieces of
|
|
381
|
+
# three pieces of one atomic batch across methods with no single
|
|
274
382
|
# place left to see that they are, together, the fix.
|
|
383
|
+
#
|
|
384
|
+
# @param entry [Ports::Persistence::Entry] the save to store
|
|
385
|
+
# @param insert_only [Boolean] when true, an existing row turns both writes into
|
|
386
|
+
# zero-row no-ops
|
|
387
|
+
# @return [Symbol] `:inserted`, `:replaced`, or `:conflicted` when `insert_only` met an
|
|
388
|
+
# existing row and nothing was written
|
|
389
|
+
# @raise [Runtime::WiringError] if D1 rejects the batch; the whole batch is rolled back
|
|
275
390
|
# rubocop:disable-next Metrics/AbcSize
|
|
276
391
|
# rubocop:disable-next Metrics/MethodLength
|
|
277
392
|
def atomic_put(entry, insert_only: false)
|
|
@@ -290,7 +405,7 @@ module Hecks
|
|
|
290
405
|
"THEN 'replaced' ELSE 'inserted' END AS status"
|
|
291
406
|
end
|
|
292
407
|
|
|
293
|
-
# `mirrors` is
|
|
408
|
+
# `mirrors` is nullable (unlike `state`) — see `append`'s own comment.
|
|
294
409
|
encoded_mirrors = entry.mirrors && JSON.generate(entry.mirrors)
|
|
295
410
|
|
|
296
411
|
entry_sql, entry_binds =
|
|
@@ -298,12 +413,12 @@ module Hecks
|
|
|
298
413
|
[
|
|
299
414
|
"INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) " \
|
|
300
415
|
"SELECT ?, ?, ?, ? #{not_exists}",
|
|
301
|
-
[entry.id, entry.operation,
|
|
416
|
+
[entry.id, entry.operation, state_json(entry.state), encoded_mirrors, entry.id.to_s]
|
|
302
417
|
]
|
|
303
418
|
else
|
|
304
419
|
[
|
|
305
420
|
"INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES (?, ?, ?, ?)",
|
|
306
|
-
[entry.id, entry.operation,
|
|
421
|
+
[entry.id, entry.operation, state_json(entry.state), encoded_mirrors]
|
|
307
422
|
]
|
|
308
423
|
end
|
|
309
424
|
|
|
@@ -329,6 +444,12 @@ module Hecks
|
|
|
329
444
|
results.fetch(0).fetch(0).fetch("status").to_sym
|
|
330
445
|
end
|
|
331
446
|
|
|
447
|
+
# Journals a delete and then removes the row, whether or not a row exists, as two
|
|
448
|
+
# separate HTTP requests.
|
|
449
|
+
#
|
|
450
|
+
# @param id [String, Object] the aggregate identity, journalled as `id.to_s`
|
|
451
|
+
# @return [Boolean] always true
|
|
452
|
+
# @raise [Runtime::WiringError] if D1 rejects either statement
|
|
332
453
|
def delete(id)
|
|
333
454
|
entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
|
|
334
455
|
append(entry)
|
|
@@ -336,6 +457,11 @@ module Hecks
|
|
|
336
457
|
true
|
|
337
458
|
end
|
|
338
459
|
|
|
460
|
+
# Inserts an emitted event into the database's shared `events` table.
|
|
461
|
+
#
|
|
462
|
+
# @param event [Runtime::Event] the emitted event; `payload` is stored as JSON
|
|
463
|
+
# @return [Array] the insert's empty result rows; callers ignore it
|
|
464
|
+
# @raise [Runtime::WiringError] if D1 rejects the insert
|
|
339
465
|
def record_event(event)
|
|
340
466
|
@db.execute(
|
|
341
467
|
"INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES (?, ?, ?, ?, ?)",
|
|
@@ -343,6 +469,12 @@ module Hecks
|
|
|
343
469
|
)
|
|
344
470
|
end
|
|
345
471
|
|
|
472
|
+
# Reads back every recorded event in insertion order — the database's whole `events`
|
|
473
|
+
# table, not only this aggregate's rows.
|
|
474
|
+
#
|
|
475
|
+
# @return [Array<Runtime::Event>] the stored events, `payload` parsed with Symbol keys
|
|
476
|
+
# and `occurred_at` as stored; `[]` when none are recorded
|
|
477
|
+
# @raise [Runtime::WiringError] if D1 rejects the statement
|
|
346
478
|
def events
|
|
347
479
|
@db.execute("SELECT * FROM events ORDER BY id").map do |row|
|
|
348
480
|
Runtime::Event.new(
|
|
@@ -355,10 +487,23 @@ module Hecks
|
|
|
355
487
|
end
|
|
356
488
|
end
|
|
357
489
|
|
|
358
|
-
#
|
|
490
|
+
# Replaces one saga instance's checkpoint, keyed by domain, process manager and
|
|
491
|
+
# correlation.
|
|
492
|
+
#
|
|
493
|
+
# ── the optional saga-persistence capability (§2) — reuses the DDL
|
|
359
494
|
# `Sqlite::SchemaBuilder` already shares with Sqlite (`d1.rb`'s own
|
|
360
495
|
# file header). Same `?`-placeholder shape every other write here
|
|
361
496
|
# already uses through `Connection#execute`.
|
|
497
|
+
#
|
|
498
|
+
# @param process_manager [String, Symbol] the process manager's name
|
|
499
|
+
# @param correlation [String, Object] the instance's correlation value, stored as
|
|
500
|
+
# `correlation.to_s`
|
|
501
|
+
# @param state [String, Symbol] the saga's current state name
|
|
502
|
+
# @param memory [Hash] the saga's memory; must be JSON-serializable
|
|
503
|
+
# @param completed_compensations [Array] the ledger of completed compensable legs; must
|
|
504
|
+
# be JSON-serializable
|
|
505
|
+
# @return [Array] the statement's empty result rows; callers ignore it
|
|
506
|
+
# @raise [Runtime::WiringError] if D1 rejects the statement
|
|
362
507
|
def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: [])
|
|
363
508
|
@db.execute(
|
|
364
509
|
"INSERT OR REPLACE INTO hecks_saga_instances (domain, process_manager, correlation, state, memory, " \
|
|
@@ -368,6 +513,13 @@ module Hecks
|
|
|
368
513
|
)
|
|
369
514
|
end
|
|
370
515
|
|
|
516
|
+
# Removes a finished saga instance's checkpoint; a missing row is not an error.
|
|
517
|
+
#
|
|
518
|
+
# @param process_manager [String, Symbol] the process manager's name
|
|
519
|
+
# @param correlation [String, Object] the instance's correlation value, matched as
|
|
520
|
+
# `correlation.to_s`
|
|
521
|
+
# @return [Array] the statement's empty result rows; callers ignore it
|
|
522
|
+
# @raise [Runtime::WiringError] if D1 rejects the statement
|
|
371
523
|
def delete_saga(process_manager:, correlation:)
|
|
372
524
|
@db.execute(
|
|
373
525
|
"DELETE FROM hecks_saga_instances WHERE domain = ? AND process_manager = ? AND correlation = ?",
|
|
@@ -375,6 +527,18 @@ module Hecks
|
|
|
375
527
|
)
|
|
376
528
|
end
|
|
377
529
|
|
|
530
|
+
# Yields every checkpointed saga instance of this adapter's domain, for
|
|
531
|
+
# `Registry#rehydrate_sagas!` to restore at boot.
|
|
532
|
+
#
|
|
533
|
+
# @yieldparam process_manager [String] the process manager's name
|
|
534
|
+
# @yieldparam correlation [String] the instance's correlation value
|
|
535
|
+
# @yieldparam state [String] the saga's state name
|
|
536
|
+
# @yieldparam memory [Hash{Symbol => Object}] the saga's memory, Symbol keys at every depth
|
|
537
|
+
# @yieldparam completed_compensations [Array] the completed-compensation ledger, `[]`
|
|
538
|
+
# when the column is NULL
|
|
539
|
+
# @return [Enumerator, Array<Hash>] an enumerator over the same five values when no
|
|
540
|
+
# block is given; otherwise the raw result rows
|
|
541
|
+
# @raise [Runtime::WiringError] if D1 rejects the statement
|
|
378
542
|
def each_saga
|
|
379
543
|
return enum_for(:each_saga) unless block_given?
|
|
380
544
|
|
|
@@ -31,7 +31,7 @@ module Hecks
|
|
|
31
31
|
load_each(File.join(root, ADAPTERS), %w[*.adapter */*.adapter */*/*.adapter])
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
#
|
|
34
|
+
# Chapters first, judged once, then everything that reads them.
|
|
35
35
|
# A chapter may be split across files (the language's own grammar is
|
|
36
36
|
# nine), and judging file one before files two-through-nine exist
|
|
37
37
|
# refuses references that are perfectly well declared a file later —
|
|
@@ -39,8 +39,8 @@ module Hecks
|
|
|
39
39
|
# already places every `*.bluebook` ahead of hecksagons and worlds,
|
|
40
40
|
# so the window ends at the last chapter pattern rather than at a
|
|
41
41
|
# hand-written list this would otherwise have to keep in step.
|
|
42
|
-
# `environment:` —
|
|
43
|
-
#
|
|
42
|
+
# `environment:` — one more pair of files, loaded last, not a glob.
|
|
43
|
+
# Recovered, not new — see Runtime::Loader.boot's own comment for
|
|
44
44
|
# the provenance. A caller passing `Hecks.boot(path, environment:
|
|
45
45
|
# "production")` gets exactly `environments/production.hecksagon`
|
|
46
46
|
# and `environments/production.world` loaded, whichever exist (a
|
|
@@ -53,7 +53,7 @@ module Hecks
|
|
|
53
53
|
# globs (all non-recursive, none named `environments/*`), so this
|
|
54
54
|
# is the only thing that ever reaches them. Loaded as genuine
|
|
55
55
|
# `Hecks.hecksagon "SameDomain" do ... end` / `Hecks.world
|
|
56
|
-
# "SameDomain" do ... end` blocks —
|
|
56
|
+
# "SameDomain" do ... end` blocks — merged into the base file's
|
|
57
57
|
# own hecksagon/world (Registry#add_hecksagon / #add_world,
|
|
58
58
|
# concatenate/override rather than replace), so an overlay can
|
|
59
59
|
# rebind or add settings for anything the base file declared
|
|
@@ -73,7 +73,7 @@ module Hecks
|
|
|
73
73
|
load_each(directory, [File.join("environments", "#{environment}.world")])
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
#
|
|
76
|
+
# Every bluebook in a folder is one declaration set. Individual files
|
|
77
77
|
# remain organized in the domain expert's language; the folder is the
|
|
78
78
|
# unit callers load. Builders group declarations by the chapter name in
|
|
79
79
|
# each file, so a folder may hold more than one chapter without a catalog.
|
|
@@ -84,7 +84,7 @@ module Hecks
|
|
|
84
84
|
Bluebook::MetaValidator.judge_deferred!(Hecks.current_registry)
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
-
#
|
|
87
|
+
# The explicit-file sibling of `load_domain` — for a caller that names
|
|
88
88
|
# its own exact files rather than a directory to glob (`Loader.boot_files`,
|
|
89
89
|
# behind `Hecks.boot_files`). No `Dir.glob`, no copying: every path here
|
|
90
90
|
# is a real file on disk, wherever it actually lives, loaded in place —
|
|
@@ -93,7 +93,7 @@ module Hecks
|
|
|
93
93
|
# tmpdir (see Loader.boot_files's own header for why that pattern is a
|
|
94
94
|
# hazard, not a convenience).
|
|
95
95
|
#
|
|
96
|
-
#
|
|
96
|
+
# Ordered by category, not by the caller's own list order — same four
|
|
97
97
|
# groups `Vocabulary.fetch("LoadOrder")` walks a directory in
|
|
98
98
|
# (bluebook chapters, translations, hecksagons, worlds), because a
|
|
99
99
|
# hecksagon can reference a bluebook's own constants and must not load
|
|
@@ -138,11 +138,11 @@ module Hecks
|
|
|
138
138
|
raise Errno::ENOENT, "no such domain directory: #{path}"
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
-
#
|
|
141
|
+
# **The domain you are standing in**. Walks up from `from` — the way git
|
|
142
142
|
# finds `.git` — and answers the nearest directory a boot would accept,
|
|
143
143
|
# or nil if there is not one above you.
|
|
144
144
|
#
|
|
145
|
-
#
|
|
145
|
+
# Marked by a `.hecksagon`, not by a `.bluebook`. Chapters are
|
|
146
146
|
# everywhere: era translations, the language's own self-hosted grammar,
|
|
147
147
|
# and `spec/fixtures`, which holds a dozen unrelated ones in a single
|
|
148
148
|
# directory. A `.hecksagon` is the file that says "this is a domain, and
|
|
@@ -152,7 +152,7 @@ module Hecks
|
|
|
152
152
|
# Both layouts, because `bluebook_directory` above accepts both: a
|
|
153
153
|
# domain directory holding a `bluebook/` subdirectory (every example in
|
|
154
154
|
# this corpus), or one holding the files directly.
|
|
155
|
-
#
|
|
155
|
+
# Normalised to the outer directory. Standing in `examples/banking/bluebook`,
|
|
156
156
|
# the `.hecksagon` is right there, so a plain walk stops on the
|
|
157
157
|
# `bluebook/` directory itself. Both boot identically — `bluebook_directory`
|
|
158
158
|
# accepts either and `Loader.boot` takes `File.dirname` of what it gets,
|
|
@@ -4,26 +4,26 @@ require_relative "../../ports/authentication"
|
|
|
4
4
|
|
|
5
5
|
module Hecks
|
|
6
6
|
module Adapters
|
|
7
|
-
#
|
|
7
|
+
# **Google's own OIDC handshake** — the `authentication` port's one real
|
|
8
8
|
# implementation today, moved here from being hand-rolled per-app
|
|
9
9
|
# (an embryonaut_console `google_auth.rb` used to do exactly this;
|
|
10
10
|
# any hecks-based app gets Google sign-in for free now, the
|
|
11
11
|
# same "one adapter, reusable everywhere" value every other adapter
|
|
12
12
|
# in this directory already has).
|
|
13
13
|
#
|
|
14
|
-
# `oauth2` does
|
|
14
|
+
# `oauth2` does only the authorization-code exchange (no Rack
|
|
15
15
|
# middleware, no Omniauth strategy indirection) ; `google-id-token`
|
|
16
|
-
# does
|
|
16
|
+
# does only ID-token verification (signature checked against
|
|
17
17
|
# Google's real, rotating JWKS — real, maintained code, never
|
|
18
18
|
# hand-rolled here). Neither library decides what a verified
|
|
19
|
-
# (issuer, subject)
|
|
19
|
+
# (issuer, subject) means — that's `Ports::IdentityResolution`'s
|
|
20
20
|
# job, called by whoever consumes this port's `verify`.
|
|
21
21
|
#
|
|
22
|
-
#
|
|
22
|
+
# Lazy requires, same reasoning `Postgres.connect_for` already
|
|
23
23
|
# holds itself to for `pg`: a domain that never binds
|
|
24
24
|
# `authentication` to this adapter should never need these gems
|
|
25
|
-
# installed. This
|
|
26
|
-
# unconditional require_relative list) ; the
|
|
25
|
+
# installed. This file loads in every boot (driven.rb's own
|
|
26
|
+
# unconditional require_relative list) ; the gems load only where
|
|
27
27
|
# a real handshake actually happens.
|
|
28
28
|
#
|
|
29
29
|
# `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET`/`GOOGLE_REDIRECT_URI` —
|
|
@@ -68,7 +68,7 @@ module Hecks
|
|
|
68
68
|
# itself. `email_verified` rides along because a caller granting
|
|
69
69
|
# access off this email needs to know Google actually checked it.
|
|
70
70
|
def verify(code:, state:, expected_state:)
|
|
71
|
-
#
|
|
71
|
+
# **Both gems, before anything else** — not staggered further down
|
|
72
72
|
# this method: the rescue clause below names GoogleIDToken
|
|
73
73
|
# ::ValidationError, and Ruby resolves that constant reference
|
|
74
74
|
# at the moment an exception is being matched, not at parse
|