hecks 1.4.0 → 1.5.1
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 +65 -0
- data/lib/hecks/adapters/driven/folder.rb +73 -0
- data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
- data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
- data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
- data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
- data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +93 -2
- data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
- 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 +35 -7
- data/lib/hecks/adapters/driven/lambda.rb +43 -2
- data/lib/hecks/adapters/driven/local_storage.rb +67 -1
- data/lib/hecks/adapters/driven/memory.rb +13 -13
- data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
- data/lib/hecks/adapters/driven/prism.rb +46 -7
- data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
- 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 +2 -0
- data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
- data/lib/hecks/behaviors/dsl.rb +58 -0
- data/lib/hecks/behaviors/expectations.rb +168 -7
- data/lib/hecks/behaviors/ir.rb +11 -0
- data/lib/hecks/behaviors/rspec.rb +8 -0
- data/lib/hecks/behaviors/runner.rb +19 -0
- data/lib/hecks/behaviors.rb +8 -0
- data/lib/hecks/bluebook/aggregate.rb +30 -0
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
- data/lib/hecks/bluebook/assembly/build.rb +25 -4
- data/lib/hecks/bluebook/assembly/contract.rb +66 -14
- data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
- data/lib/hecks/bluebook/assembly/marks.rb +143 -14
- data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
- data/lib/hecks/bluebook/assembly.rb +25 -7
- data/lib/hecks/bluebook/attribute.rb +17 -3
- data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
- data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
- data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
- data/lib/hecks/bluebook/behaviour/command.rb +33 -3
- data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
- data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
- data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
- data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
- data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
- data/lib/hecks/bluebook/behaviour/query.rb +5 -0
- data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
- data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
- data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
- data/lib/hecks/bluebook/chapter.rb +23 -0
- data/lib/hecks/bluebook/command.rb +53 -8
- data/lib/hecks/bluebook/domain_port.rb +25 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
- data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
- data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
- data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
- data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
- data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
- data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
- data/lib/hecks/bluebook/entity.rb +29 -0
- data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
- data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
- data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
- data/lib/hecks/bluebook/expression/resolver.rb +320 -77
- data/lib/hecks/bluebook/hexagon.rb +34 -0
- data/lib/hecks/bluebook/lifecycle.rb +11 -0
- data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
- data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
- data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
- data/lib/hecks/bluebook/meta_validator.rb +180 -84
- data/lib/hecks/bluebook/model_check.rb +268 -24
- data/lib/hecks/bluebook/pattern_subset.rb +23 -1
- data/lib/hecks/bluebook/process_manager.rb +13 -0
- 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 +44 -0
- data/lib/hecks/bluebook/query.rb +27 -0
- data/lib/hecks/bluebook/read_model.rb +21 -1
- data/lib/hecks/bluebook/reference.rb +21 -8
- data/lib/hecks/bluebook/smoke_test.rb +29 -6
- data/lib/hecks/bluebook/synthesizer.rb +34 -0
- data/lib/hecks/bluebook/translation.rb +30 -1
- data/lib/hecks/bluebook/value_object.rb +23 -5
- data/lib/hecks/bluebook.rb +3 -4
- data/lib/hecks/codemod.rb +107 -20
- data/lib/hecks/construct.rb +15 -1
- data/lib/hecks/corpus.rb +146 -25
- data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
- data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
- data/lib/hecks/deploy/oidc.json +5 -0
- data/lib/hecks/doc/reference.rb +185 -16
- data/lib/hecks/embryonaut_bluebook.rb +32 -9
- data/lib/hecks/facade/handle.rb +76 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
- data/lib/hecks/forms/field_shape.rb +3 -0
- data/lib/hecks/forms/page.rb +14 -0
- data/lib/hecks/forms/port_argument.rb +12 -0
- data/lib/hecks/forms/query_form_renderer.rb +63 -0
- data/lib/hecks/forms/record_renderer.rb +58 -0
- data/lib/hecks/forms/record_table.rb +27 -0
- data/lib/hecks/forms/reference_options.rb +24 -0
- data/lib/hecks/forms/value_object_shape.rb +10 -0
- data/lib/hecks/fqn.rb +58 -0
- data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
- data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
- data/lib/hecks/framework/oidc.json +15 -0
- data/lib/hecks/framework.rb +43 -20
- data/lib/hecks/freezer.rb +17 -1
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
- data/lib/hecks/fuzzing/combination_miner.rb +59 -0
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
- data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
- data/lib/hecks/fuzzing/differential.rb +34 -0
- data/lib/hecks/fuzzing/domain_generator.rb +188 -11
- data/lib/hecks/fuzzing/era_boundary.rb +45 -15
- data/lib/hecks/fuzzing/form_census.rb +86 -0
- data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
- data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
- data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
- data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
- data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
- data/lib/hecks/fuzzing/properties/guards.rb +44 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
- data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
- data/lib/hecks/fuzzing/properties/querying.rb +68 -14
- data/lib/hecks/fuzzing/properties.rb +24 -15
- data/lib/hecks/fuzzing/qa_settings.rb +12 -0
- data/lib/hecks/fuzzing/replay.rb +137 -29
- data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
- data/lib/hecks/fuzzing/self_consistency.rb +189 -40
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
- data/lib/hecks/fuzzing/shrinker.rb +114 -2
- data/lib/hecks/fuzzing/structural_skips.rb +18 -4
- data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
- data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
- data/lib/hecks/fuzzing/value_generator.rb +98 -10
- data/lib/hecks/grammar/evolve.rb +178 -2
- data/lib/hecks/grammar.rb +46 -0
- data/lib/hecks/ir.rb +38 -7
- data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
- data/lib/hecks/literal.rb +32 -0
- data/lib/hecks/naming.rb +88 -7
- data/lib/hecks/ports/access_control.rb +5 -10
- data/lib/hecks/ports/authorization.rb +3 -6
- data/lib/hecks/ports/identity_assignment.rb +1 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -2
- data/lib/hecks/ports/key_vault.port +6 -0
- data/lib/hecks/ports/key_vault.rb +85 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
- data/lib/hecks/ports/query/in_memory.rb +39 -6
- data/lib/hecks/ports/query/ordering.rb +15 -0
- data/lib/hecks/ports.rb +1 -0
- data/lib/hecks/projections/bootstrap_table.rb +43 -8
- data/lib/hecks/projections/deploy/fargate.rb +666 -0
- data/lib/hecks/projections/deploy/lambda.rb +2423 -0
- data/lib/hecks/projections/deploy/shared.rb +624 -0
- data/lib/hecks/projections/diagrams.rb +243 -7
- data/lib/hecks/projections/glossary/html.rb +88 -0
- data/lib/hecks/projections/glossary/markdown.rb +47 -0
- data/lib/hecks/projections/glossary/mermaid.rb +48 -0
- data/lib/hecks/projections/glossary/sentences.rb +105 -0
- data/lib/hecks/projections/glossary.rb +161 -19
- data/lib/hecks/projections/model/deviations.rb +44 -0
- data/lib/hecks/projections/model.rb +51 -1
- data/lib/hecks/projections/oidc.rb +40 -3
- data/lib/hecks/projections/parser_table.rb +49 -8
- data/lib/hecks/projections/reference.rb +12 -0
- data/lib/hecks/projections/rust_vocabulary.rb +219 -16
- data/lib/hecks/projections/shape.rb +8 -0
- data/lib/hecks/projections/statements.rb +63 -16
- data/lib/hecks/projections/vocabulary.rb +17 -0
- data/lib/hecks/projections.rb +3 -0
- data/lib/hecks/projector/cli_projector.rb +218 -10
- data/lib/hecks/projector/docs_projector.rb +145 -19
- data/lib/hecks/projector/exporter.rb +65 -11
- data/lib/hecks/projector/ir_projector.rb +6 -0
- data/lib/hecks/projector/narrate_projector.rb +136 -15
- data/lib/hecks/projector/target.rb +47 -10
- data/lib/hecks/projector.rb +101 -19
- data/lib/hecks/query_ir.rb +47 -0
- data/lib/hecks/query_specification/common/null_policy.rb +5 -3
- data/lib/hecks/rendering.rb +6 -0
- data/lib/hecks/router/namespace_installer.rb +13 -0
- data/lib/hecks/router.rb +55 -0
- data/lib/hecks/runtime/aggregate_lock.rb +9 -0
- data/lib/hecks/runtime/boot_gates.rb +18 -0
- data/lib/hecks/runtime/caller.rb +32 -0
- data/lib/hecks/runtime/capability_graph.rb +11 -0
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
- data/lib/hecks/runtime/command_interpreter.rb +42 -17
- data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
- data/lib/hecks/runtime/command_rules/references.rb +118 -28
- data/lib/hecks/runtime/dependency_planning.rb +45 -0
- data/lib/hecks/runtime/dispatcher.rb +28 -50
- data/lib/hecks/runtime/entity_element.rb +161 -8
- data/lib/hecks/runtime/entity_interpreter.rb +44 -9
- data/lib/hecks/runtime/errors.rb +18 -4
- data/lib/hecks/runtime/event.rb +10 -5
- data/lib/hecks/runtime/identity.rb +71 -3
- data/lib/hecks/runtime/instance.rb +67 -7
- data/lib/hecks/runtime/interpreting.rb +13 -5
- data/lib/hecks/runtime/invocation.rb +118 -36
- data/lib/hecks/runtime/loader.rb +94 -8
- data/lib/hecks/runtime/outbox.rb +145 -7
- data/lib/hecks/runtime/policy_interpreter.rb +22 -9
- data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
- data/lib/hecks/runtime/query_interpreter.rb +40 -12
- data/lib/hecks/runtime/reaction_invocation.rb +53 -8
- data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
- data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
- data/lib/hecks/runtime/reference_hop.rb +42 -0
- data/lib/hecks/runtime/refusal_wording.rb +50 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
- data/lib/hecks/runtime/registry/verification.rb +119 -4
- data/lib/hecks/runtime/registry.rb +157 -4
- data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
- data/lib/hecks/runtime/routing.rb +27 -2
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
- data/lib/hecks/runtime/saga_interpreter.rb +27 -13
- data/lib/hecks/runtime/tenant_check.rb +26 -6
- data/lib/hecks/runtime/tenant_scope.rb +18 -0
- data/lib/hecks/runtime/value/coercion.rb +255 -33
- data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
- data/lib/hecks/runtime/value.rb +50 -0
- data/lib/hecks/runtime.rb +32 -0
- data/lib/hecks/storehouse.rb +305 -9
- data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
- data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks.rb +79 -1
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
- metadata +14 -5
- data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
- data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
- data/lib/hecks/deprecation.rb +0 -95
- data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
|
@@ -253,19 +253,19 @@ module Hecks
|
|
|
253
253
|
|
|
254
254
|
private
|
|
255
255
|
|
|
256
|
-
#
|
|
257
|
-
#
|
|
256
|
+
# The language's own bootstrap saves its self-description quadratically
|
|
257
|
+
# otherwise. `MetaValidator::Judge` dispatches every declaration in the
|
|
258
258
|
# self-hosted grammar into a fresh, private, never-durable `Memory`
|
|
259
259
|
# store (meta_validator.rb's own header: "each bluebook is judged in a
|
|
260
260
|
# fresh in-memory store") — and every nested-entity dispatch (a
|
|
261
261
|
# `ValueObject::Member`, then one `ValueObject::Member::Pair` per
|
|
262
|
-
# key/value pair) re-saves the
|
|
262
|
+
# key/value pair) re-saves the whole parent aggregate, because entities
|
|
263
263
|
# have no storage of their own (S17, ADR 0026). A table of N member
|
|
264
|
-
# rows costs O(N) dispatches, each
|
|
264
|
+
# rows costs O(N) dispatches, each paying twice for the aggregate's own
|
|
265
265
|
# size-N state: once in `StateCodec.copy` (`append`/`project`'s own
|
|
266
266
|
# encode-then-decode round trip) and again in `Instance#initialize`'s
|
|
267
267
|
# `hydrate_with_defaults`, which re-walks and re-validates every
|
|
268
|
-
# element of an entity list on
|
|
268
|
+
# element of an entity list on every save regardless of how many of
|
|
269
269
|
# them were already valid as of the previous one
|
|
270
270
|
# (`Value::EntityListCoercion#hydrate_entity_list` has no "already
|
|
271
271
|
# hydrated" shortcut for entity elements — only a value-object list
|
|
@@ -277,7 +277,7 @@ module Hecks
|
|
|
277
277
|
#
|
|
278
278
|
# `Runtime::Value.judge_bootstrapping?` (judge.rb's own `send_to`,
|
|
279
279
|
# wrapping every dispatch the judge makes) is already the flag that
|
|
280
|
-
# marks exactly this window and
|
|
280
|
+
# marks exactly this window and nothing else — "never for a real
|
|
281
281
|
# domain's own declared value objects... which Judge never dispatches
|
|
282
282
|
# commands against" (coercion.rb's own comment on the same flag). It
|
|
283
283
|
# is reused here rather than a new toggle for the same reason: one
|
|
@@ -286,13 +286,13 @@ module Hecks
|
|
|
286
286
|
# both round trips here) is simpler to reason about than two flags
|
|
287
287
|
# that would always be true or false together.
|
|
288
288
|
#
|
|
289
|
-
#
|
|
289
|
+
# Why skipping both is safe here, and only here: `entry.state` a save
|
|
290
290
|
# ever hands this adapter is always `instance.state.dup`
|
|
291
|
-
# (`AppendOnly#save`) — a shallow copy of an
|
|
292
|
-
#
|
|
291
|
+
# (`AppendOnly#save`) — a shallow copy of an already-hydrated,
|
|
292
|
+
# already-validated live `Instance`'s own state, built by the very
|
|
293
293
|
# same `Value.for_attribute`/`hydrate_with_defaults` machinery
|
|
294
294
|
# `StateCodec.copy` and `Instance.new`'s default (`hydrate: true`)
|
|
295
|
-
# path would otherwise redo. Every
|
|
295
|
+
# path would otherwise redo. Every value inside it — a `Runtime::
|
|
296
296
|
# Value` (frozen through, see value.rb's own header) or a `list_of`
|
|
297
297
|
# attribute's own array (`Freezer.deep`d the moment it was built,
|
|
298
298
|
# instance.rb's own header on `Instance#dup`) — is already immutable,
|
|
@@ -303,13 +303,13 @@ module Hecks
|
|
|
303
303
|
# pure, avoidable cost for this one caller — never a correctness
|
|
304
304
|
# requirement.
|
|
305
305
|
#
|
|
306
|
-
# `bootstrap_fast_path?` is the guard, and it is deliberately
|
|
306
|
+
# `bootstrap_fast_path?` is the guard, and it is deliberately cheap —
|
|
307
307
|
# O(this aggregate's own declared attribute count), never O(N) — so
|
|
308
308
|
# it cannot reintroduce the very cost it exists to avoid: `StateCodec.
|
|
309
309
|
# decoded?` (the obvious-looking alternative) recurses into every
|
|
310
|
-
# `list_of` element to check
|
|
310
|
+
# `list_of` element to check it, which is exactly the O(N) walk this
|
|
311
311
|
# whole change removes. A `Hash` with every top-level key already a
|
|
312
|
-
# `Symbol` is what `Instance#state`
|
|
312
|
+
# `Symbol` is what `Instance#state` always looks like (`Value.hydrate`
|
|
313
313
|
# refuses anything else, coercion.rb's own header), so it is checked
|
|
314
314
|
# here instead — true for the actual shape every real save has,
|
|
315
315
|
# false (falling back to the always-correct slow path) for anything
|
|
@@ -17,8 +17,28 @@ module Hecks
|
|
|
17
17
|
# webhook against it (a smoke test, CI) has something real to key
|
|
18
18
|
# off of — the real shape production has, minus the network hop.
|
|
19
19
|
class MockStripeAdapter
|
|
20
|
+
# Accepts and discards the shared adapter constructor arguments; this adapter holds no
|
|
21
|
+
# state of its own.
|
|
22
|
+
#
|
|
23
|
+
# @param aggregate [Bluebook::Aggregate, nil] accepted for the shared adapter
|
|
24
|
+
# constructor shape and ignored
|
|
25
|
+
# @param settings [Hash] accepted for the shared adapter constructor shape and ignored
|
|
26
|
+
# @param root [String, nil] accepted for the shared adapter constructor shape and ignored
|
|
20
27
|
def initialize(aggregate: nil, settings: {}, root: nil); end
|
|
21
28
|
|
|
29
|
+
# Builds a fake checkout URL carrying `registration_id`, standing in for a real
|
|
30
|
+
# Checkout-Session call.
|
|
31
|
+
#
|
|
32
|
+
# @param event [Object] the record data a real adapter would read the price and name
|
|
33
|
+
# from; adapter-defined shape (whatever the declaring bluebook's `checkout` port
|
|
34
|
+
# operation supplies as `event`) and never read here
|
|
35
|
+
# @param registration_id [String] the registration id to key a signed webhook off of;
|
|
36
|
+
# embedded in the returned URL, never read otherwise
|
|
37
|
+
# @param success_url [String] the base URL to redirect a successful payer to
|
|
38
|
+
# @param cancel_url [String] accepted for the shared checkout-adapter call shape and
|
|
39
|
+
# never read
|
|
40
|
+
# @return [String] `success_url` with `mock_checkout=1` and `mock_registration_id`
|
|
41
|
+
# query parameters appended
|
|
22
42
|
def create_session(event:, registration_id:, success_url:, cancel_url:)
|
|
23
43
|
separator = success_url.include?("?") ? "&" : "?"
|
|
24
44
|
"#{success_url}#{separator}mock_checkout=1&mock_registration_id=#{registration_id}"
|
|
@@ -21,11 +21,22 @@ module Hecks
|
|
|
21
21
|
|
|
22
22
|
module_function
|
|
23
23
|
|
|
24
|
+
# Recovers a block's body as canonical source text.
|
|
25
|
+
#
|
|
26
|
+
# @param block [Proc] a block written in a bluebook file, such as a `given` predicate
|
|
27
|
+
# or an `identified_by` path
|
|
28
|
+
# @return [String, nil] the block body's source, normalised by
|
|
29
|
+
# `Bluebook::Expression::CanonicalForm`; nil if `body_source` finds none
|
|
24
30
|
def canonical(block)
|
|
25
31
|
body = body_source(block)
|
|
26
32
|
body && canonicalise(body)
|
|
27
33
|
end
|
|
28
34
|
|
|
35
|
+
# Reads a block's own body source text back out of the file it was defined in.
|
|
36
|
+
#
|
|
37
|
+
# @param block [Proc] the block to locate, via its own `source_location`
|
|
38
|
+
# @return [String, nil] the block body's raw source; nil if the file cannot be read,
|
|
39
|
+
# or no block node starts on the block's own line
|
|
29
40
|
def body_source(block)
|
|
30
41
|
file, line = block.source_location
|
|
31
42
|
return nil unless file && File.readable?(file)
|
|
@@ -34,6 +45,12 @@ module Hecks
|
|
|
34
45
|
node&.body&.slice
|
|
35
46
|
end
|
|
36
47
|
|
|
48
|
+
# Finds the parsed block node starting on a given line of a file.
|
|
49
|
+
#
|
|
50
|
+
# @param file [String] the file path, parsed (and cached) through `tree_for`
|
|
51
|
+
# @param line [Integer] the 1-based source line the block starts on
|
|
52
|
+
# @return [Prism::BlockNode, nil] the first block node found starting on `line`, or nil
|
|
53
|
+
# if none does
|
|
37
54
|
def block_node_at(file, line)
|
|
38
55
|
found = nil
|
|
39
56
|
walk(tree_for(file)) do |node|
|
|
@@ -45,12 +62,16 @@ module Hecks
|
|
|
45
62
|
found
|
|
46
63
|
end
|
|
47
64
|
|
|
65
|
+
# Parses a file with `prism`, caching the result for the life of the process.
|
|
66
|
+
#
|
|
67
|
+
# @param file [String] the file path to parse and cache under
|
|
68
|
+
# @return [Prism::ProgramNode] the parsed syntax tree's root node
|
|
48
69
|
def tree_for(file)
|
|
49
70
|
# ::Prism.parse_file(file) reads the file itself, at the C
|
|
50
71
|
# extension level — bypassing Ruby's own File/IO layer entirely.
|
|
51
72
|
# That's invisible to anything that virtualizes the filesystem at
|
|
52
73
|
# the Ruby level instead of the OS level (e.g. tebako's memfs,
|
|
53
|
-
#
|
|
74
|
+
# which presses a hecks-based app into a single executable —
|
|
54
75
|
# see domain/README.md's "Deploying" section in lifeadelics for
|
|
55
76
|
# why that matters). ::Prism.parse(File.read(file)) parses the
|
|
56
77
|
# exact same bytes, just read through Ruby's File.read first,
|
|
@@ -66,16 +87,29 @@ module Hecks
|
|
|
66
87
|
# tree reports a `given`/`ensures` block at its old line number,
|
|
67
88
|
# which no longer matches the freshly re-executed file's own
|
|
68
89
|
# `block.source_location` — surfacing as "did not survive
|
|
69
|
-
# extraction" on a perfectly valid file.
|
|
70
|
-
# `Hecks::Codemod` (lib/hecks/codemod.rb), which
|
|
71
|
-
#
|
|
72
|
-
# detail poked from outside. `forget
|
|
73
|
-
# API so nothing else that reloads an
|
|
74
|
-
# to know `TREES` exists at all.
|
|
90
|
+
# extraction" on a perfectly valid file. Built for real building
|
|
91
|
+
# `Hecks::Codemod` (lib/hecks/codemod.rb), which needs exactly this
|
|
92
|
+
# invalidation rather than reaching into `TREES.clear` directly — a
|
|
93
|
+
# private implementation detail poked from outside. `forget`/
|
|
94
|
+
# `forget_all` are the real API so nothing else that reloads an
|
|
95
|
+
# edited file in-process has to know `TREES` exists at all.
|
|
96
|
+
#
|
|
97
|
+
# @param file [String] the file path to drop from the cache
|
|
98
|
+
# @return [Prism::ProgramNode, nil] the cached tree that was removed, or nil if
|
|
99
|
+
# nothing was cached for `file`
|
|
75
100
|
def forget(file) = TREES.delete(file)
|
|
76
101
|
|
|
102
|
+
# Drops every cached parse tree.
|
|
103
|
+
#
|
|
104
|
+
# @return [Hash] the now-empty cache
|
|
77
105
|
def forget_all = TREES.clear
|
|
78
106
|
|
|
107
|
+
# Visits `node` and every descendant depth-first, calling `visit` on each.
|
|
108
|
+
#
|
|
109
|
+
# @param node [Prism::Node, Object] the node to walk; anything that is not a
|
|
110
|
+
# `Prism::Node` is a silent no-op
|
|
111
|
+
# @yieldparam node [Prism::Node] each node visited, `node` itself first
|
|
112
|
+
# @return [void]
|
|
79
113
|
def walk(node, &visit)
|
|
80
114
|
return unless node.is_a?(::Prism::Node)
|
|
81
115
|
|
|
@@ -83,6 +117,11 @@ module Hecks
|
|
|
83
117
|
node.compact_child_nodes.each { |child| walk(child, &visit) }
|
|
84
118
|
end
|
|
85
119
|
|
|
120
|
+
# Normalises raw source text into the framework's own canonical form.
|
|
121
|
+
#
|
|
122
|
+
# @param source [String] the raw source text to normalise
|
|
123
|
+
# @return [String] the normalised text (whitespace collapsed, linked replacements
|
|
124
|
+
# applied)
|
|
86
125
|
def canonicalise(source)
|
|
87
126
|
Bluebook::Expression::CanonicalForm.apply(source)
|
|
88
127
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Hecks
|
|
6
|
+
module Adapters
|
|
7
|
+
# THE IMPERATIVE HALF of provisioning a tenant, pulled out of
|
|
8
|
+
# `bin/project_tenant` and behind a real driven port
|
|
9
|
+
# (`Deploy::Tenant.port "TenantProvisioning"`) instead — the same
|
|
10
|
+
# hexagonal reasoning `GithubChecks` already gives for its own
|
|
11
|
+
# port: real, impure, side-effecting work (writing the overlay
|
|
12
|
+
# file) belongs in an adapter, not in a bare script or a command
|
|
13
|
+
# handler. `Deploy::Tenant`'s own `asks "Provision"` operation calls
|
|
14
|
+
# `#provision` and turns whatever it returns into `TenantProvisioned`,
|
|
15
|
+
# or whatever it raises into `ProvisioningRefused` — see
|
|
16
|
+
# `PortOperationInterpreter#ask`'s own "every failure is an answer"
|
|
17
|
+
# comment for why nothing here needs its own rescue.
|
|
18
|
+
#
|
|
19
|
+
# DELIBERATELY DOES NOT BOOT THE TARGET DOMAIN — that step (proving
|
|
20
|
+
# it boots for real, running the tenant_capable? gate) stays in
|
|
21
|
+
# `bin/project_tenant` itself, called AFTER this operation answers,
|
|
22
|
+
# not nested inside it. Booting a whole domain from inside an
|
|
23
|
+
# adapter method that is itself running mid-dispatch (this port
|
|
24
|
+
# operation) is a real, separate concern from writing one file, and
|
|
25
|
+
# keeping the two apart avoids a nested `Hecks.boot` call ever
|
|
26
|
+
# running inside another boot's own dispatch call stack.
|
|
27
|
+
#
|
|
28
|
+
# RETURNS A HASH SHAPED LIKE `Tenancy::Tenant.Register`'S OWN
|
|
29
|
+
# ARGUMENTS, on purpose — `tenancy.hecksagon`'s own `translates`
|
|
30
|
+
# reaction forwards an answered event's payload verbatim, so this
|
|
31
|
+
# adapter's own return value IS the translation from Deploy's
|
|
32
|
+
# vocabulary into Tenancy's, decided here rather than in a mapping
|
|
33
|
+
# layer neither side could see into.
|
|
34
|
+
class TenantProvisioner
|
|
35
|
+
def initialize(aggregate: nil, settings: {}, root: nil); end
|
|
36
|
+
|
|
37
|
+
# `directory:` IS REQUIRED, NOT DERIVED FROM `domain` — a real,
|
|
38
|
+
# confirmed bug found only under CI's own real-Postgres suite: a
|
|
39
|
+
# domain's own DECLARED NAME (`domain`, e.g. "Scratch") is not a
|
|
40
|
+
# filesystem path, and File.expand_path(domain, Dir.pwd) silently
|
|
41
|
+
# resolved to the wrong directory (relative to whatever the
|
|
42
|
+
# RUNNING PROCESS' own cwd happened to be) whenever the CLI's own
|
|
43
|
+
# `--domain=` value didn't happen to equal its own directory
|
|
44
|
+
# argument by coincidence. `bin/project_tenant` passes its own
|
|
45
|
+
# real `domain_directory` local straight through.
|
|
46
|
+
|
|
47
|
+
def provision(slug:, domain:, realm:, schema:, database:, adapter:, directory:)
|
|
48
|
+
domain_directory = File.expand_path(directory[:value] || directory)
|
|
49
|
+
|
|
50
|
+
overlay_path = File.join(domain_directory, "environments", "#{slug[:value] || slug}.world")
|
|
51
|
+
FileUtils.mkdir_p(File.dirname(overlay_path))
|
|
52
|
+
File.write(overlay_path, <<~WORLD)
|
|
53
|
+
Hecks.world "#{domain[:value] || domain}" do
|
|
54
|
+
realm "#{realm[:value] || realm}"
|
|
55
|
+
persisted_by("#{adapter[:value] || adapter}") do
|
|
56
|
+
database "#{database[:value] || database}"
|
|
57
|
+
schema "#{schema[:value] || schema}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
WORLD
|
|
61
|
+
|
|
62
|
+
{ slug: slug, domain: domain, realm: realm, schema: schema }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -40,7 +40,9 @@ require_relative "driven/prism"
|
|
|
40
40
|
require_relative "driven/folder"
|
|
41
41
|
require_relative "driven/d1"
|
|
42
42
|
require_relative "driven/mock_stripe_adapter"
|
|
43
|
+
require_relative "driven/tenant_provisioner"
|
|
43
44
|
require_relative "driven/secure_random_identity"
|
|
45
|
+
require_relative "driven/in_process_key_vault"
|
|
44
46
|
require_relative "driven/system_clock"
|
|
45
47
|
# `SequentialIdentity` — the deterministic identity_generation test
|
|
46
48
|
# double — is not required here on purpose. It lives at
|
|
@@ -26,6 +26,8 @@ module Hecks
|
|
|
26
26
|
# in `qa/adapters/github_ci_webhook.rb`, the subclass of this file
|
|
27
27
|
# that actually knows what a `QualityControl::Clearance` is.
|
|
28
28
|
#
|
|
29
|
+
# ## A plain rack app
|
|
30
|
+
#
|
|
29
31
|
# A plain rack app (`#call(env)`) — no Sinatra, no Rails — the same
|
|
30
32
|
# shape `Hecks::Forms::App` (lib/hecks/forms/app.rb) already
|
|
31
33
|
# established for the one other HTTP-facing surface this library
|
|
@@ -37,7 +39,9 @@ module Hecks
|
|
|
37
39
|
# `rack` installed, the same "opt in by requiring the file at all"
|
|
38
40
|
# contract `hecks/forms.rb` already has for `Forms::App`.
|
|
39
41
|
#
|
|
40
|
-
#
|
|
42
|
+
# ## Subclass responsibility
|
|
43
|
+
#
|
|
44
|
+
# Implement `#handle_event(event, action,
|
|
41
45
|
# payload)`, returning `[http_status, response_body_hash]`. Called
|
|
42
46
|
# only after the signature has verified and the body has parsed as
|
|
43
47
|
# JSON — a subclass never has to re-check either. `event` is
|
|
@@ -79,12 +83,21 @@ module Hecks
|
|
|
79
83
|
# or wherever it keeps one — rather than this class reaching into
|
|
80
84
|
# the environment itself and hiding that requirement inside a
|
|
81
85
|
# default.
|
|
86
|
+
# @param secret [String] the shared webhook secret (e.g. `ENV.fetch
|
|
87
|
+
# ("GITHUB_WEBHOOK_SECRET")`), checked against each request's `X-Hub-Signature-256`
|
|
88
|
+
# @raise [ArgumentError] if `secret` is nil or empty
|
|
82
89
|
def initialize(secret:)
|
|
83
90
|
raise ArgumentError, "no webhook secret configured" if secret.to_s.empty?
|
|
84
91
|
|
|
85
92
|
@secret = secret
|
|
86
93
|
end
|
|
87
94
|
|
|
95
|
+
# The rack entry point: verifies, parses, and routes one webhook request.
|
|
96
|
+
#
|
|
97
|
+
# @param env [Hash] the rack environment
|
|
98
|
+
# @return [Array(Integer, Hash, Array<String>)] the rack response triple — status,
|
|
99
|
+
# headers, and a one-element body Array holding the JSON-encoded response
|
|
100
|
+
# @raise [NotImplementedError] if the subclass has not implemented `#handle_event`
|
|
88
101
|
def call(env)
|
|
89
102
|
request = Rack::Request.new(env)
|
|
90
103
|
return respond(405, error: "MethodNotAllowed", message: "POST only") unless request.post?
|
data/lib/hecks/behaviors/dsl.rb
CHANGED
|
@@ -16,6 +16,7 @@ module Hecks
|
|
|
16
16
|
# `validate_expect!` (private, below) is where a malformed or empty
|
|
17
17
|
# `expect` is refused at build time rather than silently passing later.
|
|
18
18
|
class TestCaseBuilder
|
|
19
|
+
# @param description [String] the `test "description" do ... end` text
|
|
19
20
|
def initialize(description)
|
|
20
21
|
@description = description
|
|
21
22
|
@tests_command = nil
|
|
@@ -26,19 +27,46 @@ module Hecks
|
|
|
26
27
|
@expect = {}
|
|
27
28
|
end
|
|
28
29
|
|
|
30
|
+
# Records which command or query this test exercises.
|
|
31
|
+
#
|
|
32
|
+
# @param command [String, Symbol] the command or query verb, bare or dotted FQN
|
|
33
|
+
# @param on [String, Symbol, nil] the aggregate `command` is dispatched on, when
|
|
34
|
+
# `command` is not already a dotted FQN
|
|
35
|
+
# @param kind [Symbol] `:command` or `:query`
|
|
36
|
+
# @return [void]
|
|
29
37
|
def tests(command, on: nil, kind: :command)
|
|
30
38
|
@tests_command = command
|
|
31
39
|
@on_aggregate = on
|
|
32
40
|
@kind = kind
|
|
33
41
|
end
|
|
34
42
|
|
|
43
|
+
# Records one setup command to dispatch before the test itself runs.
|
|
44
|
+
#
|
|
45
|
+
# @param command [String, Symbol] the setup command's verb
|
|
46
|
+
# @param kwargs [Hash{Symbol => Object}] the setup command's own arguments
|
|
47
|
+
# @return [void]
|
|
35
48
|
def setup(command, **kwargs)
|
|
36
49
|
@setups << TestSetup.new(command: command, args: kwargs)
|
|
37
50
|
end
|
|
38
51
|
|
|
52
|
+
# Merges fields into the arguments the tested command or query is called with.
|
|
53
|
+
#
|
|
54
|
+
# @param kwargs [Hash{Symbol => Object}] argument names and values to merge in
|
|
55
|
+
# @return [Hash{Symbol => Object}] the input Hash so far
|
|
39
56
|
def input(**kwargs) = @input.merge!(kwargs)
|
|
57
|
+
|
|
58
|
+
# Merges fields into what this test expects of the tested dispatch.
|
|
59
|
+
#
|
|
60
|
+
# @param kwargs [Hash{Symbol => Object}] expectation keys (`ok:`, `refused:`,
|
|
61
|
+
# `emits:`, `count:`, or a field name) and their expected values
|
|
62
|
+
# @return [Hash{Symbol => Object}] the expectation Hash so far
|
|
40
63
|
def expect(**kwargs) = @expect.merge!(kwargs)
|
|
41
64
|
|
|
65
|
+
# Builds the `TestCase` this builder collected.
|
|
66
|
+
#
|
|
67
|
+
# @return [TestCase] the built test case
|
|
68
|
+
# @raise [Malformed] if `tests` was never called, `expect` is empty, or
|
|
69
|
+
# `expect` declares `count:` on a command or `emits:` on a query
|
|
42
70
|
def build
|
|
43
71
|
unless @tests_command
|
|
44
72
|
raise Malformed, "test #{@description.inspect} never calls `tests` — " \
|
|
@@ -84,6 +112,8 @@ module Hecks
|
|
|
84
112
|
# `vision`/`loads`/`test` calls and builds a `BehaviorsSuite` (ir.rb),
|
|
85
113
|
# refusing to build one missing either `vision` or `loads` (`#build`).
|
|
86
114
|
class BehaviorsBuilder
|
|
115
|
+
# @param name [String] the suite's declared name
|
|
116
|
+
# @param source_path [String] the `.behaviors` file's own path
|
|
87
117
|
def initialize(name, source_path:)
|
|
88
118
|
@name = name
|
|
89
119
|
@source_path = source_path
|
|
@@ -93,21 +123,40 @@ module Hecks
|
|
|
93
123
|
@tests = []
|
|
94
124
|
end
|
|
95
125
|
|
|
126
|
+
# Records the suite's one-line description of what it is examples of.
|
|
127
|
+
#
|
|
128
|
+
# @param text [String] the vision text
|
|
129
|
+
# @return [String] `text`, unchanged
|
|
96
130
|
def vision(text) = @vision = text
|
|
97
131
|
|
|
98
132
|
# Relative to this `.behaviors` file, never to the filesystem's cwd
|
|
99
133
|
# or a same-stem convention — scope is a fact this file declares,
|
|
100
134
|
# not one a runner infers.
|
|
135
|
+
#
|
|
136
|
+
# @param paths [Array<String>] paths to the files this suite's domain boots
|
|
137
|
+
# from, relative to the `.behaviors` file
|
|
138
|
+
# @return [void]
|
|
101
139
|
def loads(*paths)
|
|
102
140
|
@loads = paths.map { |path| File.expand_path(path, @source_dir) }
|
|
103
141
|
end
|
|
104
142
|
|
|
143
|
+
# Builds and records one test case.
|
|
144
|
+
#
|
|
145
|
+
# @param description [String] the test's own description
|
|
146
|
+
# @yield the test's body, evaluated against a `TestCaseBuilder`
|
|
147
|
+
# @return [void]
|
|
148
|
+
# @raise [Malformed] if the test never calls `tests`, has no `expect`, or
|
|
149
|
+
# declares `count:` on a command or `emits:` on a query
|
|
105
150
|
def test(description, &block)
|
|
106
151
|
builder = TestCaseBuilder.new(description)
|
|
107
152
|
builder.instance_eval(&block) if block
|
|
108
153
|
@tests << builder.build
|
|
109
154
|
end
|
|
110
155
|
|
|
156
|
+
# Builds the `BehaviorsSuite` this builder collected.
|
|
157
|
+
#
|
|
158
|
+
# @return [BehaviorsSuite] the built suite
|
|
159
|
+
# @raise [Malformed] if the suite declares no `vision` or no `loads`
|
|
111
160
|
def build
|
|
112
161
|
unless @vision
|
|
113
162
|
raise Malformed, "#{@source_path}: no `vision \"...\"` — say in one line " \
|
|
@@ -122,6 +171,15 @@ module Hecks
|
|
|
122
171
|
tests: @tests, path: @source_path)
|
|
123
172
|
end
|
|
124
173
|
|
|
174
|
+
# Builds a suite in one call: constructs a builder, evaluates `block`
|
|
175
|
+
# against it, and builds the result.
|
|
176
|
+
#
|
|
177
|
+
# @param name [String] the suite's declared name
|
|
178
|
+
# @param source_path [String] the `.behaviors` file's own path
|
|
179
|
+
# @yield the suite's body, evaluated against the new builder
|
|
180
|
+
# @return [BehaviorsSuite] the built suite
|
|
181
|
+
# @raise [Malformed] if the suite declares no `vision` or `loads`, or any of
|
|
182
|
+
# its `test` blocks is malformed
|
|
125
183
|
def self.build(name, source_path:, &block)
|
|
126
184
|
builder = new(name, source_path: source_path)
|
|
127
185
|
builder.instance_eval(&block) if block
|