hecks 0.3.0 → 1.0.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/d1.rb +38 -14
- data/lib/hecks/adapters/driven/governance_authorization.rb +41 -2
- data/lib/hecks/adapters/driven/heki/journal.rb +7 -2
- data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
- data/lib/hecks/adapters/driven/heki/snapshot.rb +31 -4
- data/lib/hecks/adapters/driven/heki.rb +40 -9
- data/lib/hecks/adapters/driven/lambda.rb +16 -2
- data/lib/hecks/adapters/driven/postgres/codec.rb +8 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +45 -6
- data/lib/hecks/adapters/driven/postgres.rb +76 -29
- data/lib/hecks/adapters/driven/postgres_era.adapter +11 -0
- data/lib/hecks/adapters/driven/sqlite/codec.rb +17 -0
- data/lib/hecks/adapters/driven/sqlite/projection.rb +76 -9
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +17 -5
- data/lib/hecks/adapters/driven/sqlite.rb +30 -9
- data/lib/hecks/adapters/driven.rb +19 -1
- data/lib/hecks/behaviors/dsl.rb +29 -0
- data/lib/hecks/behaviors/expectations.rb +62 -2
- data/lib/hecks/bluebook/assembly/contracts.rb +36 -7
- data/lib/hecks/bluebook/assembly/marks.rb +4 -3
- data/lib/hecks/bluebook/assembly.rb +14 -1
- data/lib/hecks/bluebook/behaviour/lifecycle.rb +18 -1
- data/lib/hecks/bluebook/behaviour/process_manager.rb +14 -1
- data/lib/hecks/bluebook/chapter.rb +21 -11
- data/lib/hecks/bluebook/command.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +117 -5
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +21 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +71 -2
- data/lib/hecks/bluebook/dsl/command_builder.rb +91 -3
- data/lib/hecks/bluebook/dsl/entity_builder.rb +129 -4
- data/lib/hecks/bluebook/dsl/policy_builder.rb +18 -3
- data/lib/hecks/bluebook/dsl/port_builder.rb +12 -3
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +109 -10
- data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -0
- data/lib/hecks/bluebook/dsl/word_gate.rb +9 -2
- data/lib/hecks/bluebook/dsl/world_builder.rb +44 -4
- data/lib/hecks/bluebook/expression/canonical_form.rb +71 -3
- data/lib/hecks/bluebook/expression/evaluator.rb +50 -7
- data/lib/hecks/bluebook/expression/projection.json +48 -0
- data/lib/hecks/bluebook/expression/resolver.rb +161 -10
- data/lib/hecks/bluebook/hexagon.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/judge.rb +78 -16
- data/lib/hecks/bluebook/meta_validator/port_judge.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/readings.rb +14 -4
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/shapes.rb +30 -10
- data/lib/hecks/bluebook/meta_validator.rb +103 -13
- data/lib/hecks/bluebook/model_check.rb +132 -5
- data/lib/hecks/bluebook/pattern_subset.rb +66 -2
- data/lib/hecks/bluebook/process_manager.rb +53 -11
- data/lib/hecks/bluebook/value_object.rb +9 -1
- data/lib/hecks/doc/reference.rb +22 -1
- data/lib/hecks/facade/cli_door.rb +6 -3
- data/lib/hecks/facade/json_door.rb +16 -4
- data/lib/hecks/forms/app.rb +47 -6
- data/lib/hecks/forms/command_form_renderer.rb +1 -1
- data/lib/hecks/forms/field_renderer.rb +11 -4
- data/lib/hecks/forms/html.rb +31 -0
- data/lib/hecks/forms/params.rb +30 -1
- data/lib/hecks/forms/port_argument.rb +46 -0
- data/lib/hecks/forms/record_renderer.rb +6 -2
- data/lib/hecks/forms/record_table.rb +6 -1
- data/lib/hecks/framework/bluebook/console_settings.bluebook +19 -19
- data/lib/hecks/framework/bluebook/governance.bluebook +26 -11
- data/lib/hecks/framework/bluebook/identity.bluebook +2 -2
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +212 -18
- data/lib/hecks/fuzzing/properties.rb +52 -6
- data/lib/hecks/fuzzing/replay.rb +51 -18
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +28 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +71 -6
- data/lib/hecks/fuzzing/sequence_generator.rb +5 -4
- data/lib/hecks/fuzzing/value_generator.rb +25 -2
- data/lib/hecks/grammar/evolve.rb +33 -0
- data/lib/hecks/grammar/expression.bluebook +8 -5
- data/lib/hecks/grammar/expression_operators.json +497 -1
- data/lib/hecks/language/bluebook/aggregate.bluebook +12 -12
- data/lib/hecks/language/bluebook/bluebook.bluebook +3 -3
- data/lib/hecks/language/bluebook/command.bluebook +42 -8
- data/lib/hecks/language/bluebook/entity.bluebook +86 -10
- data/lib/hecks/language/bluebook/policy.bluebook +21 -4
- data/lib/hecks/language/bluebook/process_manager.bluebook +135 -18
- data/lib/hecks/language/bluebook/projection.bluebook +6 -6
- data/lib/hecks/language/bluebook/query.bluebook +4 -4
- data/lib/hecks/language/bluebook/shape.bluebook +6 -6
- data/lib/hecks/language/bluebook/syntax.bluebook +12 -11
- data/lib/hecks/language/bluebook/vocabulary.bluebook +22 -7
- data/lib/hecks/language/oidc.json +20 -0
- data/lib/hecks/language/port.bluebook +30 -2
- data/lib/hecks/naming.rb +54 -1
- data/lib/hecks/ports/access_control.port +7 -2
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.port +6 -2
- data/lib/hecks/ports/agent.rb +1 -1
- data/lib/hecks/ports/authentication.port +4 -2
- data/lib/hecks/ports/authentication.rb +1 -1
- data/lib/hecks/ports/authorization.port +5 -2
- data/lib/hecks/ports/authorization.rb +14 -11
- data/lib/hecks/ports/clock.port +3 -2
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.port +3 -2
- data/lib/hecks/ports/extraction.rb +1 -1
- data/lib/hecks/ports/identity_assignment.port +3 -2
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.port +3 -2
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.port +3 -2
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/persistence/append_only.rb +40 -4
- data/lib/hecks/ports/persistence/execution.rb +6 -1
- data/lib/hecks/ports/persistence/plugin.rb +54 -0
- data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_check.rb +41 -8
- data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard.rb +24 -56
- data/lib/hecks/ports/persistence/{lineage.rb → plugins/era/lineage.rb} +31 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/era_store.rb +3 -3
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/head_compiler.rb +59 -10
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/mint_transaction.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/provisioning.rb +29 -1
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/tail_merge.rb +11 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/transform_installer.rb +20 -0
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage.rb +1 -1
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/coverage_check.rb +5 -5
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/era_resolver.rb +5 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/merge_coordinator.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/minter.rb +4 -4
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager.rb +2 -2
- data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era.rb +116 -29
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/approval_digest.rb +1 -1
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_one.rb +14 -5
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_two.rb +31 -6
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/unfed_report.rb +5 -1
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/reattest.rb +3 -3
- data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/differ.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
- data/lib/hecks/ports/persistence.rb +1 -1
- data/lib/hecks/ports/projection.rb +20 -0
- data/lib/hecks/projections/diagrams.rb +230 -1
- data/lib/hecks/projections/shape.rb +15 -2
- data/lib/hecks/projector/cli_projector.rb +10 -1
- data/lib/hecks/projector/exporter.rb +19 -2
- data/lib/hecks/query_ir.rb +19 -0
- data/lib/hecks/query_specification/common/null_policy.rb +13 -1
- data/lib/hecks/query_specification/field_path.rb +20 -2
- data/lib/hecks/query_specification/hop_path.rb +7 -5
- data/lib/hecks/runtime/aggregate_lock.rb +45 -0
- data/lib/hecks/runtime/boot_gates.rb +41 -0
- data/lib/hecks/runtime/caller.rb +19 -3
- data/lib/hecks/runtime/command_interpreter/argument_gate.rb +13 -2
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -0
- data/lib/hecks/runtime/command_interpreter.rb +97 -13
- data/lib/hecks/runtime/command_rules/admissibility.rb +64 -14
- data/lib/hecks/runtime/command_rules/arithmetic.rb +7 -1
- data/lib/hecks/runtime/command_rules/authorization.rb +2 -1
- data/lib/hecks/runtime/command_rules/references.rb +27 -19
- data/lib/hecks/runtime/dependency_planning.rb +14 -0
- data/lib/hecks/runtime/dispatcher.rb +19 -4
- data/lib/hecks/runtime/entity_interpreter.rb +85 -14
- data/lib/hecks/runtime/errors.rb +22 -0
- data/lib/hecks/runtime/identity.rb +30 -2
- data/lib/hecks/runtime/instance.rb +59 -4
- data/lib/hecks/runtime/interpreting.rb +21 -0
- data/lib/hecks/runtime/loader.rb +59 -18
- data/lib/hecks/runtime/query_interpreter.rb +36 -4
- data/lib/hecks/runtime/reaction_invocation.rb +9 -1
- data/lib/hecks/runtime/read_model_interpreter.rb +76 -1
- data/lib/hecks/runtime/refusal_wording.rb +2 -0
- data/lib/hecks/runtime/registry/saga_persistence.rb +75 -3
- data/lib/hecks/runtime/registry/verification.rb +88 -0
- data/lib/hecks/runtime/registry.rb +69 -8
- data/lib/hecks/runtime/saga_interpreter.rb +215 -13
- data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
- data/lib/hecks/runtime/value/admission.rb +19 -1
- data/lib/hecks/runtime/value/coercion.rb +75 -10
- data/lib/hecks/runtime.rb +17 -5
- data/lib/hecks/storehouse.rb +632 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +6 -1
- data/lib/hecks.rb +7 -2
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
- metadata +48 -37
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard/shape_diff.rb +0 -0
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_tamper.rb +0 -0
- /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/field_cache.rb +0 -0
- /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/resumable_backfill.rb +0 -0
- /data/lib/hecks/{runtime → ports/persistence/plugins/era}/storage_shape.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/rule_compiler.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/renderer.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/writer.rb +0 -0
- /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold.rb +0 -0
- /data/lib/hecks/{translation.rb → ports/persistence/plugins/era/translation.rb} +0 -0
|
@@ -8,16 +8,39 @@ module Hecks
|
|
|
8
8
|
# `lib/hecks/language/bluebook/` declares what a bluebook IS —
|
|
9
9
|
# Chapter, Root, Verb, Shape, Ask, Piece, and the rest, split across files
|
|
10
10
|
# by domain concept and merged into one chapter at load time (see
|
|
11
|
-
# GRAMMAR_FILES below)
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
11
|
+
# GRAMMAR_FILES below). This replays a built IR into that domain and turns
|
|
12
|
+
# any refusal into a Malformed, so the meta-domain is what actually judges
|
|
13
|
+
# rather than a description sitting beside the code — for whatever rules
|
|
14
|
+
# it carries. `spec/meta_rules_spec.rb`'s own header names the plan: port
|
|
15
|
+
# the language's rules OUT of builder `raise Malformed` calls and INTO
|
|
16
|
+
# `given`/`invariant` here, where they are declarations any reader of the
|
|
17
|
+
# meta-domain can consume instead of behavior buried in a builder.
|
|
16
18
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
19
|
+
# THIS MIGRATION IS PARTIAL, NOT DONE. As of this writing the meta-domain
|
|
20
|
+
# declares 62 given/invariant/ensures rules (`Hecks::QueryIR.collect_rules`
|
|
21
|
+
# against `grammar_registry.bluebook("Bluebook")` enumerates them) —
|
|
22
|
+
# `spec/meta_rule_reachability_spec.rb` is what proves, per declaration,
|
|
23
|
+
# not per verb, that most of them still lack a spec exercising the
|
|
24
|
+
# refusal at all (see that file's own KNOWN_GAPS for the current count).
|
|
25
|
+
# Meanwhile `lib/hecks/bluebook/dsl/` still carries well over a hundred
|
|
26
|
+
# `raise Malformed` calls of its own — some are genuinely pre-IR
|
|
27
|
+
# construction errors (arity, argument shape) that cannot become
|
|
28
|
+
# meta-domain rules, and some are exactly the semantic kind this file
|
|
29
|
+
# claims to have moved (see `aggregate_builder.rb`'s `seal_*` passes:
|
|
30
|
+
# a mutation into a field the aggregate never declares, a lifecycle guard
|
|
31
|
+
# on an aggregate with no lifecycle). No doc currently inventories which
|
|
32
|
+
# is which, or tracks migrating the latter — that inventory is the actual
|
|
33
|
+
# next step, not a "delete the folder" thought experiment.
|
|
34
|
+
#
|
|
35
|
+
# So: "delete language/bluebook/ and validation stops" is true for the 62
|
|
36
|
+
# rules actually declared here, and false for whatever a builder's own
|
|
37
|
+
# `raise Malformed` still checks — the language does not yet own its own
|
|
38
|
+
# enforcement end to end, and this comment used to claim it already did.
|
|
39
|
+
# The self-hosting mechanism itself is real and is the point worth
|
|
40
|
+
# keeping : a self-description that only describes is indistinguishable
|
|
41
|
+
# from enforcement, and the first version of this file was deleted for
|
|
42
|
+
# exactly that reason. What is not yet real is that self-hosting being
|
|
43
|
+
# the WHOLE of validation.
|
|
21
44
|
#
|
|
22
45
|
# The meta-domain is loaded ONCE and its registry reused ; each bluebook is
|
|
23
46
|
# judged in a fresh in-memory store so no domain can see another's records.
|
|
@@ -131,6 +154,12 @@ module Hecks
|
|
|
131
154
|
# dead code, never actually invoked.
|
|
132
155
|
builder = registry.bluebook_builder(name) { raise "internal: no open builder for #{name}" }
|
|
133
156
|
builder.resolve_pending_chapter_givens!
|
|
157
|
+
# THE ENTITY-SCOPED ANALOGUE, one level down — same reason,
|
|
158
|
+
# same timing: a bare entity-level given left PENDING by any
|
|
159
|
+
# file of this chapter (`EntityBuilder#pending_chapter_entity_
|
|
160
|
+
# given`) must resolve before anything below reads a piece's
|
|
161
|
+
# own `Given` fields too.
|
|
162
|
+
builder.resolve_pending_chapter_entity_givens!
|
|
134
163
|
|
|
135
164
|
# `BluebookBuilder#build` skipped its own whole-chapter battery
|
|
136
165
|
# (hops, projected fields, correlation keys, event shapes,
|
|
@@ -147,7 +176,48 @@ module Hecks
|
|
|
147
176
|
end
|
|
148
177
|
end
|
|
149
178
|
|
|
150
|
-
|
|
179
|
+
# `&& !@forcing_fixpoint` — see `while_forcing_fixpoint` below, whose own
|
|
180
|
+
# window must win even while a growth spec's `while_disabled` is open,
|
|
181
|
+
# for the reason recorded there. Otherwise the SAME stack-restore shape
|
|
182
|
+
# `while_shadow_parsing`/`while_forcing_fixpoint` use, not a bare env
|
|
183
|
+
# toggle any more — it used to be exactly that (`ENV["HECKS_META_
|
|
184
|
+
# VALIDATION"] == "off"`, read directly, with no `previous`/`ensure` of
|
|
185
|
+
# its own), and the gap between "bare toggle" and "stack-restore" was
|
|
186
|
+
# not cosmetic: a test's temporary window could reach code it was never
|
|
187
|
+
# meant to touch. If `grammar_registry`'s ONE-TIME lazy build (below)
|
|
188
|
+
# happened to land inside that window, EVERY language chapter got
|
|
189
|
+
# cached in its raw, never-judged form for the rest of the process —
|
|
190
|
+
# `unmark_scalar`'s String->Integer/Boolean fix (assembly/marks.rb)
|
|
191
|
+
# never ran, so a `Command`'s own `required: true` stayed
|
|
192
|
+
# `required: "true"` forever after, permanently memoized. Found live:
|
|
193
|
+
# an intermittent, parallel_rspec-only ir_golden_spec.rb failure,
|
|
194
|
+
# order-dependent on whether identifier_numeric_coercion_growth_spec.rb's
|
|
195
|
+
# disabled-validation window raced the ONE lazy build in its own worker
|
|
196
|
+
# process — reproduced in isolation by disabling validation before the
|
|
197
|
+
# first `grammar_registry` call. `&& !@forcing_fixpoint` was the first
|
|
198
|
+
# fix and is kept ; converting `@disabled` itself to this shape closes
|
|
199
|
+
# the gap for every OTHER caller of `while_disabled`, not just the one
|
|
200
|
+
# race that was actually observed — nothing outside this file reads
|
|
201
|
+
# `ENV["HECKS_META_VALIDATION"]` any more (confirmed: every one of the
|
|
202
|
+
# dozen growth specs that used to hand-roll `previous = ENV[...] ;
|
|
203
|
+
# ENV[...] = "off" ; ... ; ensure ENV[...] = previous` now calls
|
|
204
|
+
# `while_disabled` instead), so there is no bare global left to race.
|
|
205
|
+
def self.disabled? = @disabled && !@forcing_fixpoint
|
|
206
|
+
|
|
207
|
+
# THE SAME STACK-RESTORE SHAPE `while_shadow_parsing`/`while_forcing_
|
|
208
|
+
# fixpoint` USE. This toggle's real, intended use is a growth spec
|
|
209
|
+
# that boots a scratch bluebook from a tempfile and wants the runtime
|
|
210
|
+
# behaviour without the validation overhead ; that is always a single
|
|
211
|
+
# bounded window around one boot, never a flag meant to survive past
|
|
212
|
+
# it, so the flag itself is scoped in the same `previous`/`ensure`
|
|
213
|
+
# shape rather than a plain assignment a caller could forget to undo.
|
|
214
|
+
def self.while_disabled
|
|
215
|
+
previous = @disabled
|
|
216
|
+
@disabled = true
|
|
217
|
+
yield
|
|
218
|
+
ensure
|
|
219
|
+
@disabled = previous
|
|
220
|
+
end
|
|
151
221
|
|
|
152
222
|
# ADR 0025's own prerequisite (docs/dsl-work-slices.md, S0a): a word
|
|
153
223
|
# a later slice removes from the LIVE grammar must still parse
|
|
@@ -175,6 +245,21 @@ module Hecks
|
|
|
175
245
|
@shadow_parsing = previous
|
|
176
246
|
end
|
|
177
247
|
|
|
248
|
+
# THE SAME STACK-RESTORE SHAPE `while_shadow_parsing` USES, for the
|
|
249
|
+
# same reason: whatever this wraps must never see `disabled?` answer
|
|
250
|
+
# true, however a test elsewhere has the env toggle set at that
|
|
251
|
+
# exact moment. Only `grammar_registry`'s own one-time build (below)
|
|
252
|
+
# wraps itself in this — nothing else needs it, and nothing else
|
|
253
|
+
# should reach for it just to dodge `disabled?` for a domain
|
|
254
|
+
# bluebook, which is precisely the toggle's real, intended use.
|
|
255
|
+
def self.while_forcing_fixpoint
|
|
256
|
+
previous = @forcing_fixpoint
|
|
257
|
+
@forcing_fixpoint = true
|
|
258
|
+
yield
|
|
259
|
+
ensure
|
|
260
|
+
@forcing_fixpoint = previous
|
|
261
|
+
end
|
|
262
|
+
|
|
178
263
|
# The same bluebook judged twice gets the same verdict, and a suite reloads
|
|
179
264
|
# its fixtures constantly — banking alone is ~200 dispatches per build.
|
|
180
265
|
# Keyed on the IR itself, so a CHANGED bluebook is always re-judged.
|
|
@@ -348,9 +433,14 @@ module Hecks
|
|
|
348
433
|
# judged from here on is judged by the language the language itself
|
|
349
434
|
# produced. Outside load_grammar_into on purpose : its ensure clears
|
|
350
435
|
# @bootstrapping, and call() must see bootstrapping? == false to do
|
|
351
|
-
# anything at all.
|
|
352
|
-
|
|
353
|
-
|
|
436
|
+
# anything at all. `while_forcing_fixpoint`-wrapped so a growth
|
|
437
|
+
# spec's own `while_disabled` window can never leave this
|
|
438
|
+
# ONE-TIME build cached in its raw, never-judged form — see
|
|
439
|
+
# `disabled?`'s own comment.
|
|
440
|
+
while_forcing_fixpoint do
|
|
441
|
+
LANGUAGE_CHAPTERS.each { |name| registry.add_bluebook(call(registry.bluebook(name))) }
|
|
442
|
+
load_attached_grammar_into(registry)
|
|
443
|
+
end
|
|
354
444
|
# Stamped LAST, keyed by this registry's own identity rather than
|
|
355
445
|
# a bare boolean — a manual reset (fixpoint_spec.rb's own
|
|
356
446
|
# `@grammar_registry = nil`) makes @grammar_registry not equal
|
|
@@ -46,18 +46,51 @@ module Hecks
|
|
|
46
46
|
# states), so a state nothing ever transitions into or out of no
|
|
47
47
|
# longer exists to be unreachable — the finding this allowlisted
|
|
48
48
|
# cannot occur any more, by construction.
|
|
49
|
-
|
|
49
|
+
#
|
|
50
|
+
# "banking"/NotifyOnClosure, FlagKeyReturn — real, confirmed
|
|
51
|
+
# findings, not bugs to fix. `across "Notifications"` names a
|
|
52
|
+
# domain that does not exist anywhere in this repo — no
|
|
53
|
+
# Notifications bluebook, no hecksagon, nothing to `uses_framework`
|
|
54
|
+
# or `subscribe` to. This is deliberate: `spec/runtime/policy_spec.
|
|
55
|
+
# rb` (a test literally named "records a reaction it cannot
|
|
56
|
+
# deliver rather than swallowing it") and `lib/hecks/runtime/
|
|
57
|
+
# errors.rb`'s own `UnknownVerb` comment both treat "target domain
|
|
58
|
+
# not loaded" as the EXPECTED outcome for it — Notifications is
|
|
59
|
+
# used on purpose to exercise the undelivered-reaction runtime
|
|
60
|
+
# path, not left half-built. There is no real `subscribe` line to
|
|
61
|
+
# add (no event of Notifications' own to name) and no real domain
|
|
62
|
+
# to point `uses_framework` at.
|
|
63
|
+
ALLOWED_FINDINGS = {
|
|
64
|
+
"banking" => [
|
|
65
|
+
[:unacknowledged_relationship, "NotifyOnClosure"],
|
|
66
|
+
[:unknown_target_domain, "NotifyOnClosure"],
|
|
67
|
+
[:unacknowledged_relationship, "FlagKeyReturn"],
|
|
68
|
+
[:unknown_target_domain, "FlagKeyReturn"]
|
|
69
|
+
]
|
|
70
|
+
}.freeze
|
|
50
71
|
|
|
51
72
|
module_function
|
|
52
73
|
|
|
53
|
-
|
|
74
|
+
# `hecksagon:`/`known_domains:` — both optional, both `nil`-safe
|
|
75
|
+
# (every existing caller with no sibling hecksagon, or checking one
|
|
76
|
+
# domain in isolation, behaves exactly as before). `hecksagon` is
|
|
77
|
+
# THIS bluebook's own sibling wiring file, if the caller loaded one
|
|
78
|
+
# (see `emitted_events`'s own comment on why a caller that didn't
|
|
79
|
+
# simply finds none, correctly). `known_domains` is the caller's
|
|
80
|
+
# OWN corpus-wide view — every bluebook/hecksagon name it has
|
|
81
|
+
# booted anywhere, across every domain it has looked at, not just
|
|
82
|
+
# this one — used only to catch a typo'd `across`/`uses_framework`
|
|
83
|
+
# target; see `cross_domain_policy_findings`'s own comment for why
|
|
84
|
+
# this can only ever be a corpus-scoped heuristic, never a general
|
|
85
|
+
# correctness guarantee.
|
|
86
|
+
def call(bluebook, hecksagon: nil, known_domains: nil)
|
|
54
87
|
findings = []
|
|
55
88
|
bluebook.aggregates.each do |aggregate|
|
|
56
89
|
findings.concat(lifecycle_findings(aggregate, aggregate))
|
|
57
90
|
aggregate.entities.each { |entity| findings.concat(lifecycle_findings(aggregate, entity)) }
|
|
58
91
|
end
|
|
59
92
|
bluebook.process_managers.each { |pm| findings.concat(saga_findings(bluebook, pm)) }
|
|
60
|
-
bluebook.policies.each { |policy| findings.concat(policy_findings(bluebook, policy)) }
|
|
93
|
+
bluebook.policies.each { |policy| findings.concat(policy_findings(bluebook, policy, hecksagon, known_domains)) }
|
|
61
94
|
findings
|
|
62
95
|
end
|
|
63
96
|
|
|
@@ -200,6 +233,23 @@ module Hecks
|
|
|
200
233
|
"declares no command at — cross-domain dispatch is out of this " \
|
|
201
234
|
"checker's scope, same as CommandRules#resolve_references")
|
|
202
235
|
end
|
|
236
|
+
|
|
237
|
+
# A `compensates` DECLARED WITH NOWHERE TO EVER FIRE — the exact
|
|
238
|
+
# shape of the real bug this whole feature closes ("the
|
|
239
|
+
# reversal was written and never armed"), caught at build/
|
|
240
|
+
# model-check time instead of discovered in production. No
|
|
241
|
+
# handler anywhere answers REFUSED (`pm.saga?` false) means
|
|
242
|
+
# `SagaInterpreter#unwind` never runs for this process
|
|
243
|
+
# manager at all, so a declared `compensates` is structurally
|
|
244
|
+
# unreachable — not a warning about style, a dead declaration.
|
|
245
|
+
if !pm.saga? && handler.dispatches.any?(&:compensates)
|
|
246
|
+
handler.dispatches.select(&:compensates).each do |dispatch|
|
|
247
|
+
findings << Finding.new(kind: :unarmed_compensation, severity: :error, subject: pm.name,
|
|
248
|
+
message: "#{dispatch.command_name} compensates #{dispatch.compensates.command_name}, " \
|
|
249
|
+
"but no handler anywhere in this saga answers a refusal — the " \
|
|
250
|
+
"compensation is declared and can never fire")
|
|
251
|
+
end
|
|
252
|
+
end
|
|
203
253
|
end
|
|
204
254
|
|
|
205
255
|
if pm.saga? && !reached.include?(pm.saga.from_state)
|
|
@@ -240,8 +290,8 @@ module Hecks
|
|
|
240
290
|
|
|
241
291
|
# ── policies ───────────────────────────────────────────────────────
|
|
242
292
|
|
|
243
|
-
def policy_findings(bluebook, policy)
|
|
244
|
-
return
|
|
293
|
+
def policy_findings(bluebook, policy, hecksagon, known_domains)
|
|
294
|
+
return cross_domain_policy_findings(policy, hecksagon, known_domains) if policy.target_domain
|
|
245
295
|
|
|
246
296
|
emitted = emitted_events(bluebook)
|
|
247
297
|
findings = []
|
|
@@ -276,6 +326,83 @@ module Hecks
|
|
|
276
326
|
findings
|
|
277
327
|
end
|
|
278
328
|
|
|
329
|
+
# ── cross-domain policies (Context Mapping) ───────────────────────
|
|
330
|
+
#
|
|
331
|
+
# `uses_framework "X"` already IS a Shared Kernel relationship — it
|
|
332
|
+
# merges X's own bluebook into THIS registry, no boundary. A cross-
|
|
333
|
+
# domain `policy ... across: "X"` already IS a Customer/Supplier
|
|
334
|
+
# relationship — it dispatches into X over real cross-Lambda RPC in
|
|
335
|
+
# the Rust host (`rust/host/src/lambda_client.rs`). Neither is a new
|
|
336
|
+
# word; this makes the CHOICE between them checked instead of a
|
|
337
|
+
# prose comment nobody enforces (`examples/banking/bluebook/
|
|
338
|
+
# banking.hecksagon`'s own hand-written note explaining why
|
|
339
|
+
# Compliance is reached via `across`, never `uses_framework`).
|
|
340
|
+
#
|
|
341
|
+
# NO NEW KEYWORD ANYWHERE — ADR 0025 principle 1 ("one idea, one
|
|
342
|
+
# spelling") refuses a `relationship:`/`as:` argument that would
|
|
343
|
+
# just restate, as a string, the fact the chosen keyword (
|
|
344
|
+
# `uses_framework` vs `across`) already states completely. The
|
|
345
|
+
# DDD vocabulary (Shared Kernel, Customer/Supplier) lives here, in
|
|
346
|
+
# the finding's own name and this comment, and in prose docs — not
|
|
347
|
+
# in the grammar.
|
|
348
|
+
def cross_domain_policy_findings(policy, hecksagon, known_domains)
|
|
349
|
+
return [] unless hecksagon # no sibling hecksagon loaded — nothing to check a relationship against.
|
|
350
|
+
|
|
351
|
+
target = policy.target_domain
|
|
352
|
+
findings = []
|
|
353
|
+
|
|
354
|
+
if hecksagon.framework_members.include?(target)
|
|
355
|
+
# SHARED KERNEL AND CUSTOMER/SUPPLIER ARE MUTUALLY EXCLUSIVE
|
|
356
|
+
# CLAIMS about the SAME target — `uses_framework` means "X is
|
|
357
|
+
# loaded in-process, right here"; `across` means "X is a
|
|
358
|
+
# separate deployment, reached only by RPC." Declaring both is
|
|
359
|
+
# either a pointless RPC to a domain already local, or a
|
|
360
|
+
# `uses_framework` that isn't really doing what its name says.
|
|
361
|
+
findings << Finding.new(kind: :contradictory_relationship, severity: :error, subject: policy.name,
|
|
362
|
+
message: "across #{target.inspect} dispatches over RPC (Customer/Supplier), " \
|
|
363
|
+
"but this hecksagon also uses_framework #{target.inspect} (Shared " \
|
|
364
|
+
"Kernel) — #{target} is already loaded in-process here, so the two " \
|
|
365
|
+
"relationship declarations contradict each other for the same " \
|
|
366
|
+
"target domain")
|
|
367
|
+
elsif hecksagon.subscriptions.none? { |subscribed| Naming.qualifier(subscribed) == target }
|
|
368
|
+
# THIS IS WHAT FINALLY GIVES `subscribe` REAL TEETH — checked
|
|
369
|
+
# here, at model-check time, still never routed at runtime
|
|
370
|
+
# (nothing dispatches off a `subscribe` line; see hecksagon.md's
|
|
371
|
+
# own "checked, not routed" section). ADR 0025 names `subscribe`
|
|
372
|
+
# by number as failing the corpus-use bar; this is the real use.
|
|
373
|
+
findings << Finding.new(kind: :unacknowledged_relationship, severity: :error, subject: policy.name,
|
|
374
|
+
message: "across #{target.inspect} declares a Customer/Supplier " \
|
|
375
|
+
"relationship, but nothing in this hecksagon records the " \
|
|
376
|
+
"expectation — add subscribe \"#{target}.SomeEvent\" for what " \
|
|
377
|
+
"you expect back from it, or uses_framework #{target.inspect} to " \
|
|
378
|
+
"attach it in-process instead")
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# TYPO DETECTION, DELIBERATELY WEAKER — `known_domains` can only
|
|
382
|
+
# ever be a MONOREPO-SCOPED heuristic: a real external hecks
|
|
383
|
+
# consumer's own domain (this repo's own embryonaut/lifeadelics-
|
|
384
|
+
# shaped case) lives in a genuinely separate repository this
|
|
385
|
+
# corpus scan can never see, so a target this check cannot find
|
|
386
|
+
# is "unknown to THIS corpus," never proof of a typo. Two real,
|
|
387
|
+
# legitimate reasons a target is unresolvable — genuinely
|
|
388
|
+
# undefined by design (the corpus's own "Notifications," used
|
|
389
|
+
# deliberately to exercise the undelivered-reaction runtime path)
|
|
390
|
+
# and real-but-external (a separate repository) — both go in
|
|
391
|
+
# `ALLOWED_FINDINGS`, the same judged-exception mechanism this
|
|
392
|
+
# file already uses for ExternalSettlement, rather than a new
|
|
393
|
+
# keyword invented to declare "this one's fine."
|
|
394
|
+
if known_domains && !known_domains.include?(target)
|
|
395
|
+
findings << Finding.new(kind: :unknown_target_domain, severity: :error, subject: policy.name,
|
|
396
|
+
message: "across #{target.inspect} names a domain nowhere in the corpus " \
|
|
397
|
+
"this check has booted — a typo, or a real domain intentionally " \
|
|
398
|
+
"outside this corpus (undefined by design, or living in a " \
|
|
399
|
+
"separate repository) belongs in ALLOWED_FINDINGS, named and " \
|
|
400
|
+
"explained, not silently assumed correct")
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
findings
|
|
404
|
+
end
|
|
405
|
+
|
|
279
406
|
# ── shared enumeration ────────────────────────────────────────────
|
|
280
407
|
|
|
281
408
|
# A PORT OPERATION EMITS TOO — the primary/driving port an adapter
|
|
@@ -66,6 +66,14 @@ module Hecks
|
|
|
66
66
|
def validate(pattern)
|
|
67
67
|
chars = pattern.to_s.chars
|
|
68
68
|
index = 0
|
|
69
|
+
# A CHARACTER-CLASS INTERIOR IS A DIFFERENT ALPHABET : inside `[...]`,
|
|
70
|
+
# `*`, `+`, `?`, `(`, `?` are literal characters, not quantifiers or
|
|
71
|
+
# group syntax — `[*+]` means "a literal asterisk or plus". `]` is
|
|
72
|
+
# only the class's close when it isn't the first character after `[`
|
|
73
|
+
# or `[^` (where it is itself a literal, per POSIX bracket-expression
|
|
74
|
+
# rules).
|
|
75
|
+
in_class = false
|
|
76
|
+
class_start = nil
|
|
69
77
|
|
|
70
78
|
while index < chars.length
|
|
71
79
|
if chars[index] == "\\"
|
|
@@ -78,14 +86,36 @@ module Hecks
|
|
|
78
86
|
next
|
|
79
87
|
end
|
|
80
88
|
|
|
89
|
+
if in_class
|
|
90
|
+
if chars[index] == "]" && index != class_start
|
|
91
|
+
in_class = false
|
|
92
|
+
index += 1
|
|
93
|
+
next
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
return refuse(:posix_class) if posix_class_at?(chars, index)
|
|
97
|
+
|
|
98
|
+
index += 1
|
|
99
|
+
next
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
if chars[index] == "["
|
|
103
|
+
return refuse(:posix_class) if posix_class_at?(chars, index)
|
|
104
|
+
|
|
105
|
+
in_class = true
|
|
106
|
+
class_start = index + 1
|
|
107
|
+
class_start += 1 if chars[class_start] == "^"
|
|
108
|
+
index += 1
|
|
109
|
+
next
|
|
110
|
+
end
|
|
111
|
+
|
|
81
112
|
if chars[index] == "(" && chars[index + 1] == "?"
|
|
82
113
|
return refuse(:lookahead) if %w[= !].include?(chars[index + 2])
|
|
83
114
|
return refuse(:lookbehind) if chars[index + 2] == "<" && %w[= !].include?(chars[index + 3])
|
|
84
115
|
return refuse(:atomic_group) if chars[index + 2] == ">"
|
|
85
116
|
end
|
|
86
117
|
|
|
87
|
-
return refuse(:
|
|
88
|
-
return refuse(:possessive) if %w[* + ?].include?(chars[index]) && chars[index + 1] == "+"
|
|
118
|
+
return refuse(:possessive) if possessive_at?(chars, index)
|
|
89
119
|
|
|
90
120
|
index += 1
|
|
91
121
|
end
|
|
@@ -115,6 +145,40 @@ module Hecks
|
|
|
115
145
|
cursor += 1 while chars[cursor]&.match?(/[a-zA-Z]/)
|
|
116
146
|
chars[cursor] == ":" && chars[cursor + 1] == "]"
|
|
117
147
|
end
|
|
148
|
+
|
|
149
|
+
# A possessive quantifier is `*+`, `++`, `?+`, or a bounded `{n}`/{n,m}`
|
|
150
|
+
# immediately followed by `+` — only checked OUTSIDE a character class,
|
|
151
|
+
# where `*`, `+`, `?`, `{`, `}` are quantifier syntax rather than
|
|
152
|
+
# literal characters.
|
|
153
|
+
def possessive_at?(chars, index)
|
|
154
|
+
return true if %w[* + ?].include?(chars[index]) && chars[index + 1] == "+"
|
|
155
|
+
return false unless chars[index] == "{"
|
|
156
|
+
|
|
157
|
+
len = bounded_quantifier_length(chars, index)
|
|
158
|
+
!len.nil? && chars[index + len] == "+"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Length of a `{n}` / `{n,}` / `{n,m}` bound starting at `index`, or nil
|
|
162
|
+
# if what's there isn't one.
|
|
163
|
+
def bounded_quantifier_length(chars, index)
|
|
164
|
+
cursor = index + 1
|
|
165
|
+
digit_seen = false
|
|
166
|
+
|
|
167
|
+
while chars[cursor]&.match?(/[0-9]/)
|
|
168
|
+
digit_seen = true
|
|
169
|
+
cursor += 1
|
|
170
|
+
end
|
|
171
|
+
return nil unless digit_seen
|
|
172
|
+
|
|
173
|
+
if chars[cursor] == ","
|
|
174
|
+
cursor += 1
|
|
175
|
+
cursor += 1 while chars[cursor]&.match?(/[0-9]/)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
return nil unless chars[cursor] == "}"
|
|
179
|
+
|
|
180
|
+
cursor - index + 1
|
|
181
|
+
end
|
|
118
182
|
end
|
|
119
183
|
end
|
|
120
184
|
end
|
|
@@ -3,7 +3,16 @@ require_relative "../ir"
|
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Bluebook
|
|
6
|
-
|
|
6
|
+
# `compensates` — a SECOND `DispatchSpec`, shape-identical to this
|
|
7
|
+
# one, naming the command that undoes THIS dispatch specifically
|
|
8
|
+
# (see `ProcessManagerBuilder::HandlerBuilder#dispatch_impl`'s own
|
|
9
|
+
# comment). `nil` for a dispatch with nothing to undo (a pure
|
|
10
|
+
# bookkeeping mark, or one whose own effect is superseded by a later
|
|
11
|
+
# command rather than needing its own compensation). Never nested
|
|
12
|
+
# further — a compensation is not itself compensable; no known
|
|
13
|
+
# corpus need, and ADR 0025's own "a word earns its place by being
|
|
14
|
+
# used" bar would refuse a second level speculatively.
|
|
15
|
+
DispatchSpec = Struct.new(:command_name, :with_spec, :compensates, keyword_init: true) do
|
|
7
16
|
# A Struct already answers to_h; including the mixin puts the
|
|
8
17
|
# DECLARED emission ahead of Struct's own in the ancestry, which
|
|
9
18
|
# is what makes the shape data rather than a method body.
|
|
@@ -11,7 +20,8 @@ module Hecks
|
|
|
11
20
|
|
|
12
21
|
emits_ir(
|
|
13
22
|
command_name: -> { command_name.to_s },
|
|
14
|
-
with_spec: -> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } }
|
|
23
|
+
with_spec: -> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } },
|
|
24
|
+
compensates: one(:compensates)
|
|
15
25
|
)
|
|
16
26
|
end
|
|
17
27
|
|
|
@@ -35,14 +45,31 @@ module Hecks
|
|
|
35
45
|
# word — so here they are two objects, and a procedure either has a saga or
|
|
36
46
|
# does not.
|
|
37
47
|
#
|
|
38
|
-
# `undoes` is the ordered list of commands the compensation sends
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
# `undoes` is the ordered list of commands the compensation sends — a
|
|
49
|
+
# STATIC PREVIEW, declaration order (`Behaviour::ProcessManager#saga`),
|
|
50
|
+
# not one instance's own runtime history. Per-dispatch compensation
|
|
51
|
+
# (`compensates`, on the step it compensates for) moved most of what
|
|
52
|
+
# a saga undoes off this leg's own hand-written body and onto
|
|
53
|
+
# whichever forward dispatch each one undoes — this reads every
|
|
54
|
+
# declared `compensates` across the WHOLE saga first, then whatever
|
|
55
|
+
# this leg's own hand-written body still lists, for compensation
|
|
56
|
+
# that isn't expressible as "undo command X." WHICH of a declared
|
|
57
|
+
# `compensates` actually fires for one instance, and in what order
|
|
58
|
+
# (newest-first, completed-legs-only), is `SagaInterpreter`'s own
|
|
59
|
+
# dynamic `completed_compensations` — a per-instance runtime fact
|
|
60
|
+
# this declaration-only object could never hold.
|
|
61
|
+
#
|
|
62
|
+
# NAMING COLLISION, ONCE FLAGGED, NOW RESOLVED — `command`'s own
|
|
63
|
+
# `corrects event, reverses: true` (docs/implemented/decisions/0036-
|
|
64
|
+
# corrects-is-an-appended-fact-not-a-rewrite.md) already claimed
|
|
65
|
+
# `reverses` for a different meaning: auto-deriving a command's OWN
|
|
66
|
+
# corrective mutation from a past EVENT, not a saga's own
|
|
67
|
+
# compensating leg from a past DISPATCH. This feature keeps
|
|
68
|
+
# `reverses` reserved for `corrects` and uses `compensates` for
|
|
69
|
+
# per-dispatch saga compensation instead — a deliberate choice, not
|
|
70
|
+
# an accidental collision.
|
|
71
|
+
Saga = Struct.new(:trigger, :from_state, :to_state, :compensations, keyword_init: true) do
|
|
72
|
+
def undoes = compensations.map(&:command_name)
|
|
46
73
|
|
|
47
74
|
def to_s = "#{trigger} → #{to_state} (#{undoes.join(', ')})"
|
|
48
75
|
end
|
|
@@ -63,7 +90,22 @@ module Hecks
|
|
|
63
90
|
|
|
64
91
|
emits_ir(
|
|
65
92
|
name: :name,
|
|
66
|
-
|
|
93
|
+
# M11 — `&.`, not `.`: a DSL-built process manager always carries
|
|
94
|
+
# a real `correlates_by` (`ProcessManagerBuilder#build` refuses to
|
|
95
|
+
# mint one without it), but the IR class itself defaults it to
|
|
96
|
+
# `nil` and is what `Assembly::Build`'s `:identity` reader
|
|
97
|
+
# (`value&.to_sym`) round-trips against. A bare `.to_s` mapped
|
|
98
|
+
# that absent case to `""`, indistinguishable on the wire from a
|
|
99
|
+
# real empty name and read back as the wrong, non-nil `:""`
|
|
100
|
+
# instead of `nil` — the same nil-erasure S1 fixed for
|
|
101
|
+
# `render_value`, one field over. `correlates_by` is always a
|
|
102
|
+
# bare Symbol (`SagaInterpreter` hash-looks-up a payload by it),
|
|
103
|
+
# never a `Literal`-encoded polymorphic value, so this stays a
|
|
104
|
+
# local `&.` rather than routing through `Literal.render` — that
|
|
105
|
+
# would wrap a real value in a leading `:` and break both the
|
|
106
|
+
# `:identity` reader's plain `to_sym` and the pinned golden IR
|
|
107
|
+
# fixtures' bare-string spelling (`"reference.value"`).
|
|
108
|
+
correlates_by: -> { correlates_by&.to_s },
|
|
67
109
|
starts_on: :starts_on,
|
|
68
110
|
ends_on: :ends_on,
|
|
69
111
|
states: :states,
|
|
@@ -30,7 +30,15 @@ module Hecks
|
|
|
30
30
|
attributes: many(:attributes),
|
|
31
31
|
invariants: -> { invariants.map { |rule| { description: rule.description, canonical: rule.canonical } } },
|
|
32
32
|
closed_set: :closed_set?,
|
|
33
|
-
|
|
33
|
+
# THE FIELD NAME IS STRINGIFIED, NEVER THE VALUE. A `member` row can
|
|
34
|
+
# hold any of the scalar types an attribute declares — `Integer 84`
|
|
35
|
+
# (`StatementFrequency#retention_months`, statements.bluebook), not
|
|
36
|
+
# only `String` — and `value.to_s` used to erase that on the way
|
|
37
|
+
# out, so `84` and `"84"` (a member some other row might
|
|
38
|
+
# legitimately spell as text) became indistinguishable once they
|
|
39
|
+
# reached `to_h`. The declared name still moves (`field.to_s`) —
|
|
40
|
+
# that half was never a Ruby object with a type to lose.
|
|
41
|
+
members: -> { members.map { |member| member.map { |field, value| [field.to_s, value] } } }
|
|
34
42
|
)
|
|
35
43
|
|
|
36
44
|
class << self
|
data/lib/hecks/doc/reference.rb
CHANGED
|
@@ -257,7 +257,8 @@ module Hecks
|
|
|
257
257
|
"guides" => guide_index(root),
|
|
258
258
|
"reference" => reference_index(root),
|
|
259
259
|
"tools" => tool_table(root),
|
|
260
|
-
"corpus" => corpus_roster(root)
|
|
260
|
+
"corpus" => corpus_roster(root),
|
|
261
|
+
"diagrams" => diagram_showcase(root)
|
|
261
262
|
}
|
|
262
263
|
end
|
|
263
264
|
|
|
@@ -311,6 +312,26 @@ module Hecks
|
|
|
311
312
|
text.length > 140 ? "#{text[0, 137]}..." : text
|
|
312
313
|
end
|
|
313
314
|
|
|
315
|
+
# ONE REAL, COMMITTED FILE, READ FRESH — not re-derived from a boot
|
|
316
|
+
# (this module never requires `hecks/projections/diagrams`, and
|
|
317
|
+
# shouldn't just to draw one example). `docs/generated/diagrams/`
|
|
318
|
+
# is already held to the declaration by `spec/diagrams_spec.rb`'s
|
|
319
|
+
# own drift check; this just quotes its own output, so the two
|
|
320
|
+
# can't independently drift from each other either — a stale
|
|
321
|
+
# Order_lifecycle.mmd fails THAT spec long before this one runs.
|
|
322
|
+
def diagram_showcase(root)
|
|
323
|
+
lifecycle = File.read(File.join(root, "docs/generated/diagrams/pizzas/Order_lifecycle.mmd")).strip
|
|
324
|
+
<<~MARKDOWN.strip
|
|
325
|
+
`bin/project_diagrams` reads a booted domain's own declaration and draws it as Mermaid — nine kinds so far: `<Name>_lifecycle.mmd`, `relationships.mmd`, `dispatch.mmd`, `roles.mmd`, `ports.mmd`, `read_models.mmd`, `<Name>_surface.mmd` (what a command does, and what it writes), `<Name>_saga.mmd`, and `frameworks.mmd`. Nothing hand-drawn — the same reason a domain is data at all. Order's own lifecycle, straight off the bluebook above:
|
|
326
|
+
|
|
327
|
+
```mermaid
|
|
328
|
+
#{lifecycle}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
The full set for every domain in this checkout — `examples/pizzas`, `examples/banking` — lives in [`docs/generated/diagrams/`](docs/generated/diagrams/), held to the declaration by `spec/diagrams_spec.rb` the same drift-refusing way this page is held to its own source.
|
|
332
|
+
MARKDOWN
|
|
333
|
+
end
|
|
334
|
+
|
|
314
335
|
def corpus_roster(root)
|
|
315
336
|
dirs = Dir.glob(File.join(root, "examples/*/")).sort
|
|
316
337
|
lines = dirs.filter_map do |dir|
|
|
@@ -33,10 +33,13 @@ module Hecks
|
|
|
33
33
|
|
|
34
34
|
pairs.each_with_object({}) do |pair, args|
|
|
35
35
|
path, value = split(pair)
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
# key? first, never `||` — full names whichever spelling (the
|
|
37
|
+
# bare path, or its one-argument expansion) actually declares
|
|
38
|
+
# this option, and the lookup below must hold to that same
|
|
39
|
+
# decision rather than re-guessing which one exists.
|
|
40
|
+
full = options.key?(path) ? path : expand(path, options)
|
|
41
|
+
argument = options.key?(full) ? options[full] : raise(Runtime::NotFound, unknown(path, options.keys))
|
|
38
42
|
|
|
39
|
-
full = options.key?(path) ? path : expand(path, options)
|
|
40
43
|
next append(args, full.split("."), cast(value, argument[:type])) if argument[:list]
|
|
41
44
|
|
|
42
45
|
bury(args, full.split("."), cast(value, argument[:type]))
|
|
@@ -81,12 +81,24 @@ module Hecks
|
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
# A URL segment or a JSON body's "command" field, checked against what
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
84
|
+
# a `Handle` can actually dispatch — NOT `klass.commands`, which is
|
|
85
|
+
# `AggregateDoor`'s own door-level list and includes the one creating
|
|
86
|
+
# command too (`aggregate_door.rb`'s `commands` singleton method maps
|
|
87
|
+
# every `ir.commands`, full stop). A `Handle` only ever defines
|
|
88
|
+
# singleton methods for the NON-creating ones
|
|
89
|
+
# (`Handle#define_verb_methods`, `@ir.commands.reject(&:creates?)`) —
|
|
90
|
+
# the creating command lives on the aggregate class itself, dispatched
|
|
91
|
+
# through `.creating_command` above, not through a `Handle` in hand.
|
|
92
|
+
# Accepting a creating-command name here let it past this gate clean,
|
|
93
|
+
# only to blow up as a raw `NoMethodError` the moment a caller tried
|
|
94
|
+
# `handle.public_send(name, **args)`, instead of the 404 this method
|
|
95
|
+
# promises. Filtering `reject(&:creates?)` here, the same filter
|
|
96
|
+
# `Handle` itself applies, is what keeps "accepted here" and
|
|
97
|
+
# "dispatchable there" the same set.
|
|
87
98
|
def validate_command!(klass, name)
|
|
88
99
|
wanted = name.to_s
|
|
89
|
-
|
|
100
|
+
dispatchable = klass.ir.commands.reject(&:creates?).map { |command| "#{Naming.snake(command.hecks_name)}!" }
|
|
101
|
+
return wanted if dispatchable.include?(wanted)
|
|
90
102
|
|
|
91
103
|
raise Runtime::NotFound, "#{klass.ir.hecks_name} declares no command named #{wanted.inspect}"
|
|
92
104
|
end
|