hecks 1.3.0 → 1.5.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 +72 -7
- data/lib/hecks/adapters/driven/d1.rb +187 -23
- data/lib/hecks/adapters/driven/folder.rb +83 -10
- data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
- data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
- data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
- data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +106 -9
- data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
- data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
- data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
- data/lib/hecks/adapters/driven/lambda.rb +82 -35
- data/lib/hecks/adapters/driven/local_storage.rb +83 -10
- data/lib/hecks/adapters/driven/memory.rb +205 -9
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -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 +50 -11
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- 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/system_clock.rb +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
- data/lib/hecks/adapters/driven.rb +6 -4
- data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
- data/lib/hecks/behaviors/dsl.rb +60 -2
- data/lib/hecks/behaviors/expectations.rb +190 -29
- data/lib/hecks/behaviors/ir.rb +12 -1
- data/lib/hecks/behaviors/rspec.rb +9 -1
- data/lib/hecks/behaviors/runner.rb +21 -2
- data/lib/hecks/behaviors.rb +9 -1
- data/lib/hecks/bluebook/aggregate.rb +43 -13
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
- data/lib/hecks/bluebook/assembly/build.rb +26 -5
- data/lib/hecks/bluebook/assembly/contract.rb +98 -23
- data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
- data/lib/hecks/bluebook/assembly/marks.rb +159 -30
- data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
- data/lib/hecks/bluebook/assembly.rb +32 -14
- data/lib/hecks/bluebook/attribute.rb +26 -12
- data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
- data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
- data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
- data/lib/hecks/bluebook/behaviour/command.rb +55 -25
- data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
- data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
- data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
- data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
- data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
- data/lib/hecks/bluebook/behaviour/query.rb +6 -1
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
- data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
- data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
- data/lib/hecks/bluebook/capabilities.rb +27 -0
- data/lib/hecks/bluebook/chapter.rb +51 -9
- data/lib/hecks/bluebook/command.rb +62 -17
- data/lib/hecks/bluebook/domain_port.rb +34 -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 +282 -123
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
- data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
- 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 +191 -61
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
- 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 +111 -47
- data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
- data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
- data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
- 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 +40 -11
- data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
- data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
- data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
- data/lib/hecks/bluebook/expression/resolver.rb +369 -128
- data/lib/hecks/bluebook/hexagon.rb +35 -1
- data/lib/hecks/bluebook/lifecycle.rb +12 -1
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
- data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
- data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
- data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
- data/lib/hecks/bluebook/meta_validator.rb +235 -139
- data/lib/hecks/bluebook/model_check.rb +434 -104
- data/lib/hecks/bluebook/pattern_subset.rb +32 -10
- data/lib/hecks/bluebook/policy.rb +15 -13
- data/lib/hecks/bluebook/process_manager.rb +27 -14
- data/lib/hecks/bluebook/project_discovery.rb +5 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +50 -6
- data/lib/hecks/bluebook/query.rb +31 -4
- data/lib/hecks/bluebook/read_model.rb +35 -15
- data/lib/hecks/bluebook/reference.rb +26 -13
- data/lib/hecks/bluebook/smoke_test.rb +46 -23
- data/lib/hecks/bluebook/synthesizer.rb +46 -12
- data/lib/hecks/bluebook/translation.rb +34 -5
- data/lib/hecks/bluebook/value_object.rb +29 -11
- data/lib/hecks/bluebook.rb +5 -6
- data/lib/hecks/codemod.rb +138 -50
- data/lib/hecks/construct.rb +21 -7
- data/lib/hecks/corpus.rb +438 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/doc/reference.rb +200 -31
- data/lib/hecks/embryonaut_bluebook.rb +38 -15
- 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 +155 -35
- data/lib/hecks/facade/json_door.rb +106 -25
- data/lib/hecks/facade/surface/aggregate_door.rb +50 -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 +183 -19
- data/lib/hecks/forms/html.rb +51 -7
- data/lib/hecks/forms/index_renderer.rb +14 -2
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/params.rb +120 -23
- data/lib/hecks/forms/port_argument.rb +14 -2
- data/lib/hecks/forms/query_form_renderer.rb +65 -2
- data/lib/hecks/forms/record_renderer.rb +60 -2
- data/lib/hecks/forms/record_table.rb +28 -1
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +13 -3
- data/lib/hecks/forms.rb +24 -4
- data/lib/hecks/fqn.rb +59 -1
- data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +79 -25
- data/lib/hecks/freezer.rb +27 -11
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
- data/lib/hecks/fuzzing/combination_miner.rb +178 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
- data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
- data/lib/hecks/fuzzing/differential.rb +192 -0
- data/lib/hecks/fuzzing/domain_generator.rb +871 -0
- data/lib/hecks/fuzzing/era_boundary.rb +68 -19
- data/lib/hecks/fuzzing/form_census.rb +121 -20
- data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
- data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
- data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
- data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
- data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
- data/lib/hecks/fuzzing/properties/guards.rb +86 -42
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
- data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
- data/lib/hecks/fuzzing/properties/querying.rb +82 -28
- data/lib/hecks/fuzzing/properties.rb +84 -51
- data/lib/hecks/fuzzing/qa_settings.rb +164 -0
- data/lib/hecks/fuzzing/replay.rb +241 -133
- data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
- data/lib/hecks/fuzzing/self_consistency.rb +315 -137
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
- 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 +59 -32
- data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
- data/lib/hecks/fuzzing/shrinker.rb +309 -0
- data/lib/hecks/fuzzing/structural_skips.rb +37 -130
- data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
- data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
- data/lib/hecks/fuzzing/value_generator.rb +110 -22
- data/lib/hecks/fuzzing.rb +5 -0
- data/lib/hecks/grammar/evolve.rb +188 -12
- data/lib/hecks/grammar.rb +53 -7
- data/lib/hecks/ir.rb +51 -20
- 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/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/language/oidc.json +5 -0
- data/lib/hecks/literal.rb +41 -9
- data/lib/hecks/naming.rb +112 -31
- data/lib/hecks/ports/access_control.rb +53 -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 +50 -11
- data/lib/hecks/ports/clock.rb +42 -23
- data/lib/hecks/ports/extraction.rb +16 -0
- data/lib/hecks/ports/identity_assignment.rb +23 -2
- data/lib/hecks/ports/identity_generation.rb +17 -3
- data/lib/hecks/ports/identity_resolution.rb +17 -1
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +58 -0
- 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 +34 -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 +95 -6
- 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 +48 -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 +58 -19
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -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 +41 -8
- data/lib/hecks/ports/query/ordering.rb +21 -6
- data/lib/hecks/ports/query.rb +35 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +147 -0
- data/lib/hecks/projections/diagrams.rb +315 -79
- data/lib/hecks/projections/glossary/html.rb +91 -3
- data/lib/hecks/projections/glossary/markdown.rb +50 -3
- data/lib/hecks/projections/glossary/mermaid.rb +49 -1
- data/lib/hecks/projections/glossary/sections.rb +1 -1
- data/lib/hecks/projections/glossary/sentences.rb +110 -5
- data/lib/hecks/projections/glossary.rb +173 -31
- data/lib/hecks/projections/ir.rb +1 -1
- data/lib/hecks/projections/model/deviations.rb +62 -17
- data/lib/hecks/projections/model.rb +75 -21
- data/lib/hecks/projections/oidc.rb +45 -8
- data/lib/hecks/projections/parser_table.rb +53 -12
- data/lib/hecks/projections/reference.rb +15 -3
- data/lib/hecks/projections/rust_vocabulary.rb +646 -0
- data/lib/hecks/projections/shape.rb +10 -2
- data/lib/hecks/projections/statements.rb +69 -22
- data/lib/hecks/projections/vocabulary.rb +26 -9
- data/lib/hecks/projections.rb +5 -3
- data/lib/hecks/projector/cli_projector.rb +245 -37
- data/lib/hecks/projector/docs_projector.rb +154 -28
- data/lib/hecks/projector/exporter.rb +104 -29
- data/lib/hecks/projector/ir_projector.rb +7 -1
- data/lib/hecks/projector/narrate_projector.rb +144 -23
- data/lib/hecks/projector/target.rb +42 -18
- data/lib/hecks/projector.rb +86 -18
- data/lib/hecks/query_ir.rb +94 -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 +61 -15
- 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 +9 -3
- data/lib/hecks/router/namespace_installer.rb +16 -3
- data/lib/hecks/router.rb +56 -1
- data/lib/hecks/runtime/aggregate_lock.rb +20 -11
- data/lib/hecks/runtime/boot_gates.rb +21 -3
- data/lib/hecks/runtime/caller.rb +40 -8
- data/lib/hecks/runtime/capability_graph.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
- data/lib/hecks/runtime/command_interpreter.rb +195 -113
- data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
- data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
- 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 +145 -55
- data/lib/hecks/runtime/command_rules.rb +3 -0
- data/lib/hecks/runtime/dependency_planning.rb +56 -11
- data/lib/hecks/runtime/dispatcher.rb +263 -92
- data/lib/hecks/runtime/entity_element.rb +304 -67
- data/lib/hecks/runtime/entity_interpreter.rb +149 -88
- data/lib/hecks/runtime/errors.rb +37 -23
- data/lib/hecks/runtime/event.rb +15 -10
- data/lib/hecks/runtime/identity.rb +91 -23
- data/lib/hecks/runtime/instance.rb +103 -18
- data/lib/hecks/runtime/interpreting.rb +24 -16
- data/lib/hecks/runtime/invocation.rb +358 -0
- data/lib/hecks/runtime/loader.rb +105 -19
- data/lib/hecks/runtime/outbox.rb +164 -26
- data/lib/hecks/runtime/policy_interpreter.rb +73 -60
- data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
- data/lib/hecks/runtime/query_interpreter.rb +93 -74
- data/lib/hecks/runtime/reaction_invocation.rb +73 -28
- data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
- data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
- data/lib/hecks/runtime/reference_hop.rb +48 -6
- data/lib/hecks/runtime/refusal_wording.rb +142 -115
- data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
- data/lib/hecks/runtime/registry/verification.rb +153 -28
- data/lib/hecks/runtime/registry.rb +202 -32
- data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
- data/lib/hecks/runtime/routing.rb +36 -154
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
- data/lib/hecks/runtime/saga_interpreter.rb +90 -76
- data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
- data/lib/hecks/runtime/tenant_check.rb +33 -13
- data/lib/hecks/runtime/tenant_scope.rb +23 -5
- data/lib/hecks/runtime/value/admission.rb +75 -30
- data/lib/hecks/runtime/value/coercion.rb +555 -142
- data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
- data/lib/hecks/runtime/value.rb +71 -21
- data/lib/hecks/runtime.rb +39 -7
- data/lib/hecks/storehouse.rb +368 -72
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +3 -3
- data/lib/hecks/vocabulary.rb +205 -4
- data/lib/hecks.rb +91 -11
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
- metadata +28 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 830e92d9660dc86cc7de1af49d85fd7ce5ab611e07341dd11f4633274b920fda
|
|
4
|
+
data.tar.gz: 1c70b3c2994fd520a604ebd8c8cc8a53e7531e3cb5ba1dd59c6fd751b9b9dd99
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3aeb0240903bed58fbd5684c23ee2069e9b3021aef07082d11e26ffa5052cebccf42d78e3aff243ba192183460056113e0510c7667306d1ed2cde3048ad118c0
|
|
7
|
+
data.tar.gz: e104c2252f0dbbb1ba5ea97a0f6f56f890cab76e12ff86f246605003738228b94062e317f16db1ebfb376753536d8309cedff86e00275781497232de67b17e1e
|
|
@@ -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,10 +40,22 @@ module Hecks
|
|
|
40
40
|
|
|
41
41
|
module_function
|
|
42
42
|
|
|
43
|
-
#
|
|
43
|
+
# Asks the model for the next best interview question, as a raw parsed-JSON reply.
|
|
44
|
+
#
|
|
45
|
+
# **The next best question**. `state` is whatever
|
|
44
46
|
# `Interview::Session#declaration`/`#gaps` produced — passed
|
|
45
47
|
# through as JSON, not reformatted, so this adapter never
|
|
46
48
|
# re-derives what the session already knows.
|
|
49
|
+
#
|
|
50
|
+
# @param state [Hash] the interview's whole current picture (declaration plus gaps),
|
|
51
|
+
# JSON-able
|
|
52
|
+
# @param asked [Array<Object>] JSON-able record of the questions already asked
|
|
53
|
+
# @return [Object] the parsed JSON reply, expected to be a Hash holding a `"questions"`
|
|
54
|
+
# array; shape validated downstream by `Ports::Agent::Answers.questions`
|
|
55
|
+
# @raise [Ports::Agent::Unavailable] if the `claude` binary is missing, the subprocess
|
|
56
|
+
# fails, or the call times out
|
|
57
|
+
# @raise [Ports::Agent::ValidationError] if the CLI's own envelope has no `"result"`, or
|
|
58
|
+
# the result text is not valid JSON
|
|
47
59
|
def ask(state:, asked:)
|
|
48
60
|
call(
|
|
49
61
|
system: SYSTEM_PREFIX + "Given the domain model so far, ask the single best next " \
|
|
@@ -53,7 +65,18 @@ module Hecks
|
|
|
53
65
|
)
|
|
54
66
|
end
|
|
55
67
|
|
|
56
|
-
#
|
|
68
|
+
# Turns a human's sentence into proposed declarations, as a raw parsed-JSON reply.
|
|
69
|
+
#
|
|
70
|
+
# Prose -> proposed declarations.
|
|
71
|
+
#
|
|
72
|
+
# @param prose [String] a human's plain-English sentence
|
|
73
|
+
# @param state [Hash] the interview's whole current picture, JSON-able
|
|
74
|
+
# @return [Object] the parsed JSON reply, expected to be a Hash holding a `"proposals"`
|
|
75
|
+
# array; shape validated downstream by `Ports::Agent::Answers.proposals`
|
|
76
|
+
# @raise [Ports::Agent::Unavailable] if the `claude` binary is missing, the subprocess
|
|
77
|
+
# fails, or the call times out
|
|
78
|
+
# @raise [Ports::Agent::ValidationError] if the CLI's own envelope has no `"result"`, or
|
|
79
|
+
# the result text is not valid JSON
|
|
57
80
|
def interpret(prose:, state:)
|
|
58
81
|
call(
|
|
59
82
|
system: SYSTEM_PREFIX + "Given the domain model so far and a sentence the human just said, " \
|
|
@@ -66,10 +89,22 @@ module Hecks
|
|
|
66
89
|
)
|
|
67
90
|
end
|
|
68
91
|
|
|
69
|
-
#
|
|
92
|
+
# Asks the model to judge a declared model on taste, as a raw parsed-JSON reply.
|
|
93
|
+
#
|
|
94
|
+
# What is wrong with this as a model — closed to the same kind
|
|
70
95
|
# vocabulary `Ports::Agent::CRITIQUE_KINDS` declares, spelled out
|
|
71
96
|
# here too since the system prompt is the only place the model
|
|
72
97
|
# itself ever sees that list.
|
|
98
|
+
#
|
|
99
|
+
# @param declared [Hash] the chapter as declared so far, JSON-able
|
|
100
|
+
# @param refusals [Array<Object>] JSON-able refusals the language itself already raised
|
|
101
|
+
# @param findings [Array<Object>] JSON-able mechanical findings already found
|
|
102
|
+
# @return [Object] the parsed JSON reply, expected to be a Hash holding a `"findings"`
|
|
103
|
+
# array; shape validated downstream by `Ports::Agent::Answers.findings`
|
|
104
|
+
# @raise [Ports::Agent::Unavailable] if the `claude` binary is missing, the subprocess
|
|
105
|
+
# fails, or the call times out
|
|
106
|
+
# @raise [Ports::Agent::ValidationError] if the CLI's own envelope has no `"result"`, or
|
|
107
|
+
# the result text is not valid JSON
|
|
73
108
|
def critique(declared:, refusals:, findings:)
|
|
74
109
|
kinds = Ports::Agent::CRITIQUE_KINDS.join(", ")
|
|
75
110
|
call(
|
|
@@ -82,9 +117,22 @@ module Hecks
|
|
|
82
117
|
)
|
|
83
118
|
end
|
|
84
119
|
|
|
85
|
-
#
|
|
120
|
+
# Suggests a name for a construct, as a raw parsed-JSON reply.
|
|
121
|
+
#
|
|
122
|
+
# **Vocabulary help**. Named `suggest_name`, not `name` — see
|
|
86
123
|
# `Ports::Agent#suggest_name`'s own comment for why `name` is
|
|
87
124
|
# never a safe module-function name here.
|
|
125
|
+
#
|
|
126
|
+
# @param meaning [String] what the new name needs to mean
|
|
127
|
+
# @param kind [String] the kind of construct being named, such as `"event"`
|
|
128
|
+
# @param near [Array<String>] names already in use nearby, which a suggestion must not
|
|
129
|
+
# collide with
|
|
130
|
+
# @return [Object] the parsed JSON reply, expected to be a Hash holding a `"names"`
|
|
131
|
+
# array; shape validated downstream by `Ports::Agent::Answers.suggestions`
|
|
132
|
+
# @raise [Ports::Agent::Unavailable] if the `claude` binary is missing, the subprocess
|
|
133
|
+
# fails, or the call times out
|
|
134
|
+
# @raise [Ports::Agent::ValidationError] if the CLI's own envelope has no `"result"`, or
|
|
135
|
+
# the result text is not valid JSON
|
|
88
136
|
def suggest_name(meaning:, kind:, near:)
|
|
89
137
|
call(
|
|
90
138
|
system: SYSTEM_PREFIX + "Suggest a name for a #{kind} meaning \"#{meaning}\", distinct from " \
|
|
@@ -96,6 +144,16 @@ module Hecks
|
|
|
96
144
|
|
|
97
145
|
# ── transport ───────────────────────────────────────────────────
|
|
98
146
|
|
|
147
|
+
# Runs `claude` as a subprocess with `payload` on stdin and returns its parsed JSON reply.
|
|
148
|
+
#
|
|
149
|
+
# @param system [String] the system-prompt text appended via `--append-system-prompt`
|
|
150
|
+
# @param payload [Hash] the JSON-able payload written to the subprocess's stdin
|
|
151
|
+
# @return [Object] the parsed JSON value nested under the CLI envelope's `"result"` key
|
|
152
|
+
# (see `unwrap`)
|
|
153
|
+
# @raise [Ports::Agent::Unavailable] if the `claude` binary is missing, the subprocess
|
|
154
|
+
# exits non-zero, or the call does not finish within `TIMEOUT_SECONDS`
|
|
155
|
+
# @raise [Ports::Agent::ValidationError] if the envelope has no `"result"`, or the
|
|
156
|
+
# result text is not valid JSON (see `unwrap`)
|
|
99
157
|
def call(system:, payload:)
|
|
100
158
|
stdout, status = Timeout.timeout(TIMEOUT_SECONDS) do
|
|
101
159
|
Open3.capture2(
|
|
@@ -113,6 +171,13 @@ module Hecks
|
|
|
113
171
|
raise Ports::Agent::Unavailable, "claude is not on PATH: #{e.message}"
|
|
114
172
|
end
|
|
115
173
|
|
|
174
|
+
# Unwraps the CLI's own JSON envelope down to the model's parsed reply.
|
|
175
|
+
#
|
|
176
|
+
# @param stdout [String] the raw stdout of the `claude` CLI invocation, the outer
|
|
177
|
+
# `{"result": "..."}` envelope
|
|
178
|
+
# @return [Object] the JSON value parsed out of the envelope's `"result"` string
|
|
179
|
+
# @raise [Ports::Agent::ValidationError] if the envelope has no `"result"` key, or
|
|
180
|
+
# `result` is not valid JSON
|
|
116
181
|
def unwrap(stdout)
|
|
117
182
|
envelope = JSON.parse(stdout)
|
|
118
183
|
result = envelope["result"]
|
|
@@ -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
|
|