hecks 1.4.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 +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.hecksagon +19 -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/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 +58 -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/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/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 +29 -5
- data/lib/hecks/projector.rb +74 -6
- 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 +11 -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
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
|
|
@@ -40,10 +40,22 @@ module Hecks
|
|
|
40
40
|
|
|
41
41
|
module_function
|
|
42
42
|
|
|
43
|
+
# Asks the model for the next best interview question, as a raw parsed-JSON reply.
|
|
44
|
+
#
|
|
43
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
|
|
|
68
|
+
# Turns a human's sentence into proposed declarations, as a raw parsed-JSON reply.
|
|
69
|
+
#
|
|
56
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
|
|
|
92
|
+
# Asks the model to judge a declared model on taste, as a raw parsed-JSON reply.
|
|
93
|
+
#
|
|
69
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
|
|
|
120
|
+
# Suggests a name for a construct, as a raw parsed-JSON reply.
|
|
121
|
+
#
|
|
85
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"]
|
|
@@ -14,16 +14,26 @@ module Hecks
|
|
|
14
14
|
PORTS = "ports".freeze
|
|
15
15
|
ADAPTERS = "adapters".freeze
|
|
16
16
|
|
|
17
|
+
# @param settings [Hash] accepted but not read by this class
|
|
18
|
+
# @param root [String, nil] accepted but not read by this class
|
|
17
19
|
def initialize(settings: {}, root: nil)
|
|
18
20
|
@settings = settings
|
|
19
21
|
@root = root
|
|
20
22
|
end
|
|
21
23
|
|
|
24
|
+
# Loads the framework's own bundled ports and adapters from `lib/hecks/{ports,adapters}`.
|
|
25
|
+
#
|
|
26
|
+
# @return [void]
|
|
22
27
|
def load_library
|
|
23
28
|
load_each(library(PORTS), %w[*.port])
|
|
24
29
|
load_each(library(ADAPTERS), %w[*/*.adapter */*/*.adapter])
|
|
25
30
|
end
|
|
26
31
|
|
|
32
|
+
# Loads a shared root's own ports and adapters, if one was found.
|
|
33
|
+
#
|
|
34
|
+
# @param root [String, nil] the shared root directory, as `shared_root` resolves it;
|
|
35
|
+
# nil is a no-op
|
|
36
|
+
# @return [void]
|
|
27
37
|
def load_project(root)
|
|
28
38
|
return unless root
|
|
29
39
|
|
|
@@ -39,6 +49,7 @@ module Hecks
|
|
|
39
49
|
# already places every `*.bluebook` ahead of hecksagons and worlds,
|
|
40
50
|
# so the window ends at the last chapter pattern rather than at a
|
|
41
51
|
# hand-written list this would otherwise have to keep in step.
|
|
52
|
+
#
|
|
42
53
|
# `environment:` — one more pair of files, loaded last, not a glob.
|
|
43
54
|
# Recovered, not new — see Runtime::Loader.boot's own comment for
|
|
44
55
|
# the provenance. A caller passing `Hecks.boot(path, environment:
|
|
@@ -58,6 +69,12 @@ module Hecks
|
|
|
58
69
|
# concatenate/override rather than replace), so an overlay can
|
|
59
70
|
# rebind or add settings for anything the base file declared
|
|
60
71
|
# without needing to know what else the base file said.
|
|
72
|
+
#
|
|
73
|
+
# @param directory [String] the domain's bluebook directory to load, as
|
|
74
|
+
# `bluebook_directory` resolves it
|
|
75
|
+
# @param environment [String, nil] the environment overlay to load after the domain's
|
|
76
|
+
# own files (e.g. `"production"`); nil loads no overlay
|
|
77
|
+
# @return [void]
|
|
61
78
|
def load_domain(directory, environment: nil)
|
|
62
79
|
boundary = DOMAIN_ORDER.rindex { |pattern| pattern.end_with?(".bluebook") }
|
|
63
80
|
if boundary
|
|
@@ -79,6 +96,11 @@ module Hecks
|
|
|
79
96
|
# each file, so a folder may hold more than one chapter without a catalog.
|
|
80
97
|
# Sorting makes source order deterministic while the deferred window keeps
|
|
81
98
|
# cross-file references from being judged against a partial chapter.
|
|
99
|
+
#
|
|
100
|
+
# @param directory [String] the directory to glob for bluebook chapter files
|
|
101
|
+
# @param patterns [Array<String>] glob patterns, relative to `directory`, selecting the
|
|
102
|
+
# chapter files to load
|
|
103
|
+
# @return [void]
|
|
82
104
|
def load_bluebooks(directory, patterns = ["*.bluebook"])
|
|
83
105
|
Bluebook::MetaValidator.defer { load_each(directory, patterns) }
|
|
84
106
|
Bluebook::MetaValidator.judge_deferred!(Hecks.current_registry)
|
|
@@ -101,6 +123,10 @@ module Hecks
|
|
|
101
123
|
# "x.hecksagon", "x.bluebook"` in. Bluebook chapters are judged as one
|
|
102
124
|
# deferred group exactly like `load_domain` does, for the identical
|
|
103
125
|
# forward-reference reason (MetaValidator.defer's own header).
|
|
126
|
+
# @param files [Array<String>] file paths to load, relative to `bluebook_directory`
|
|
127
|
+
# @param environment [String, nil] the environment name whose overlay, if present,
|
|
128
|
+
# loads after the selected files
|
|
129
|
+
# @return [void]
|
|
104
130
|
def load_selected(files, environment: nil)
|
|
105
131
|
bluebooks, rest = files.partition { |f| f.end_with?(".bluebook") }
|
|
106
132
|
|
|
@@ -120,6 +146,14 @@ module Hecks
|
|
|
120
146
|
load_each(directory, [File.join("environments", "#{environment}.world")])
|
|
121
147
|
end
|
|
122
148
|
|
|
149
|
+
# Loads every file under `directory` matching any of `patterns`, in pattern order.
|
|
150
|
+
#
|
|
151
|
+
# A no-op if `directory` does not exist.
|
|
152
|
+
#
|
|
153
|
+
# @param directory [String] the directory to search
|
|
154
|
+
# @param patterns [Array<String>] glob patterns, relative to `directory`, of files to
|
|
155
|
+
# load with `Kernel.load`
|
|
156
|
+
# @return [void]
|
|
123
157
|
def load_each(directory, patterns)
|
|
124
158
|
return unless File.directory?(directory)
|
|
125
159
|
|
|
@@ -128,6 +162,13 @@ module Hecks
|
|
|
128
162
|
end
|
|
129
163
|
end
|
|
130
164
|
|
|
165
|
+
# Resolves a domain path to the directory that actually holds its bluebook files.
|
|
166
|
+
#
|
|
167
|
+
# @param path [String] a domain directory, holding either a `bluebook/` subdirectory or
|
|
168
|
+
# its chapter files directly
|
|
169
|
+
# @return [String] the `bluebook/` subdirectory's absolute path, if one exists; `path`'s
|
|
170
|
+
# own absolute path otherwise
|
|
171
|
+
# @raise [Errno::ENOENT] if neither directory exists
|
|
131
172
|
def bluebook_directory(path)
|
|
132
173
|
expanded = File.expand_path(path)
|
|
133
174
|
nested = File.join(expanded, "bluebook")
|
|
@@ -138,6 +179,8 @@ module Hecks
|
|
|
138
179
|
raise Errno::ENOENT, "no such domain directory: #{path}"
|
|
139
180
|
end
|
|
140
181
|
|
|
182
|
+
# Finds the nearest domain directory at or above `from`, or nil if there is not one.
|
|
183
|
+
#
|
|
141
184
|
# **The domain you are standing in**. Walks up from `from` — the way git
|
|
142
185
|
# finds `.git` — and answers the nearest directory a boot would accept,
|
|
143
186
|
# or nil if there is not one above you.
|
|
@@ -159,6 +202,11 @@ module Hecks
|
|
|
159
202
|
# so the registry root comes out the same — but `examples/banking` is the
|
|
160
203
|
# directory a person names, and the one a `.world`'s `dir "data"` reads
|
|
161
204
|
# as relative to.
|
|
205
|
+
#
|
|
206
|
+
# @param from [String] the directory to walk up from; defaults to the process's current
|
|
207
|
+
# working directory
|
|
208
|
+
# @return [String, nil] the resolved domain directory's absolute path, or nil if none is
|
|
209
|
+
# found above `from`
|
|
162
210
|
def domain_root(from = Dir.pwd)
|
|
163
211
|
found = nearest_domain(File.expand_path(from))
|
|
164
212
|
return nil unless found
|
|
@@ -167,6 +215,12 @@ module Hecks
|
|
|
167
215
|
File.basename(found) == "bluebook" && domain?(parent) ? parent : found
|
|
168
216
|
end
|
|
169
217
|
|
|
218
|
+
# Walks up from `current` until it finds a domain directory, or reaches the filesystem
|
|
219
|
+
# root.
|
|
220
|
+
#
|
|
221
|
+
# @param current [String] the absolute directory path to start searching from
|
|
222
|
+
# @return [String, nil] the nearest directory (`current` or an ancestor) that `domain?`
|
|
223
|
+
# accepts, or nil if none is found before the filesystem root
|
|
170
224
|
def nearest_domain(current)
|
|
171
225
|
loop do
|
|
172
226
|
return current if domain?(current)
|
|
@@ -178,11 +232,25 @@ module Hecks
|
|
|
178
232
|
end
|
|
179
233
|
end
|
|
180
234
|
|
|
235
|
+
# Reports whether `directory` is a domain root.
|
|
236
|
+
#
|
|
237
|
+
# @param directory [String] the directory to check
|
|
238
|
+
# @return [Boolean] true if `directory`, or its `bluebook/` subdirectory, holds a
|
|
239
|
+
# `.hecksagon` file
|
|
181
240
|
def domain?(directory)
|
|
182
241
|
!Dir[File.join(directory, "*.hecksagon")].empty? ||
|
|
183
242
|
!Dir[File.join(directory, "bluebook", "*.hecksagon")].empty?
|
|
184
243
|
end
|
|
185
244
|
|
|
245
|
+
# Resolves the shared root a domain's own ports/adapters overlay from.
|
|
246
|
+
#
|
|
247
|
+
# @param given [String, nil] an explicit shared-root override; returned expanded as-is
|
|
248
|
+
# when present
|
|
249
|
+
# @param directory [String] the domain directory to search upward from when `given` is
|
|
250
|
+
# nil
|
|
251
|
+
# @return [String, nil] `given`'s expanded path, or the nearest ancestor of `directory`
|
|
252
|
+
# holding a `ports` or `adapters` folder; nil if none is found before the filesystem
|
|
253
|
+
# root
|
|
186
254
|
def shared_root(given, directory)
|
|
187
255
|
return File.expand_path(given) if given
|
|
188
256
|
|
|
@@ -198,6 +266,11 @@ module Hecks
|
|
|
198
266
|
end
|
|
199
267
|
end
|
|
200
268
|
|
|
269
|
+
# Resolves the framework's own bundled `folder` directory under `lib/hecks`.
|
|
270
|
+
#
|
|
271
|
+
# @param folder [String] `"ports"` or `"adapters"`, the framework's own bundled
|
|
272
|
+
# directory name
|
|
273
|
+
# @return [String] the absolute path to `lib/hecks/<folder>`
|
|
201
274
|
def library(folder)
|
|
202
275
|
File.expand_path("../../#{folder}", __dir__)
|
|
203
276
|
end
|
|
@@ -5,11 +5,11 @@ require_relative "../../ports/authentication"
|
|
|
5
5
|
module Hecks
|
|
6
6
|
module Adapters
|
|
7
7
|
# **Google's own OIDC handshake** — the `authentication` port's one real
|
|
8
|
-
# implementation today
|
|
9
|
-
# (an embryonaut_console `google_auth.rb`
|
|
10
|
-
# any hecks-based app gets Google sign-in for free
|
|
8
|
+
# implementation today. Consolidates what was hand-rolled per-app
|
|
9
|
+
# (an embryonaut_console `google_auth.rb` did this on its own) into
|
|
10
|
+
# one adapter, so any hecks-based app gets Google sign-in for free, the
|
|
11
11
|
# same "one adapter, reusable everywhere" value every other adapter
|
|
12
|
-
# in this directory already has
|
|
12
|
+
# in this directory already has.
|
|
13
13
|
#
|
|
14
14
|
# `oauth2` does only the authorization-code exchange (no Rack
|
|
15
15
|
# middleware, no Omniauth strategy indirection) ; `google-id-token`
|
|
@@ -34,6 +34,10 @@ module Hecks
|
|
|
34
34
|
|
|
35
35
|
module_function
|
|
36
36
|
|
|
37
|
+
# Builds the OAuth2 client for Google's token endpoints.
|
|
38
|
+
#
|
|
39
|
+
# @return [OAuth2::Client] a client configured for Google's OAuth2/OIDC endpoints
|
|
40
|
+
# @raise [KeyError] if `GOOGLE_CLIENT_ID` or `GOOGLE_CLIENT_SECRET` is unset
|
|
37
41
|
def client
|
|
38
42
|
require "oauth2"
|
|
39
43
|
OAuth2::Client.new(
|
|
@@ -47,6 +51,11 @@ module Hecks
|
|
|
47
51
|
# The URL to send a browser to, carrying a fresh CSRF `state` the
|
|
48
52
|
# caller is responsible for stashing (a session, typically) and
|
|
49
53
|
# checking again in `verify`.
|
|
54
|
+
#
|
|
55
|
+
# @return [Array(String, String)] the URL to send the browser to, and the fresh CSRF
|
|
56
|
+
# `state` embedded in it
|
|
57
|
+
# @raise [KeyError] if `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` or
|
|
58
|
+
# `GOOGLE_REDIRECT_URI` is unset
|
|
50
59
|
def authorization_url
|
|
51
60
|
state = SecureRandom.hex(24)
|
|
52
61
|
url = client.auth_code.authorize_url(
|
|
@@ -67,6 +76,18 @@ module Hecks
|
|
|
67
76
|
# the raw token, never anything a caller would need to re-verify
|
|
68
77
|
# itself. `email_verified` rides along because a caller granting
|
|
69
78
|
# access off this email needs to know Google actually checked it.
|
|
79
|
+
#
|
|
80
|
+
# @param code [String] the authorization code the provider sent back
|
|
81
|
+
# @param state [String, nil] the state parameter the provider returned; nil is refused
|
|
82
|
+
# @param expected_state [String, nil] the state `authorization_url` handed out before
|
|
83
|
+
# the redirect; nil is refused
|
|
84
|
+
# @return [Hash{Symbol => Object}] `issuer:` and `subject:` (String), `email:` (String,
|
|
85
|
+
# or nil if the token carries none), `email_verified:` (Boolean)
|
|
86
|
+
# @raise [Ports::Authentication::ValidationError] if either state is nil or the two
|
|
87
|
+
# differ, the code exchange fails, the response has no ID token, or the ID token does
|
|
88
|
+
# not verify
|
|
89
|
+
# @raise [KeyError] if `GOOGLE_CLIENT_ID` or `GOOGLE_REDIRECT_URI` is unset, or a
|
|
90
|
+
# verified token lacks an `iss` or `sub` claim
|
|
70
91
|
def verify(code:, state:, expected_state:)
|
|
71
92
|
# **Both gems, before anything else** — not staggered further down
|
|
72
93
|
# this method: the rescue clause below names GoogleIDToken
|
|
@@ -12,12 +12,12 @@ module Hecks
|
|
|
12
12
|
# dependency on when Governance's bluebook loads relative to this
|
|
13
13
|
# adapter, only that it has by the time `holds_role?` is called.
|
|
14
14
|
#
|
|
15
|
-
# An active assignment, not merely a
|
|
16
|
-
# answers with every assignment an actor has ever held
|
|
17
|
-
# (`AssignmentsForActor`'s own description
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
15
|
+
# An active assignment, not merely a past one — `RoleAssignment`
|
|
16
|
+
# answers with every assignment an actor has ever held, current or
|
|
17
|
+
# past (`AssignmentsForActor`'s own description covers both), and
|
|
18
|
+
# leaves `ends_at` for the caller to read, the same deferral
|
|
19
|
+
# `Governance::RoleTransition.Allowed` makes for the same reason.
|
|
20
|
+
# This is that caller.
|
|
21
21
|
module GovernanceAuthorization
|
|
22
22
|
module_function
|
|
23
23
|
|
|
@@ -29,6 +29,18 @@ module Hecks
|
|
|
29
29
|
# resolved from `Ports::Clock.now`, called by the caller at the
|
|
30
30
|
# door, never by this adapter — see `Ports::Clock`'s own header
|
|
31
31
|
# for why the dispatch path must not consult the clock itself.
|
|
32
|
+
#
|
|
33
|
+
# @param registry [Runtime::Registry] the booted registry, to resolve the authorization
|
|
34
|
+
# provider's verb
|
|
35
|
+
# @param actor_id [String] the actor whose grants are checked, compared as a String
|
|
36
|
+
# @param role [String, Symbol] the role name to look for, compared as a String
|
|
37
|
+
# @param as_of [Integer, nil] Unix epoch seconds; a grant whose `starts_at` is later, or
|
|
38
|
+
# does not parse as a time, does not count. nil skips the `starts_at` check
|
|
39
|
+
# @param scope [String, nil] the scope the caller acts in; nil skips the scope check
|
|
40
|
+
# @return [Boolean] true if at least one live grant of `role` to `actor_id` passes the
|
|
41
|
+
# `as_of` and `scope` checks
|
|
42
|
+
# @raise [Runtime::WiringError] if the loaded chapters providing `"authorization"` are
|
|
43
|
+
# not exactly one (see `provided_verb`)
|
|
32
44
|
def holds_role?(registry, actor_id:, role:, as_of: nil, scope: nil)
|
|
33
45
|
rows = Runtime::Dispatcher.new(registry).query(
|
|
34
46
|
provided_verb(registry, :assignments),
|
|
@@ -48,6 +60,11 @@ module Hecks
|
|
|
48
60
|
# in gets the pre-scope behavior: any live assignment for the role
|
|
49
61
|
# authorizes, everywhere. A caller that does state one only
|
|
50
62
|
# authorizes against an assignment granted for that scope.
|
|
63
|
+
#
|
|
64
|
+
# @param row [Hash{Symbol => Object}] one `RoleAssignment` row as
|
|
65
|
+
# `Runtime::Dispatcher#query` returns it; `:scope` holds a `{value: String}` Hash
|
|
66
|
+
# @param scope [String, nil] the scope to check against; nil accepts any scope
|
|
67
|
+
# @return [Boolean] true if `scope` is nil, or the row's own scope matches it
|
|
51
68
|
def in_scope?(row, scope)
|
|
52
69
|
scope.nil? || row[:scope][:value] == scope.to_s
|
|
53
70
|
end
|
|
@@ -59,6 +76,12 @@ module Hecks
|
|
|
59
76
|
# Fails closed : a `starts_at` that does not parse is treated as
|
|
60
77
|
# not-yet-started rather than silently ignored, the same direction
|
|
61
78
|
# every other check in this method already fails.
|
|
79
|
+
#
|
|
80
|
+
# @param row [Hash{Symbol => Object}] one `RoleAssignment` row as
|
|
81
|
+
# `Runtime::Dispatcher#query` returns it; `:starts_at` holds a `{value: String}` Hash
|
|
82
|
+
# @param as_of [Integer, nil] Unix epoch seconds to compare against; nil accepts any row
|
|
83
|
+
# @return [Boolean] true if `as_of` is nil, or the row's `starts_at` parses and is at or
|
|
84
|
+
# before `as_of`; false if `starts_at` does not parse as a time
|
|
62
85
|
def started?(row, as_of)
|
|
63
86
|
return true if as_of.nil?
|
|
64
87
|
|
|
@@ -67,11 +90,23 @@ module Hecks
|
|
|
67
90
|
false
|
|
68
91
|
end
|
|
69
92
|
|
|
93
|
+
# Answers whether one role may act as another.
|
|
94
|
+
#
|
|
70
95
|
# **The other half** — may role X act as role Y. `RoleTransition.Allowed`
|
|
71
96
|
# is identified by the exact pair, so at most one row ever comes
|
|
72
97
|
# back ; still read as `.any?` rather than trusting that structurally,
|
|
73
98
|
# the same defensiveness `holds_role?` already has to have anyway
|
|
74
99
|
# since `AssignmentsForActor` can return several.
|
|
100
|
+
#
|
|
101
|
+
# @param registry [Runtime::Registry] the booted registry, to resolve the authorization
|
|
102
|
+
# provider's verb
|
|
103
|
+
# @param from_role [String, Symbol] the role the caller holds, compared as a String
|
|
104
|
+
# @param to_role [String, Symbol] the role the caller wants to act as, compared as a
|
|
105
|
+
# String
|
|
106
|
+
# @return [Boolean] true if a live (not ended) allowance lets `from_role` act as
|
|
107
|
+
# `to_role`
|
|
108
|
+
# @raise [Runtime::WiringError] if the loaded chapters providing `"authorization"` are
|
|
109
|
+
# not exactly one (see `provided_verb`)
|
|
75
110
|
def authorized_as?(registry, from_role:, to_role:)
|
|
76
111
|
rows = Runtime::Dispatcher.new(registry).query(
|
|
77
112
|
provided_verb(registry, :transitions),
|
|
@@ -87,6 +122,14 @@ module Hecks
|
|
|
87
122
|
# against a caller-supplied guess. `nil` for no live assignment at
|
|
88
123
|
# all — the caller's own fallback (an aggregate's own role field,
|
|
89
124
|
# a default) is domain-specific and does not belong here.
|
|
125
|
+
#
|
|
126
|
+
# @param registry [Runtime::Registry] the booted registry, to resolve the authorization
|
|
127
|
+
# provider's verb
|
|
128
|
+
# @param actor_id [String] the actor to look up, compared as a String
|
|
129
|
+
# @return [String, nil] the role name of the actor's first live (not ended) grant, or
|
|
130
|
+
# nil if it has none
|
|
131
|
+
# @raise [Runtime::WiringError] if the loaded chapters providing `"authorization"` are
|
|
132
|
+
# not exactly one (see `provided_verb`)
|
|
90
133
|
def live_role_for(registry, actor_id:)
|
|
91
134
|
rows = Runtime::Dispatcher.new(registry).query(
|
|
92
135
|
provided_verb(registry, :assignments),
|
|
@@ -102,6 +145,14 @@ module Hecks
|
|
|
102
145
|
# loaded chapter declares it (Governance's, in every boot today).
|
|
103
146
|
# Exactly one provider, the same "the runtime will not choose for
|
|
104
147
|
# you" rule `Ports::Authorization.adapter` applies to adapters.
|
|
148
|
+
#
|
|
149
|
+
# @param registry [Runtime::Registry] the booted registry to search for the
|
|
150
|
+
# `"authorization"` provider
|
|
151
|
+
# @param key [Symbol] which declared verb to read — `:assignments`, `:grant`, or
|
|
152
|
+
# `:transitions`
|
|
153
|
+
# @return [String] the fully-qualified verb the loaded chapter provides for `key`
|
|
154
|
+
# @raise [Runtime::WiringError] if the loaded chapters providing `"authorization"` are
|
|
155
|
+
# not exactly one
|
|
105
156
|
def provided_verb(registry, key)
|
|
106
157
|
providers = registry.authorization_providers
|
|
107
158
|
unless providers.size == 1
|
|
@@ -6,6 +6,11 @@ module Hecks
|
|
|
6
6
|
# The append-only journal beside the snapshot: one JSON line per
|
|
7
7
|
# entry, fsynced on append, replayed over the snapshot on read.
|
|
8
8
|
module Journal
|
|
9
|
+
# Reads the whole journal back in append order, for `AppendOnly#recover!` to replay.
|
|
10
|
+
#
|
|
11
|
+
# @return [Array<Ports::Persistence::Entry>] every journalled entry, state decoded
|
|
12
|
+
# through the state codec; `[]` when the journal file does not exist or is empty
|
|
13
|
+
# @raise [Malformed] if a journal line is not valid JSON
|
|
9
14
|
def entries
|
|
10
15
|
return [] unless File.exist?(@journal_path)
|
|
11
16
|
|
|
@@ -46,6 +51,9 @@ module Hecks
|
|
|
46
51
|
# once. A crash before `write` completes leaves the journal
|
|
47
52
|
# fully intact and the prior snapshot untouched, exactly today's
|
|
48
53
|
# existing crash-recovery guarantee.
|
|
54
|
+
#
|
|
55
|
+
# @return [void]
|
|
56
|
+
# @raise [Malformed] if the snapshot or journal file is corrupt
|
|
49
57
|
def compact!
|
|
50
58
|
with_lock do
|
|
51
59
|
current = replay_journal(read_snapshot)
|
|
@@ -4,13 +4,19 @@ require_relative "journal"
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Adapters
|
|
6
6
|
class Heki
|
|
7
|
-
# The optional saga-persistence capability (§2)
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
7
|
+
# The optional saga-persistence capability (§2)'s Heki-specific store — a sibling
|
|
8
|
+
# snapshot+journal file pair, keyed by (domain, process manager, correlation).
|
|
9
|
+
#
|
|
10
|
+
# ## Why a sibling file pair, not a table
|
|
11
|
+
#
|
|
12
|
+
# Built the exact same way an aggregate's own persistence already
|
|
13
|
+
# is: `Snapshot`/`Journal` (heki/snapshot.rb, heki/journal.rb)
|
|
14
|
+
# operate generically on `@path`/`@journal_path`/`@entry_mirrors`
|
|
15
|
+
# and never touch `@aggregate`, so this reuses them unchanged
|
|
16
|
+
# rather than re-deriving the same binary framing and
|
|
17
|
+
# crash-recovery replay.
|
|
18
|
+
#
|
|
19
|
+
# ## Where the file lives
|
|
14
20
|
#
|
|
15
21
|
# Reserved file name (`hecks_saga_instances.heki`, matching the
|
|
16
22
|
# `hecks_`-prefix convention every other new saga table in this
|
|
@@ -24,11 +30,15 @@ module Hecks
|
|
|
24
30
|
# Postgres's own `hecks_saga_instances` keeps an explicit `domain`
|
|
25
31
|
# column under schema isolation (§3).
|
|
26
32
|
#
|
|
33
|
+
# ## Record shape
|
|
34
|
+
#
|
|
27
35
|
# One flat records hash, keyed by a composite string (Heki's own
|
|
28
36
|
# snapshot format is id-keyed, not tuple-keyed) — never exposed
|
|
29
37
|
# outside this class; `each_saga` yields the five real fields a
|
|
30
38
|
# caller actually wants, not the internal key shape.
|
|
31
39
|
#
|
|
40
|
+
# ## Locking
|
|
41
|
+
#
|
|
32
42
|
# Locked the same way an aggregate's own store is: `with_lock`
|
|
33
43
|
# (`Snapshot`, shared) serializes each save/delete's read-modify-
|
|
34
44
|
# write against `@path`'s own lock file — a saga gets exactly the
|
|
@@ -38,12 +48,27 @@ module Hecks
|
|
|
38
48
|
include Snapshot
|
|
39
49
|
include Journal
|
|
40
50
|
|
|
51
|
+
# @param dir [String] the directory to hold the saga snapshot+journal file pair,
|
|
52
|
+
# shared with the aggregate `.heki` files that already live there
|
|
41
53
|
def initialize(dir)
|
|
42
54
|
@path = File.join(dir, "hecks_saga_instances.heki")
|
|
43
55
|
@journal_path = "#{@path}.journal"
|
|
44
56
|
@entry_mirrors = nil
|
|
45
57
|
end
|
|
46
58
|
|
|
59
|
+
# Upserts one saga instance's checkpoint, keyed by domain, process manager and
|
|
60
|
+
# correlation, under the file lock.
|
|
61
|
+
#
|
|
62
|
+
# @param domain [String] the owning domain, carried in the record and filtered on
|
|
63
|
+
# read
|
|
64
|
+
# @param process_manager [String] the process manager's name
|
|
65
|
+
# @param correlation [String] the instance's correlation value
|
|
66
|
+
# @param state [String] the saga's current state name
|
|
67
|
+
# @param memory [Hash] the saga's working memory to persist
|
|
68
|
+
# @param completed_compensations [Array] the ledger of completed compensable legs;
|
|
69
|
+
# `[]` when none
|
|
70
|
+
# @return [Hash{String => Hash}] the store's full internal records Hash after the
|
|
71
|
+
# write; callers ignore it
|
|
47
72
|
def save_saga(domain, process_manager, correlation, state, memory, completed_compensations = [])
|
|
48
73
|
key = key_for(domain, process_manager, correlation)
|
|
49
74
|
record = { "domain" => domain, "process_manager" => process_manager,
|
|
@@ -59,6 +84,14 @@ module Hecks
|
|
|
59
84
|
end
|
|
60
85
|
end
|
|
61
86
|
|
|
87
|
+
# Removes a finished saga instance's checkpoint, if present, under the file lock; a
|
|
88
|
+
# missing one is not an error.
|
|
89
|
+
#
|
|
90
|
+
# @param domain [String] the owning domain
|
|
91
|
+
# @param process_manager [String] the process manager's name
|
|
92
|
+
# @param correlation [String] the instance's correlation value
|
|
93
|
+
# @return [Hash{String => Hash}] the store's full internal records Hash after the
|
|
94
|
+
# delete; callers ignore it
|
|
62
95
|
def delete_saga(domain, process_manager, correlation)
|
|
63
96
|
key = key_for(domain, process_manager, correlation)
|
|
64
97
|
|
|
@@ -71,6 +104,19 @@ module Hecks
|
|
|
71
104
|
end
|
|
72
105
|
end
|
|
73
106
|
|
|
107
|
+
# Yields every checkpointed saga instance of `domain`, for `Registry
|
|
108
|
+
# #rehydrate_sagas!` to restore at boot.
|
|
109
|
+
#
|
|
110
|
+
# @param domain [String] the owning domain to filter records to
|
|
111
|
+
# @yieldparam process_manager [String] the process manager's name
|
|
112
|
+
# @yieldparam correlation [String] the instance's correlation value
|
|
113
|
+
# @yieldparam state [String] the saga's state name
|
|
114
|
+
# @yieldparam memory [Hash{Symbol => Object}] the saga's memory, Symbol keys at every
|
|
115
|
+
# depth
|
|
116
|
+
# @yieldparam completed_compensations [Array] the completed-compensation ledger, `[]`
|
|
117
|
+
# when the record carries none
|
|
118
|
+
# @return [Enumerator, Hash{String => Hash}] an enumerator over the same five values
|
|
119
|
+
# when no block is given; otherwise the store's full internal records Hash
|
|
74
120
|
def each_saga(domain)
|
|
75
121
|
return enum_for(:each_saga, domain) unless block_given?
|
|
76
122
|
|
|
@@ -23,7 +23,7 @@ module Hecks
|
|
|
23
23
|
raise Malformed, "#{@path}: json error: #{e.message}"
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
# Temp-file-plus-rename, fsynced
|
|
26
|
+
# Temp-file-plus-rename, fsynced ahead of the `File.rename` call: a reader can
|
|
27
27
|
# only ever see the last complete snapshot or the one before it,
|
|
28
28
|
# never a truncated or partial one — `File.binwrite`'s old
|
|
29
29
|
# truncate-then-write left a window, proportional to the whole
|