hecks 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
- data/lib/hecks/adapters/driven/claude_code.rb +127 -0
- data/lib/hecks/adapters/driven/d1.adapter +12 -0
- data/lib/hecks/adapters/driven/d1.rb +403 -0
- data/lib/hecks/adapters/driven/folder.adapter +3 -0
- data/lib/hecks/adapters/driven/folder.rb +199 -0
- data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
- data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
- data/lib/hecks/adapters/driven/governance_authorization.rb +62 -0
- data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
- data/lib/hecks/adapters/driven/heki.adapter +4 -0
- data/lib/hecks/adapters/driven/heki.rb +148 -0
- data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
- data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
- data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
- data/lib/hecks/adapters/driven/lambda.adapter +4 -0
- data/lib/hecks/adapters/driven/lambda.rb +131 -0
- data/lib/hecks/adapters/driven/memory.adapter +3 -0
- data/lib/hecks/adapters/driven/memory.rb +103 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
- data/lib/hecks/adapters/driven/postgres/codec.rb +80 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
- data/lib/hecks/adapters/driven/postgres.adapter +5 -0
- data/lib/hecks/adapters/driven/postgres.rb +391 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
- data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
- data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
- data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
- data/lib/hecks/adapters/driven/prism.adapter +3 -0
- data/lib/hecks/adapters/driven/prism.rb +80 -0
- data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
- data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
- data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
- data/lib/hecks/adapters/driven/sqlite.rb +285 -0
- data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
- data/lib/hecks/adapters/driven/system_clock.rb +14 -0
- data/lib/hecks/adapters/driven.rb +36 -0
- data/lib/hecks/adapters.rb +6 -0
- data/lib/hecks/behaviors/dsl.rb +96 -0
- data/lib/hecks/behaviors/expectations.rb +280 -0
- data/lib/hecks/behaviors/ir.rb +31 -0
- data/lib/hecks/behaviors/rspec.rb +42 -0
- data/lib/hecks/behaviors/runner.rb +96 -0
- data/lib/hecks/behaviors.rb +25 -0
- data/lib/hecks/bluebook/aggregate.rb +108 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
- data/lib/hecks/bluebook/assembly/build.rb +48 -0
- data/lib/hecks/bluebook/assembly/contract.rb +112 -0
- data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
- data/lib/hecks/bluebook/assembly/marks.rb +227 -0
- data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
- data/lib/hecks/bluebook/assembly.rb +78 -0
- data/lib/hecks/bluebook/attribute.rb +96 -0
- data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
- data/lib/hecks/bluebook/behaviour/command.rb +116 -0
- data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
- data/lib/hecks/bluebook/behaviour/query.rb +10 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
- data/lib/hecks/bluebook/chapter.rb +68 -0
- data/lib/hecks/bluebook/command.rb +124 -0
- data/lib/hecks/bluebook/domain_port.rb +102 -0
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
- data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
- data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
- data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
- data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
- data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
- data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
- data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
- data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
- data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
- data/lib/hecks/bluebook/dsl.rb +45 -0
- data/lib/hecks/bluebook/entity.rb +103 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
- data/lib/hecks/bluebook/expression/projection.json +170 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
- data/lib/hecks/bluebook/expression/resolver.rb +630 -0
- data/lib/hecks/bluebook/expression.rb +13 -0
- data/lib/hecks/bluebook/hexagon.rb +60 -0
- data/lib/hecks/bluebook/lifecycle.rb +42 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
- data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
- data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
- data/lib/hecks/bluebook/meta_validator.rb +452 -0
- data/lib/hecks/bluebook/model_check.rb +318 -0
- data/lib/hecks/bluebook/pattern_subset.rb +120 -0
- data/lib/hecks/bluebook/policy.rb +41 -0
- data/lib/hecks/bluebook/process_manager.rb +86 -0
- data/lib/hecks/bluebook/project_discovery.rb +30 -0
- data/lib/hecks/bluebook/project_loader.rb +40 -0
- data/lib/hecks/bluebook/project_register.rb +82 -0
- data/lib/hecks/bluebook/query.rb +61 -0
- data/lib/hecks/bluebook/read_model.rb +109 -0
- data/lib/hecks/bluebook/reference.rb +74 -0
- data/lib/hecks/bluebook/smoke_test.rb +166 -0
- data/lib/hecks/bluebook/synthesizer.rb +95 -0
- data/lib/hecks/bluebook/translation.rb +92 -0
- data/lib/hecks/bluebook/value_object.rb +58 -0
- data/lib/hecks/bluebook.rb +74 -0
- data/lib/hecks/codemod.rb +342 -0
- data/lib/hecks/construct.rb +71 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
- data/lib/hecks/deploy/oidc.json +18 -0
- data/lib/hecks/doc/reference.rb +389 -0
- data/lib/hecks/embryonaut_bluebook.rb +75 -0
- data/lib/hecks/facade/cli_door.rb +116 -0
- data/lib/hecks/facade/cli_runner.rb +190 -0
- data/lib/hecks/facade/command_request.rb +105 -0
- data/lib/hecks/facade/handle.rb +173 -0
- data/lib/hecks/facade/json_door.rb +154 -0
- data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
- data/lib/hecks/facade/surface/chapter.rb +107 -0
- data/lib/hecks/facade/surface.rb +48 -0
- data/lib/hecks/facade.rb +44 -0
- data/lib/hecks/forms/app.rb +300 -0
- data/lib/hecks/forms/command_form_renderer.rb +113 -0
- data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
- data/lib/hecks/forms/field_renderer.rb +170 -0
- data/lib/hecks/forms/field_shape.rb +232 -0
- data/lib/hecks/forms/html.rb +53 -0
- data/lib/hecks/forms/index_renderer.rb +35 -0
- data/lib/hecks/forms/page.rb +157 -0
- data/lib/hecks/forms/params.rb +132 -0
- data/lib/hecks/forms/query_form_renderer.rb +114 -0
- data/lib/hecks/forms/record_renderer.rb +115 -0
- data/lib/hecks/forms/record_table.rb +63 -0
- data/lib/hecks/forms/reference_options.rb +30 -0
- data/lib/hecks/forms/value_object_shape.rb +46 -0
- data/lib/hecks/forms.rb +54 -0
- data/lib/hecks/fqn.rb +94 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
- data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
- data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
- data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
- data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
- data/lib/hecks/framework/oidc.json +39 -0
- data/lib/hecks/framework.rb +90 -0
- data/lib/hecks/freezer.rb +67 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
- data/lib/hecks/fuzzing/properties.rb +1146 -0
- data/lib/hecks/fuzzing/replay.rb +635 -0
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
- data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
- data/lib/hecks/fuzzing/value_generator.rb +177 -0
- data/lib/hecks/fuzzing.rb +12 -0
- data/lib/hecks/grammar/evolve.rb +291 -0
- data/lib/hecks/grammar/expression.bluebook +417 -0
- data/lib/hecks/grammar/expression_operators.json +1152 -0
- data/lib/hecks/grammar/grammar.hecksagon +20 -0
- data/lib/hecks/grammar/oidc.json +69 -0
- data/lib/hecks/grammar/translation.bluebook +203 -0
- data/lib/hecks/grammar.rb +178 -0
- data/lib/hecks/ir.rb +126 -0
- data/lib/hecks/language/adapter.bluebook +116 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
- data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
- data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
- data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
- data/lib/hecks/language/bluebook/command.bluebook +437 -0
- data/lib/hecks/language/bluebook/entity.bluebook +316 -0
- data/lib/hecks/language/bluebook/policy.bluebook +172 -0
- data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
- data/lib/hecks/language/bluebook/projection.bluebook +267 -0
- data/lib/hecks/language/bluebook/query.bluebook +245 -0
- data/lib/hecks/language/bluebook/shape.bluebook +292 -0
- data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
- data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
- data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
- data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
- data/lib/hecks/language/oidc.json +313 -0
- data/lib/hecks/language/port.bluebook +92 -0
- data/lib/hecks/language/translation/translation.bluebook +110 -0
- data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
- data/lib/hecks/language/world/wiring.bluebook +62 -0
- data/lib/hecks/language/world/world.bluebook +84 -0
- data/lib/hecks/literal.rb +125 -0
- data/lib/hecks/naming.rb +121 -0
- data/lib/hecks/ports/access_control.port +4 -0
- data/lib/hecks/ports/access_control.rb +62 -0
- data/lib/hecks/ports/agent/answers.rb +104 -0
- data/lib/hecks/ports/agent.port +4 -0
- data/lib/hecks/ports/agent.rb +167 -0
- data/lib/hecks/ports/authentication.port +4 -0
- data/lib/hecks/ports/authentication.rb +50 -0
- data/lib/hecks/ports/authorization.port +4 -0
- data/lib/hecks/ports/authorization.rb +59 -0
- data/lib/hecks/ports/clock.port +4 -0
- data/lib/hecks/ports/clock.rb +62 -0
- data/lib/hecks/ports/extraction.port +4 -0
- data/lib/hecks/ports/extraction.rb +37 -0
- data/lib/hecks/ports/identity_assignment.port +4 -0
- data/lib/hecks/ports/identity_assignment.rb +45 -0
- data/lib/hecks/ports/identity_generation.port +4 -0
- data/lib/hecks/ports/identity_generation.rb +49 -0
- data/lib/hecks/ports/identity_resolution.port +4 -0
- data/lib/hecks/ports/identity_resolution.rb +40 -0
- data/lib/hecks/ports/loading.port +4 -0
- data/lib/hecks/ports/loading.rb +13 -0
- data/lib/hecks/ports/persistence/append_only.rb +102 -0
- data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
- data/lib/hecks/ports/persistence/execution.rb +18 -0
- data/lib/hecks/ports/persistence/lineage.rb +277 -0
- data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
- data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
- data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
- data/lib/hecks/ports/persistence.port +4 -0
- data/lib/hecks/ports/persistence.rb +39 -0
- data/lib/hecks/ports/projection.port +4 -0
- data/lib/hecks/ports/projection.rb +76 -0
- data/lib/hecks/ports/query/in_memory.rb +60 -0
- data/lib/hecks/ports/query/ordering.rb +41 -0
- data/lib/hecks/ports/query.rb +36 -0
- data/lib/hecks/ports.rb +27 -0
- data/lib/hecks/projections/diagrams.rb +413 -0
- data/lib/hecks/projections/ir.rb +18 -0
- data/lib/hecks/projections/model/deviations.rb +98 -0
- data/lib/hecks/projections/model.rb +145 -0
- data/lib/hecks/projections/oidc.rb +110 -0
- data/lib/hecks/projections/parser_table.rb +159 -0
- data/lib/hecks/projections/reference.rb +38 -0
- data/lib/hecks/projections/shape.rb +31 -0
- data/lib/hecks/projections/statements.rb +110 -0
- data/lib/hecks/projections/vocabulary.rb +100 -0
- data/lib/hecks/projections.rb +32 -0
- data/lib/hecks/projector/cli_projector.rb +437 -0
- data/lib/hecks/projector/docs_projector.rb +321 -0
- data/lib/hecks/projector/exporter.rb +141 -0
- data/lib/hecks/projector/ir_projector.rb +18 -0
- data/lib/hecks/projector/narrate_projector.rb +243 -0
- data/lib/hecks/projector/target.rb +97 -0
- data/lib/hecks/projector.rb +186 -0
- data/lib/hecks/query_ir.rb +392 -0
- data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
- data/lib/hecks/query_specification/common/comparators.rb +25 -0
- data/lib/hecks/query_specification/common/comparison.rb +174 -0
- data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
- data/lib/hecks/query_specification/common/dsl.rb +58 -0
- data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
- data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
- data/lib/hecks/query_specification/common/null_policy.rb +80 -0
- data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
- data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
- data/lib/hecks/query_specification/common/options.rb +32 -0
- data/lib/hecks/query_specification/common/order_by.rb +9 -0
- data/lib/hecks/query_specification/common/specification.rb +10 -0
- data/lib/hecks/query_specification/common/where_clause.rb +9 -0
- data/lib/hecks/query_specification/field_path.rb +89 -0
- data/lib/hecks/query_specification/hop_path.rb +130 -0
- data/lib/hecks/query_specification/read_model/specification.rb +18 -0
- data/lib/hecks/query_specification.rb +14 -0
- data/lib/hecks/rendering.rb +48 -0
- data/lib/hecks/router/namespace_installer.rb +157 -0
- data/lib/hecks/router.rb +70 -0
- data/lib/hecks/runtime/caller.rb +50 -0
- data/lib/hecks/runtime/capability_graph.rb +44 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
- data/lib/hecks/runtime/command_interpreter.rb +413 -0
- data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
- data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
- data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
- data/lib/hecks/runtime/command_rules/emission.rb +34 -0
- data/lib/hecks/runtime/command_rules/references.rb +181 -0
- data/lib/hecks/runtime/command_rules.rb +28 -0
- data/lib/hecks/runtime/dependency_planning.rb +231 -0
- data/lib/hecks/runtime/dispatcher.rb +286 -0
- data/lib/hecks/runtime/entity_element.rb +253 -0
- data/lib/hecks/runtime/entity_interpreter.rb +228 -0
- data/lib/hecks/runtime/era_check.rb +136 -0
- data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
- data/lib/hecks/runtime/era_guard.rb +193 -0
- data/lib/hecks/runtime/era_tamper.rb +61 -0
- data/lib/hecks/runtime/errors.rb +100 -0
- data/lib/hecks/runtime/event.rb +51 -0
- data/lib/hecks/runtime/identity.rb +128 -0
- data/lib/hecks/runtime/instance.rb +120 -0
- data/lib/hecks/runtime/interpreting.rb +69 -0
- data/lib/hecks/runtime/loader.rb +143 -0
- data/lib/hecks/runtime/policy_interpreter.rb +366 -0
- data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
- data/lib/hecks/runtime/query_interpreter.rb +254 -0
- data/lib/hecks/runtime/reaction_invocation.rb +245 -0
- data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
- data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
- data/lib/hecks/runtime/reference_hop.rb +99 -0
- data/lib/hecks/runtime/refusal_wording.rb +124 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
- data/lib/hecks/runtime/registry/verification.rb +158 -0
- data/lib/hecks/runtime/registry.rb +218 -0
- data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
- data/lib/hecks/runtime/routing.rb +96 -0
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
- data/lib/hecks/runtime/saga_interpreter.rb +301 -0
- data/lib/hecks/runtime/storage_shape.rb +120 -0
- data/lib/hecks/runtime/tenant_check.rb +84 -0
- data/lib/hecks/runtime/tenant_scope.rb +55 -0
- data/lib/hecks/runtime/value/admission.rb +112 -0
- data/lib/hecks/runtime/value/coercion.rb +500 -0
- data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
- data/lib/hecks/runtime/value.rb +125 -0
- data/lib/hecks/runtime.rb +95 -0
- data/lib/hecks/translation/audit/approval_digest.rb +31 -0
- data/lib/hecks/translation/audit/layer_one.rb +37 -0
- data/lib/hecks/translation/audit/layer_two.rb +77 -0
- data/lib/hecks/translation/audit/unfed_report.rb +42 -0
- data/lib/hecks/translation/audit.rb +70 -0
- data/lib/hecks/translation/reattest.rb +72 -0
- data/lib/hecks/translation/rule_compiler.rb +120 -0
- data/lib/hecks/translation/scaffold/differ.rb +183 -0
- data/lib/hecks/translation/scaffold/renderer.rb +41 -0
- data/lib/hecks/translation/scaffold/writer.rb +28 -0
- data/lib/hecks/translation/scaffold.rb +30 -0
- data/lib/hecks/translation.rb +11 -0
- data/lib/hecks/version.rb +16 -0
- data/lib/hecks/vocabulary.rb +213 -0
- data/lib/hecks.rb +118 -6
- metadata +401 -222
- data/bin/hecks +0 -7
- data/bin/hecks-package +0 -65
- data/bin/hecks_console +0 -12
- data/bin/hecks_serverless +0 -6
- data/lib/cli/build.rb +0 -14
- data/lib/cli/command_runner.rb +0 -28
- data/lib/cli/console.rb +0 -10
- data/lib/cli/generate.rb +0 -37
- data/lib/cli/hecks-cli.rb +0 -27
- data/lib/cli/test.rb +0 -57
- data/lib/console/commands.rb +0 -8
- data/lib/console/hecks-console.rb +0 -1
- data/lib/packager/README.md +0 -0
- data/lib/packager/app_runner.rb +0 -21
- data/lib/packager/args.rb +0 -26
- data/lib/packager/compatibility/fixnum.rb +0 -6
- data/lib/packager/hecks.rb +0 -39
- data/lib/packager/query_runner.rb +0 -21
- data/lib/packager/resources/Dockerfile +0 -11
- data/lib/packager/resources/app_binary +0 -7
- data/lib/packager/resources/bundle_config +0 -3
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
- data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
- data/lib/serverless/Domain +0 -32
- data/lib/serverless/cli.rb +0 -75
- data/lib/serverless/resources/command_name.js +0 -5
- data/lib/serverless/resources/environment.js +0 -7
- data/lib/serverless/resources/handler.js.tt +0 -28
- data/lib/serverless/resources/run_binary.js +0 -22
- data/lib/serverless/resources/serverless.yml +0 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c40a3c6e1879b5df4a299ae07488cffe80fc0c9ee22905044b876b3887c2cfc3
|
|
4
|
+
data.tar.gz: 983121c63439d9f7d79c77e0dabcf34e1625df7067368c27438ff64f0065291e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 17ffccfc67272cb90216229ebb41e6fdf4199b6ca6f4e1f5ecee7964191190c2e0f562b2c1fd0c0f7d304f3da95ab3521a3ca13fdcc162935fc99c9a33aca866
|
|
7
|
+
data.tar.gz: 9cb2f68d53120a1e7a7ab59e91387661151243d51927f7de690986597c3e2932f09b7680cabb0933e1b82683dffd29b9581167f05799864c56e3ddfa1b502970
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
require "json"
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
5
|
+
require_relative "../../ports/agent"
|
|
6
|
+
|
|
7
|
+
module Hecks
|
|
8
|
+
module Adapters
|
|
9
|
+
# THE REAL `agent` FULFILLMENT — shells out to the `claude` CLI
|
|
10
|
+
# itself, `claude -p --output-format json`, one process per call.
|
|
11
|
+
# `Ports::Agent`'s own scripted double (`spec/fixtures/scripted_
|
|
12
|
+
# agent.{adapter,rb}`) is the deterministic sibling every spec binds
|
|
13
|
+
# instead, the same relationship `SecureRandomIdentity` already has
|
|
14
|
+
# to `SequentialIdentity`.
|
|
15
|
+
#
|
|
16
|
+
# THIS FILE OWNS TRANSPORT ONLY — spawning the process, unwrapping
|
|
17
|
+
# the CLI's own JSON envelope (`{"result": "..."}`) down to the
|
|
18
|
+
# model's raw text, and parsing THAT text as JSON. It hands back a
|
|
19
|
+
# plain Hash. Whether that Hash has the keys a caller asked for, and
|
|
20
|
+
# whether its values are within the closed vocabularies this port
|
|
21
|
+
# recognizes (a critique's `kind`, a proposal's `verb` pattern) is
|
|
22
|
+
# `Ports::Agent::Answers`' job, not this file's — see `ports/agent.rb`'s
|
|
23
|
+
# own header for why that split is load-bearing.
|
|
24
|
+
#
|
|
25
|
+
# `Open3.capture2` with an explicit argv array, never a shell string
|
|
26
|
+
# — the same shape every other subprocess call in this repo already
|
|
27
|
+
# uses (`bin/rust_conformance`), and the reason `claude` being
|
|
28
|
+
# shell-aliased to `claude --dangerously-skip-permissions` in a
|
|
29
|
+
# human's own terminal never reaches this code at all: an argv spawn
|
|
30
|
+
# invokes the real binary directly, no shell, no alias.
|
|
31
|
+
#
|
|
32
|
+
# `--allowedTools ""` — this adapter asks a question and reads back
|
|
33
|
+
# text; it never wants the model reaching for a tool mid-answer.
|
|
34
|
+
module ClaudeCode
|
|
35
|
+
TIMEOUT_SECONDS = 120
|
|
36
|
+
|
|
37
|
+
SYSTEM_PREFIX = "You are assisting a domain-modeling interview for the hecks " \
|
|
38
|
+
"event-sourced framework. Reply with EXACTLY ONE JSON object, no prose " \
|
|
39
|
+
"before or after it, no markdown code fence. "
|
|
40
|
+
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
# THE NEXT BEST QUESTION. `state` is whatever
|
|
44
|
+
# `Interview::Session#declaration`/`#gaps` produced — passed
|
|
45
|
+
# through as JSON, not reformatted, so this adapter never
|
|
46
|
+
# re-derives what the session already knows.
|
|
47
|
+
def ask(state:, asked:)
|
|
48
|
+
call(
|
|
49
|
+
system: SYSTEM_PREFIX + "Given the domain model so far, ask the single best next " \
|
|
50
|
+
'discovery question. Reply as {"questions": [{"text": "...", "because": "..."}]} ' \
|
|
51
|
+
"with exactly one entry. Never repeat a question already asked.",
|
|
52
|
+
payload: { state: state, already_asked: asked }
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# PROSE -> PROPOSED DECLARATIONS.
|
|
57
|
+
def interpret(prose:, state:)
|
|
58
|
+
call(
|
|
59
|
+
system: SYSTEM_PREFIX + "Given the domain model so far and a sentence the human just said, " \
|
|
60
|
+
"propose zero or more declarations that capture what the sentence names as domain " \
|
|
61
|
+
"fact. A verb must be fully qualified as Chapter::Aggregate.Command. Reply as " \
|
|
62
|
+
'{"proposals": [{"verb": "...", "rationale": "...", "arguments": ' \
|
|
63
|
+
'[{"name": "...", "field": "...", "value": "..."}]}]}. If the sentence carries no ' \
|
|
64
|
+
'declaration (a question back, a clarification), reply {"proposals": []}.',
|
|
65
|
+
payload: { state: state, prose: prose }
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# WHAT IS WRONG WITH THIS AS A MODEL — closed to the same kind
|
|
70
|
+
# vocabulary `Ports::Agent::CRITIQUE_KINDS` declares, spelled out
|
|
71
|
+
# here too since the system prompt is the only place the model
|
|
72
|
+
# itself ever sees that list.
|
|
73
|
+
def critique(declared:, refusals:, findings:)
|
|
74
|
+
kinds = Ports::Agent::CRITIQUE_KINDS.join(", ")
|
|
75
|
+
call(
|
|
76
|
+
system: SYSTEM_PREFIX + "Given a domain declaration, the refusals it already triggered, and " \
|
|
77
|
+
"the mechanical findings already reported, judge it on TASTE — do not restate what is " \
|
|
78
|
+
"already known. Only use one of these kinds: #{kinds}. Only use severity error or " \
|
|
79
|
+
'warning. Reply as {"findings": [{"kind": "...", "severity": "...", "subject": "...", ' \
|
|
80
|
+
'"message": "..."}]}. Reply {"findings": []} if there is nothing worth saying.',
|
|
81
|
+
payload: { declared: declared, refusals: refusals, findings: findings }
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# VOCABULARY HELP. Named `suggest_name`, not `name` — see
|
|
86
|
+
# `Ports::Agent#suggest_name`'s own comment for why `name` is
|
|
87
|
+
# never a safe module-function name here.
|
|
88
|
+
def suggest_name(meaning:, kind:, near:)
|
|
89
|
+
call(
|
|
90
|
+
system: SYSTEM_PREFIX + "Suggest a name for a #{kind} meaning \"#{meaning}\", distinct from " \
|
|
91
|
+
'the names already in use nearby. Reply as {"names": [{"name": "...", "because": ' \
|
|
92
|
+
'"...", "rejected": ["...", "..."]}]} with exactly one entry.',
|
|
93
|
+
payload: { meaning: meaning, kind: kind, near: near }
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# ── transport ───────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
def call(system:, payload:)
|
|
100
|
+
stdout, status = Timeout.timeout(TIMEOUT_SECONDS) {
|
|
101
|
+
Open3.capture2(
|
|
102
|
+
"claude", "-p", "--output-format", "json",
|
|
103
|
+
"--append-system-prompt", system, "--allowedTools", "",
|
|
104
|
+
stdin_data: JSON.generate(payload)
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
raise Ports::Agent::Unavailable, "claude exited #{status.exitstatus}: #{stdout}" unless status.success?
|
|
108
|
+
|
|
109
|
+
unwrap(stdout)
|
|
110
|
+
rescue Timeout::Error
|
|
111
|
+
raise Ports::Agent::Unavailable, "claude did not answer within #{TIMEOUT_SECONDS}s"
|
|
112
|
+
rescue Errno::ENOENT => e
|
|
113
|
+
raise Ports::Agent::Unavailable, "claude is not on PATH: #{e.message}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def unwrap(stdout)
|
|
117
|
+
envelope = JSON.parse(stdout)
|
|
118
|
+
result = envelope["result"]
|
|
119
|
+
raise Ports::Agent::ValidationError, "no \"result\" in the claude envelope: #{stdout}" unless result
|
|
120
|
+
|
|
121
|
+
JSON.parse(result)
|
|
122
|
+
rescue JSON::ParserError => e
|
|
123
|
+
raise Ports::Agent::ValidationError, "claude's reply was not JSON: #{e.message}\n#{stdout}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Hecks.adapter "D1" do
|
|
2
|
+
port "persistence"
|
|
3
|
+
field :account_id
|
|
4
|
+
field :database_id
|
|
5
|
+
secret :api_token
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# No "D1Projection" declared yet, unlike Sqlite/SqliteProjection —
|
|
9
|
+
# read-model projection support (query_read_model, mirroring
|
|
10
|
+
# sqlite/projection.rb) is real future work, not started here. Declaring
|
|
11
|
+
# the adapter without the class behind it would just be a WiringError trap
|
|
12
|
+
# for whoever binds projected_by("D1Projection") first.
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
require_relative "sql_query_builder"
|
|
6
|
+
require_relative "sqlite/schema_builder"
|
|
7
|
+
require_relative "sqlite/codec"
|
|
8
|
+
require_relative "sqlite" # for Sqlite::SchemaBuilder/Sqlite::Codec — see the reuse note below
|
|
9
|
+
require_relative "../../ports/persistence/append_only"
|
|
10
|
+
require_relative "../../query_specification/common/null_policy"
|
|
11
|
+
require_relative "../../query_specification/common/order_by"
|
|
12
|
+
require_relative "../../runtime/errors"
|
|
13
|
+
require_relative "../../runtime/event"
|
|
14
|
+
require_relative "../../runtime/instance"
|
|
15
|
+
|
|
16
|
+
module Hecks
|
|
17
|
+
module Adapters
|
|
18
|
+
# Cloudflare D1 — SQLite, managed, reached over its REST API rather
|
|
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
|
+
# and the column encode/decode) and SqlQueryBuilder's dialect hooks
|
|
22
|
+
# are copied near-verbatim from sqlite.rb — the only real difference
|
|
23
|
+
# is the transport (D1::Connection, an HTTP call per query, vs a
|
|
24
|
+
# persistent local sqlite3 handle). See sqlite.rb's own header
|
|
25
|
+
# comment: "this file supplies only SQLite's dialect" — true here too.
|
|
26
|
+
class D1
|
|
27
|
+
# THE TRANSPORT — mirrors just the slice of SQLite3::Database's own
|
|
28
|
+
# interface (execute/get_first_row/get_first_value, rows as
|
|
29
|
+
# column-name-keyed hashes) that Sqlite::SchemaBuilder, Sqlite::Codec,
|
|
30
|
+
# and this file's own methods already assume. One stateless HTTP call
|
|
31
|
+
# per execute, not a persistent connection — D1 has no connection to
|
|
32
|
+
# hold open.
|
|
33
|
+
class Connection
|
|
34
|
+
ENDPOINT = "https://api.cloudflare.com/client/v4"
|
|
35
|
+
|
|
36
|
+
def initialize(account_id:, database_id:, api_token:)
|
|
37
|
+
@uri = URI("#{ENDPOINT}/accounts/#{account_id}/d1/database/#{database_id}/query")
|
|
38
|
+
@api_token = api_token
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def execute(sql, binds = [])
|
|
42
|
+
response_results({ sql: sql, params: binds }).first.fetch("results", [])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# D1 batches are SQL transactions: statements execute in order and a
|
|
46
|
+
# failure rolls the entire sequence back. Keep the tuple-shaped local
|
|
47
|
+
# seam small so adapter code and focused fakes do not need to know the
|
|
48
|
+
# REST request envelope.
|
|
49
|
+
# https://developers.cloudflare.com/d1/worker-api/d1-database/#batch
|
|
50
|
+
def batch(statements)
|
|
51
|
+
payload = {
|
|
52
|
+
batch: statements.map do |sql, binds|
|
|
53
|
+
{ sql: sql, params: binds || [] }
|
|
54
|
+
end
|
|
55
|
+
}
|
|
56
|
+
response_results(payload).map { |result| result.fetch("results", []) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def response_results(payload)
|
|
62
|
+
request = Net::HTTP::Post.new(@uri)
|
|
63
|
+
request["Authorization"] = "Bearer #{@api_token}"
|
|
64
|
+
request["Content-Type"] = "application/json"
|
|
65
|
+
request.body = JSON.generate(payload)
|
|
66
|
+
|
|
67
|
+
response = Net::HTTP.start(@uri.host, @uri.port, use_ssl: true) { |http| http.request(request) }
|
|
68
|
+
body =
|
|
69
|
+
begin
|
|
70
|
+
JSON.parse(response.body)
|
|
71
|
+
rescue JSON::ParserError
|
|
72
|
+
raise Runtime::WiringError, "D1 query failed: non-JSON response (HTTP #{response.code}): #{response.body}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
unless body["success"]
|
|
76
|
+
messages = (body["errors"] || []).map { |error| error["message"] }.join("; ")
|
|
77
|
+
raise Runtime::WiringError, "D1 query failed: #{messages.empty? ? response.body : messages}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
results = body.fetch("result")
|
|
81
|
+
failed = results.find { |result| result["success"] == false }
|
|
82
|
+
if failed
|
|
83
|
+
messages = (body["errors"] || []).map { |error| error["message"] }.join("; ")
|
|
84
|
+
detail = failed["error"] || failed["message"] || messages
|
|
85
|
+
raise Runtime::WiringError, "D1 query failed: #{detail.to_s.empty? ? 'a batched statement failed' : detail}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
results
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
public
|
|
92
|
+
|
|
93
|
+
def get_first_row(sql, binds = [])
|
|
94
|
+
execute(sql, binds).first
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def get_first_value(sql, binds = [])
|
|
98
|
+
get_first_row(sql, binds)&.values&.first
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
include SqlQueryBuilder
|
|
103
|
+
include Sqlite::SchemaBuilder
|
|
104
|
+
include Sqlite::Codec
|
|
105
|
+
|
|
106
|
+
SQL_TYPES = { "Integer" => "INTEGER", "Float" => "REAL" }.freeze
|
|
107
|
+
|
|
108
|
+
attr_reader :aggregate
|
|
109
|
+
|
|
110
|
+
def persistence_capabilities = [:atomic_put]
|
|
111
|
+
|
|
112
|
+
def initialize(aggregate:, settings: {}, root: nil)
|
|
113
|
+
@aggregate = aggregate
|
|
114
|
+
|
|
115
|
+
account_id = settings[:account_id] || settings["account_id"]
|
|
116
|
+
database_id = settings[:database_id] || settings["database_id"]
|
|
117
|
+
api_token = settings[:api_token] || settings["api_token"]
|
|
118
|
+
{ "account_id" => account_id, "database_id" => database_id, "api_token" => api_token }.each do |name, value|
|
|
119
|
+
raise Runtime::WiringError, "D1 needs a #{name.inspect} in its world settings" if value.to_s.empty?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
@db = Connection.new(account_id: account_id, database_id: database_id, api_token: api_token)
|
|
123
|
+
# THE OPTIONAL saga-persistence capability's own scoping column
|
|
124
|
+
# (§2/§4) — D1's domain isolation is by whole-database identity
|
|
125
|
+
# (one D1 database per domain in practice), so unlike Sqlite's
|
|
126
|
+
# own per-aggregate-file default, there's no "which file does
|
|
127
|
+
# this end up in" ambiguity here: every aggregate on D1 within a
|
|
128
|
+
# domain already shares one database.
|
|
129
|
+
@domain = (settings[:domain] || settings["domain"] || aggregate.name).to_s
|
|
130
|
+
|
|
131
|
+
# No PRAGMA synchronous here, unlike Sqlite — D1 is a managed
|
|
132
|
+
# durable service; there is no local fsync policy for a caller to
|
|
133
|
+
# tune, and the query endpoint has no PRAGMA write-access to it.
|
|
134
|
+
create_aggregate_table!
|
|
135
|
+
create_entry_table!
|
|
136
|
+
ensure_entry_operation_column!
|
|
137
|
+
ensure_entry_mirrors_column!
|
|
138
|
+
create_event_table!
|
|
139
|
+
create_saga_table!
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def table = @aggregate.storage_name
|
|
143
|
+
|
|
144
|
+
def find(id)
|
|
145
|
+
row = @db.get_first_row("SELECT * FROM #{quoted_table} WHERE id = ?", [id.to_s])
|
|
146
|
+
return nil unless row
|
|
147
|
+
|
|
148
|
+
Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# order_by IS A RUNTIME VALUE — see postgres.rb's own all for the
|
|
152
|
+
# full reasoning; whitelisted the identical way, same order_clause
|
|
153
|
+
# Sqlite's own all reuses (D1 speaks the identical dialect).
|
|
154
|
+
def all(order_by: nil, direction: :asc)
|
|
155
|
+
order_sql = "ORDER BY id"
|
|
156
|
+
if order_by
|
|
157
|
+
name = order_by.to_s.split(".").first
|
|
158
|
+
raise Runtime::WiringError, "#{@aggregate.name} has no attribute #{order_by.inspect} to order by" unless @aggregate.lifecycle&.field.to_s == name || @aggregate.attribute(name)
|
|
159
|
+
|
|
160
|
+
spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction)
|
|
161
|
+
order_sql = "ORDER BY #{order_clause(spec, nil)}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
@db.execute("SELECT * FROM #{quoted_table} #{order_sql}").map do |row|
|
|
165
|
+
Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def count = @db.get_first_value("SELECT COUNT(*) FROM #{quoted_table}").to_i
|
|
170
|
+
|
|
171
|
+
def append(entry)
|
|
172
|
+
@db.execute(
|
|
173
|
+
"INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES (?, ?, ?, ?)",
|
|
174
|
+
[entry.id, entry.operation, JSON.generate(entry.state), JSON.generate(entry.mirrors)]
|
|
175
|
+
)
|
|
176
|
+
entry
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def project(entry)
|
|
180
|
+
return @db.execute("DELETE FROM #{quoted_table} WHERE id = ?", [entry.id]) if entry.delete?
|
|
181
|
+
|
|
182
|
+
instance = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
|
|
183
|
+
columns = (["id"] + persisted_fields.map { |field| field[:name].to_s }).map { |c| quote_ident(c) }
|
|
184
|
+
values = [instance.id.to_s] + persisted_fields.map { |field| encode_field(field, instance[field[:name]]) }
|
|
185
|
+
slots = Array.new(columns.size, "?").join(", ")
|
|
186
|
+
|
|
187
|
+
@db.execute(
|
|
188
|
+
"INSERT OR REPLACE INTO #{quoted_table} (#{columns.join(', ')}) VALUES (#{slots})",
|
|
189
|
+
values
|
|
190
|
+
)
|
|
191
|
+
instance
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def entries
|
|
195
|
+
@db.execute("SELECT aggregate_id, operation, state, mirrors FROM #{quoted_entry_table} ORDER BY sequence").map do |row|
|
|
196
|
+
state = JSON.parse(row["state"])
|
|
197
|
+
Ports::Persistence::Entry.new(
|
|
198
|
+
operation: row["operation"] || "save",
|
|
199
|
+
id: row["aggregate_id"],
|
|
200
|
+
state: state&.transform_keys(&:to_sym),
|
|
201
|
+
mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def reset!
|
|
207
|
+
@db.execute("DELETE FROM #{quoted_table}")
|
|
208
|
+
@db.execute("DELETE FROM #{quoted_entry_table}")
|
|
209
|
+
self
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def save(instance)
|
|
213
|
+
entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
|
|
214
|
+
append(entry)
|
|
215
|
+
project(entry)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Classification, durable journal append and current-state projection
|
|
219
|
+
# are one D1 batch transaction and therefore one HTTP request. The first
|
|
220
|
+
# statement supplies the outcome from database state inside that same
|
|
221
|
+
# transaction; the runtime performs no preliminary find.
|
|
222
|
+
#
|
|
223
|
+
# `insert_only:` used to spend a SEPARATE, EARLIER round trip finding
|
|
224
|
+
# out whether the row existed before ever building the batch — a real
|
|
225
|
+
# TOCTOU gap (two concurrent creates at the same identity could both
|
|
226
|
+
# pass that check before either wrote). D1's batch has no conditional
|
|
227
|
+
# BRANCH of its own, true, but it does not need one: a batch's own
|
|
228
|
+
# statements already execute in order, atomically, as one transaction
|
|
229
|
+
# (Connection#batch's own comment) — the exact guarantee the single-
|
|
230
|
+
# connection adapters' own `@db.transaction do ... end` gets locally.
|
|
231
|
+
# So the existence check moves INSIDE the batch as its own first
|
|
232
|
+
# statement, and the two writes are individually gated with `WHERE NOT
|
|
233
|
+
# EXISTS (...)` against that same table, evaluated in the same
|
|
234
|
+
# transaction — a row that already existed makes both writes into
|
|
235
|
+
# real, zero-row no-ops rather than skipping them from the Ruby side,
|
|
236
|
+
# matching Sqlite#atomic_put's `next` (skip append AND project both,
|
|
237
|
+
# together) with no second HTTP call and no gap for another writer to
|
|
238
|
+
# land in between the check and the write.
|
|
239
|
+
def atomic_put(entry, insert_only: false)
|
|
240
|
+
instance = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
|
|
241
|
+
columns = (["id"] + persisted_fields.map { |field| field[:name].to_s }).map { |column| quote_ident(column) }
|
|
242
|
+
values = [instance.id.to_s] + persisted_fields.map { |field| encode_field(field, instance[field[:name]]) }
|
|
243
|
+
slots = Array.new(columns.size, "?").join(", ")
|
|
244
|
+
not_exists = "WHERE NOT EXISTS (SELECT 1 FROM #{quoted_table} WHERE id = ?)"
|
|
245
|
+
|
|
246
|
+
status_sql =
|
|
247
|
+
if insert_only
|
|
248
|
+
"SELECT CASE WHEN EXISTS (SELECT 1 FROM #{quoted_table} WHERE id = ?) " \
|
|
249
|
+
"THEN 'conflicted' ELSE 'inserted' END AS status"
|
|
250
|
+
else
|
|
251
|
+
"SELECT CASE WHEN EXISTS (SELECT 1 FROM #{quoted_table} WHERE id = ?) " \
|
|
252
|
+
"THEN 'replaced' ELSE 'inserted' END AS status"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
entry_sql, entry_binds =
|
|
256
|
+
if insert_only
|
|
257
|
+
[
|
|
258
|
+
"INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) " \
|
|
259
|
+
"SELECT ?, ?, ?, ? #{not_exists}",
|
|
260
|
+
[entry.id, entry.operation, JSON.generate(entry.state), JSON.generate(entry.mirrors), entry.id.to_s]
|
|
261
|
+
]
|
|
262
|
+
else
|
|
263
|
+
[
|
|
264
|
+
"INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES (?, ?, ?, ?)",
|
|
265
|
+
[entry.id, entry.operation, JSON.generate(entry.state), JSON.generate(entry.mirrors)]
|
|
266
|
+
]
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
aggregate_sql, aggregate_binds =
|
|
270
|
+
if insert_only
|
|
271
|
+
[
|
|
272
|
+
"INSERT INTO #{quoted_table} (#{columns.join(', ')}) SELECT #{slots} #{not_exists}",
|
|
273
|
+
values + [entry.id.to_s]
|
|
274
|
+
]
|
|
275
|
+
else
|
|
276
|
+
[
|
|
277
|
+
"INSERT OR REPLACE INTO #{quoted_table} (#{columns.join(', ')}) VALUES (#{slots})",
|
|
278
|
+
values
|
|
279
|
+
]
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
results = @db.batch([
|
|
283
|
+
[status_sql, [entry.id.to_s]],
|
|
284
|
+
[entry_sql, entry_binds],
|
|
285
|
+
[aggregate_sql, aggregate_binds]
|
|
286
|
+
])
|
|
287
|
+
|
|
288
|
+
results.fetch(0).fetch(0).fetch("status").to_sym
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def delete(id)
|
|
292
|
+
entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
|
|
293
|
+
append(entry)
|
|
294
|
+
project(entry)
|
|
295
|
+
true
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def record_event(event)
|
|
299
|
+
@db.execute(
|
|
300
|
+
"INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES (?, ?, ?, ?, ?)",
|
|
301
|
+
[event.name, event.aggregate, event.id.to_s, JSON.generate(event.payload), event.occurred_at]
|
|
302
|
+
)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def events
|
|
306
|
+
@db.execute("SELECT * FROM events ORDER BY id").map do |row|
|
|
307
|
+
Runtime::Event.new(
|
|
308
|
+
name: row["name"],
|
|
309
|
+
aggregate: row["aggregate"],
|
|
310
|
+
id: row["aggregate_id"],
|
|
311
|
+
payload: JSON.parse(row["payload"], symbolize_names: true),
|
|
312
|
+
occurred_at: row["occurred_at"]
|
|
313
|
+
)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# ── the OPTIONAL saga-persistence capability (§2) — reuses the DDL
|
|
318
|
+
# `Sqlite::SchemaBuilder` already shares with Sqlite (`d1.rb`'s own
|
|
319
|
+
# file header). Same `?`-placeholder shape every other write here
|
|
320
|
+
# already uses through `Connection#execute`.
|
|
321
|
+
def save_saga(process_manager:, correlation:, state:, memory:)
|
|
322
|
+
@db.execute(
|
|
323
|
+
"INSERT OR REPLACE INTO hecks_saga_instances (domain, process_manager, correlation, state, memory) " \
|
|
324
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
325
|
+
[@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory)]
|
|
326
|
+
)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def delete_saga(process_manager:, correlation:)
|
|
330
|
+
@db.execute(
|
|
331
|
+
"DELETE FROM hecks_saga_instances WHERE domain = ? AND process_manager = ? AND correlation = ?",
|
|
332
|
+
[@domain, process_manager.to_s, correlation.to_s]
|
|
333
|
+
)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def each_saga
|
|
337
|
+
return enum_for(:each_saga) unless block_given?
|
|
338
|
+
|
|
339
|
+
@db.execute(
|
|
340
|
+
"SELECT process_manager, correlation, state, memory FROM hecks_saga_instances WHERE domain = ?",
|
|
341
|
+
[@domain]
|
|
342
|
+
).each do |row|
|
|
343
|
+
yield row["process_manager"], row["correlation"], row["state"],
|
|
344
|
+
JSON.parse(row["memory"], symbolize_names: true)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
private
|
|
349
|
+
|
|
350
|
+
# ── SqlQueryBuilder's dialect hooks — identical to Sqlite's, since
|
|
351
|
+
# D1 speaks the same dialect (copied, not shared by module include,
|
|
352
|
+
# because Sqlite's own copies are private instance methods on a
|
|
353
|
+
# different class — see the file header) ───────────────────────
|
|
354
|
+
def select_list = "*"
|
|
355
|
+
def from_relation = quoted_table
|
|
356
|
+
def dialect_name = "D1"
|
|
357
|
+
def empty_in_clause = "0"
|
|
358
|
+
|
|
359
|
+
def placeholder(binds, value)
|
|
360
|
+
binds << value
|
|
361
|
+
"?"
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def contains_clause(expression, placeholder)
|
|
365
|
+
"instr(#{expression}, #{placeholder}) > 0"
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def list_contains_clause(column, member, placeholder)
|
|
369
|
+
target = member.empty? ? "json_each.value" : "json_extract(json_each.value, '$.#{member}')"
|
|
370
|
+
"EXISTS (SELECT 1 FROM json_each(#{quote_ident(column)}) WHERE #{target} = #{placeholder})"
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def plain_column(name) = quote_ident(name)
|
|
374
|
+
|
|
375
|
+
def nested_expression(name, path, member)
|
|
376
|
+
json_path = path.empty? ? "$.#{member || 'value'}" : "$.#{path.join('.')}"
|
|
377
|
+
"json_extract(#{quote_ident(name)}, '#{json_path}')"
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# SQLite (and D1, the same engine) has no bare OFFSET — LIMIT -1 is
|
|
381
|
+
# its own documented unbounded spelling, exactly for this case.
|
|
382
|
+
def unbounded_limit = " LIMIT -1"
|
|
383
|
+
|
|
384
|
+
def order_clause(order_by, policy)
|
|
385
|
+
QuerySpecification::Common::NullPolicy.sql_order(query_expression(order_by.field), order_by.direction, policy)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def execute_query(sql, binds)
|
|
389
|
+
@db.execute(sql, binds).map { |row| Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row)) }
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# ── the rest of the dialect ─────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
def quote_ident(name)
|
|
395
|
+
%("#{name.to_s.gsub('"', '""')}")
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def quoted_table = quote_ident(table)
|
|
399
|
+
def entry_table = "#{table}_entries"
|
|
400
|
+
def quoted_entry_table = quote_ident(entry_table)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|