hecks 2.7.0 → 2.8.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.
Files changed (150) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +13 -0
  3. data/lib/hecks/adapters/driven/memory.rb +1 -1
  4. data/lib/hecks/adapters/driven/postgres.rb +1 -1
  5. data/lib/hecks/adapters/driven/prism.rb +1 -1
  6. data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
  7. data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
  8. data/lib/hecks/behaviors/expectations.rb +2 -2
  9. data/lib/hecks/bluebook/assembly/contract.rb +1 -1
  10. data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
  11. data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
  12. data/lib/hecks/bluebook/assembly.rb +1 -1
  13. data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
  14. data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
  15. data/lib/hecks/bluebook/capabilities.rb +2 -2
  16. data/lib/hecks/bluebook/chapter.rb +1 -1
  17. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
  18. data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
  19. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
  20. data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
  21. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
  22. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
  23. data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
  24. data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
  25. data/lib/hecks/bluebook/dsl/world_builder.rb +5 -0
  26. data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
  27. data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
  28. data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
  29. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
  30. data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
  31. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
  32. data/lib/hecks/bluebook/meta_validator.rb +1 -1
  33. data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
  34. data/lib/hecks/bluebook/model_check.rb +12 -13
  35. data/lib/hecks/cache_dir.rb +103 -0
  36. data/lib/hecks/cli/document.rb +104 -0
  37. data/lib/hecks/cli/ir.rb +47 -0
  38. data/lib/hecks/cli/mcp.rb +32 -0
  39. data/lib/hecks/cli/mcp_door.rb +461 -0
  40. data/lib/hecks/cli/model_check.rb +270 -0
  41. data/lib/hecks/cli/project_cli.rb +123 -0
  42. data/lib/hecks/cli/project_diagrams.rb +43 -0
  43. data/lib/hecks/cli/run.rb +219 -0
  44. data/lib/hecks/cli/smoke_test.rb +59 -0
  45. data/lib/hecks/cli/stores.rb +54 -0
  46. data/lib/hecks/cli.rb +166 -0
  47. data/lib/hecks/facade/handle.rb +2 -2
  48. data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
  49. data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
  50. data/lib/hecks/mcp_door_scope.rb +176 -0
  51. data/lib/hecks/ports/persistence/append_only.rb +1 -1
  52. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +1 -1
  53. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
  54. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
  55. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
  56. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
  57. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
  58. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +12 -6
  59. data/lib/hecks/ports/persistence/state_codec.rb +1 -1
  60. data/lib/hecks/projections/deploy/fargate.rb +1 -1
  61. data/lib/hecks/projections/deploy/lambda.rb +3 -3
  62. data/lib/hecks/projections/reference.rb +7 -1
  63. data/lib/hecks/projector/exporter.rb +4 -4
  64. data/lib/hecks/projector/target.rb +1 -1
  65. data/lib/hecks/release/runner/ci_publisher.rb +96 -0
  66. data/lib/hecks/release/runner/commands.rb +68 -0
  67. data/lib/hecks/release/runner/console.rb +52 -0
  68. data/lib/hecks/release/runner/gem_publisher.rb +44 -0
  69. data/lib/hecks/release/runner/git.rb +48 -0
  70. data/lib/hecks/release/runner/npm_publisher.rb +89 -0
  71. data/lib/hecks/release/runner/preflight.rb +119 -0
  72. data/lib/hecks/release/runner/published.rb +55 -0
  73. data/lib/hecks/release/runner/tagger.rb +87 -0
  74. data/lib/hecks/release/runner.rb +244 -0
  75. data/lib/hecks/router/namespace_installer.rb +1 -1
  76. data/lib/hecks/runtime/command_interpreter.rb +3 -3
  77. data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
  78. data/lib/hecks/runtime/entity_element.rb +1 -1
  79. data/lib/hecks/runtime/entity_interpreter.rb +1 -1
  80. data/lib/hecks/runtime/invocation.rb +1 -1
  81. data/lib/hecks/runtime/loader.rb +1 -1
  82. data/lib/hecks/runtime/outbox.rb +1 -1
  83. data/lib/hecks/runtime/reaction_invocation.rb +3 -3
  84. data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
  85. data/lib/hecks/runtime/reference_hop.rb +4 -4
  86. data/lib/hecks/runtime/registry/verification.rb +11 -11
  87. data/lib/hecks/runtime/registry.rb +8 -4
  88. data/lib/hecks/runtime/saga_interpreter.rb +1 -1
  89. data/lib/hecks/runtime/value/coercion.rb +3 -3
  90. data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
  91. data/lib/hecks/storehouse.rb +18 -12
  92. data/lib/hecks/version.rb +1 -1
  93. data/lib/hecks/vocabulary.rb +5 -0
  94. data/lib/hecks.rb +6 -1
  95. metadata +28 -58
  96. data/lib/hecks/bench/cli.rb +0 -74
  97. data/lib/hecks/bench/environment.rb +0 -100
  98. data/lib/hecks/bench/postgres_probe.rb +0 -41
  99. data/lib/hecks/bench/report.rb +0 -116
  100. data/lib/hecks/bench/ruby_runner.rb +0 -151
  101. data/lib/hecks/bench/run.rb +0 -55
  102. data/lib/hecks/bench/rust_runner.rb +0 -165
  103. data/lib/hecks/bench/stats.rb +0 -75
  104. data/lib/hecks/bench/suite.rb +0 -172
  105. data/lib/hecks/bench/workload.rb +0 -137
  106. data/lib/hecks/bench.rb +0 -28
  107. data/lib/hecks/codemod.rb +0 -443
  108. data/lib/hecks/corpus.rb +0 -479
  109. data/lib/hecks/doc/reference.rb +0 -585
  110. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  111. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  112. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  113. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  114. data/lib/hecks/fuzzing/differential.rb +0 -192
  115. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  116. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  117. data/lib/hecks/fuzzing/form_census.rb +0 -285
  118. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  119. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  120. data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
  121. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  122. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  123. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  124. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  125. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  126. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  127. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  128. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  129. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  130. data/lib/hecks/fuzzing/properties.rb +0 -267
  131. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  132. data/lib/hecks/fuzzing/replay.rb +0 -930
  133. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  134. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  135. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  136. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  137. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  138. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  139. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  140. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  141. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  142. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  143. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  144. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  145. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  146. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  147. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  148. data/lib/hecks/fuzzing.rb +0 -23
  149. data/lib/hecks/grammar/evolve.rb +0 -524
  150. data/lib/hecks/query_ir.rb +0 -482
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a91dfe0fb86ceb9bb96ecc6b08da680213fed526f49f922f1a2f66d9cb89c80e
4
- data.tar.gz: 22fc90a2e361f45e27796248c22391997281e62fbfeec01b25ba35378e6e5681
3
+ metadata.gz: 66f85d62ad01467ff2ae17c6d3be63c706bea69a9e6f7b20839d9f5fc22234a5
4
+ data.tar.gz: 45413f0bd639bfe548ca9f8f7970d5136ed6aa30a25765870ca0212806af3c90
5
5
  SHA512:
6
- metadata.gz: 824fd1d23971f6dacfd3c3ea18a35f5d0d61e7f07e2056b789d7be7d105a7bc47d13a61e0391d7aeb46d20bcfe8400e307bf05728ed10f9e4c69b46ac7d0df45
7
- data.tar.gz: 3f6e4679c8187f212f913de1d17944c3bac2ed0e4385bcff3bcd259e1b6ccb49d6c8270c7f84885fcc3f308e9ca7def653341050c649d1a048b8a91228ee65e7
6
+ metadata.gz: b817baff1d4e990f1161080d23f9f10fadd2e1eb8a6f76b1ed551fe021992311f10d64f0eef63218e515905fa2757d879a607abc4b40ecfeb4d0d45a6011a8a6
7
+ data.tar.gz: '0819f496494a76afe81697f8a83301c07acef480b218501748616f1537f0c5d3c0148d67271dea0fd4eea6b6bcb9bbb278331a203883b3c169c08cd59f384451'
data/exe/hecks ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # The `hecks` command the gem installs: run, read, check and serve a domain you
4
+ # supply. `hecks` with no arguments lists the subcommands; `hecks <command> --help`
5
+ # prints one's usage. The router is `Hecks::CLI` (lib/hecks/cli.rb).
6
+ #
7
+ # The gem's own `lib` goes first on the load path, as it does for the `bin/`
8
+ # scripts, so this file also runs straight from a checkout.
9
+
10
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
11
+ require "hecks/cli"
12
+
13
+ exit Hecks::CLI.start(ARGV)
@@ -217,7 +217,7 @@ module Hecks
217
217
  #
218
218
  # @param id [Integer] the row id `outbox_enqueue` assigned
219
219
  # @return [Boolean] true when the row was pending and is now claimed; false when it is
220
- # unknown or no longer pending
220
+ # unknown or not pending
221
221
  def outbox_claim(id) # rubocop:disable Naming/PredicateMethod
222
222
  row = @outbox.find { |held| held.id == id }
223
223
  return false unless row&.pending?
@@ -247,7 +247,7 @@ module Hecks
247
247
  # an update to apply; nil writes unconditionally. Ignored for a delete
248
248
  # @return [Runtime::Instance, PG::Result, nil] for a save, a new instance over the
249
249
  # entry's state with `version` set to the stored `hecks_version`, or nil when
250
- # `expected_version` no longer matched and nothing was written; for a delete, the
250
+ # `expected_version` did not match and nothing was written; for a delete, the
251
251
  # `DELETE` statement's `PG::Result`
252
252
  # @raise [PG::Error] if the statement fails
253
253
  # rubocop:disable Metrics/AbcSize -- the CAS/plain upsert split is one
@@ -85,7 +85,7 @@ module Hecks
85
85
  # never edited out from under it), but wrong for anything that
86
86
  # legitimately reloads an edited file in-process: a stale cached
87
87
  # tree reports a `given`/`ensures` block at its old line number,
88
- # which no longer matches the freshly re-executed file's own
88
+ # which does not match the freshly re-executed file's own
89
89
  # `block.source_location` — surfacing as "did not survive
90
90
  # extraction" on a perfectly valid file. Built for real building
91
91
  # `Hecks::Codemod` (lib/hecks/codemod.rb), which needs exactly this
@@ -15,7 +15,7 @@ module Hecks
15
15
  # Rebuilds a read store from the authoritative journal, entry by entry.
16
16
  # A value object is written as its JSON object and a reference as the
17
17
  # bare id it holds — the same shapes the command path writes, so there
18
- # is no second representation to accept here any more.
18
+ # is no second representation to accept here.
19
19
  class SqliteProjection < Sqlite
20
20
  # Replaces or deletes the read store's row for one journal entry, encoding the entry's
21
21
  # state directly rather than through a `Runtime::Instance`.
@@ -4,7 +4,7 @@ require "fileutils"
4
4
 
5
5
  module Hecks
6
6
  module Adapters
7
- # THE IMPERATIVE HALF of provisioning a tenant, pulled out of
7
+ # The imperative half of provisioning a tenant, pulled out of
8
8
  # `bin/project_tenant` and behind a real driven port
9
9
  # (`Deploy::Tenant.port "TenantProvisioning"`) instead — the same
10
10
  # hexagonal reasoning `GithubChecks` already gives for its own
@@ -16,34 +16,55 @@ module Hecks
16
16
  # `PortOperationInterpreter#ask`'s own "every failure is an answer"
17
17
  # comment for why nothing here needs its own rescue.
18
18
  #
19
- # DELIBERATELY DOES NOT BOOT THE TARGET DOMAIN — that step (proving
19
+ # ## What it leaves out
20
+ #
21
+ # Deliberately does not boot the target domain — that step (proving
20
22
  # it boots for real, running the tenant_capable? gate) stays in
21
- # `bin/project_tenant` itself, called AFTER this operation answers,
23
+ # `bin/project_tenant` itself, called after this operation answers,
22
24
  # not nested inside it. Booting a whole domain from inside an
23
25
  # adapter method that is itself running mid-dispatch (this port
24
26
  # operation) is a real, separate concern from writing one file, and
25
27
  # keeping the two apart avoids a nested `Hecks.boot` call ever
26
28
  # running inside another boot's own dispatch call stack.
27
29
  #
28
- # RETURNS A HASH SHAPED LIKE `Tenancy::Tenant.Register`'S OWN
29
- # ARGUMENTS, on purpose — `tenancy.hecksagon`'s own `translates`
30
+ # ## Return shape
31
+ #
32
+ # Returns a hash shaped like `Tenancy::Tenant.Register`'s own
33
+ # arguments, on purpose — `tenancy.hecksagon`'s own `translates`
30
34
  # reaction forwards an answered event's payload verbatim, so this
31
- # adapter's own return value IS the translation from Deploy's
35
+ # adapter's own return value is the translation from Deploy's
32
36
  # vocabulary into Tenancy's, decided here rather than in a mapping
33
37
  # layer neither side could see into.
34
38
  class TenantProvisioner
39
+ # Accepts the arguments every driven adapter is built with and keeps none of them.
40
+ #
41
+ # @param aggregate [Object, nil] unused
42
+ # @param settings [Hash] unused
43
+ # @param root [String, nil] unused
35
44
  def initialize(aggregate: nil, settings: {}, root: nil); end
36
45
 
37
- # `directory:` IS REQUIRED, NOT DERIVED FROM `domain` — a real,
46
+ # Writes the tenant's environment overlay file under its domain directory.
47
+ #
48
+ # `directory:` is required, not derived from `domain` — a real,
38
49
  # confirmed bug found only under CI's own real-Postgres suite: a
39
- # domain's own DECLARED NAME (`domain`, e.g. "Scratch") is not a
50
+ # domain's own declared name (`domain`, e.g. "Scratch") is not a
40
51
  # filesystem path, and File.expand_path(domain, Dir.pwd) silently
41
52
  # resolved to the wrong directory (relative to whatever the
42
- # RUNNING PROCESS' own cwd happened to be) whenever the CLI's own
53
+ # running process' own cwd happened to be) whenever the CLI's own
43
54
  # `--domain=` value didn't happen to equal its own directory
44
55
  # argument by coincidence. `bin/project_tenant` passes its own
45
56
  # real `domain_directory` local straight through.
46
-
57
+ #
58
+ # Every argument arrives either bare or wrapped as `{ value: ... }`; both are read.
59
+ #
60
+ # @param slug [String, Hash] the tenant's slug, which names the overlay file
61
+ # @param domain [String, Hash] the declared domain name the overlay is for
62
+ # @param realm [String, Hash] the realm the overlay declares
63
+ # @param schema [String, Hash] the schema the overlay declares
64
+ # @param database [String, Hash] the database the overlay declares
65
+ # @param adapter [String, Hash] the persistence adapter the overlay declares
66
+ # @param directory [String, Hash] the domain's own directory on disk
67
+ # @return [Hash{Symbol => Object}] `:slug`, `:domain`, `:realm` and `:schema`, as given
47
68
  def provision(slug:, domain:, realm:, schema:, database:, adapter:, directory:)
48
69
  domain_directory = File.expand_path(directory[:value] || directory)
49
70
 
@@ -228,7 +228,7 @@ module Hecks
228
228
  # the dispatch's own result
229
229
  # @return [Hash{Symbol => Object}] the settled record's current state, read
230
230
  # back from the repository; `result.state` (or `{}`) when the result has no
231
- # id or the repository no longer has that aggregate/record
231
+ # id or the repository does not have that aggregate/record
232
232
  def settled_state(runtime, verb, result)
233
233
  return result.state || {} unless result.respond_to?(:id) && result.id
234
234
 
@@ -266,7 +266,7 @@ module Hecks
266
266
  # only so long as nothing else ever asked it to resolve a port-operation
267
267
  # verb. Now that a `policy` can legitimately `trigger` a port operation
268
268
  # (`ReactionInvocation#resolve_target`'s own port-operation branch),
269
- # that raise no longer happens, so this checks directly instead.
269
+ # that raise does not happen, so this checks directly instead.
270
270
  #
271
271
  # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
272
272
  # booted runtime
@@ -151,7 +151,7 @@ module Hecks
151
151
  # Entity's contract claims `owner: :parent`
152
152
  #
153
153
  # `shape` and `handler` are not on this list — S17 (ADR 0026) made Member and
154
- # Dispatch nested entities, so neither is a parent field any more the way
154
+ # Dispatch nested entities, so neither is a parent field the way
155
155
  # `owner` is. `spec/assembly_spec` derives this set from `Plan` and the
156
156
  # contracts directly and fails on any drift, rather than trusting this list to
157
157
  # stay in sync with a second hand-written copy kept by the model generator.
@@ -243,7 +243,7 @@ module Hecks
243
243
 
244
244
  # S17, ADR 0026 — Handler is a genuine entity now, nested under
245
245
  # ProcessManager (`entity "Handler"`, reaction.bluebook). Neither
246
- # `position` nor `handler` is a stored field any more: a saga
246
+ # `position` nor `handler` is a stored field: a saga
247
247
  # answers each event once, so `event_type` is Handler's own real,
248
248
  # non-positional identity (no walk-minted `position` to derive),
249
249
  # and the process manager it belongs to is structural now — which
@@ -429,10 +429,10 @@ module Hecks
429
429
  # A member's pairs are an open map, which is why Member is its own root in
430
430
  # the language. The IR keeps them as a plain hash on the value object, so
431
431
  # they are assembled with their shape rather than as a construct.
432
- # S17, ADR 0026 — Member is a genuine entity now, nested under
433
- # ValueObject (`entity "Member"`, shape.bluebook). It no longer
434
- # holds a `shape` field at all (that was the free-text, un-parsed
435
- # spelling a standalone root once needed ; an entity's element is
432
+ # S17, ADR 0026 — Member is a genuine entity, nested under
433
+ # ValueObject (`entity "Member"`, shape.bluebook). It holds no
434
+ # `shape` field at all (a free-text, un-parsed spelling would suit
435
+ # only a standalone root ; an entity's element is
436
436
  # never serialized as text) — only `position` (walk-minted, see
437
437
  # `entity_own_identity`, judge.rb) and `pairs` (still an open map,
438
438
  # still one row per entry, still why a value object cannot hold it
@@ -21,8 +21,8 @@ module Hecks
21
21
  module Specializer
22
22
  module_function
23
23
 
24
- # S17, ADR 0026 — `Handler` is a genuine entity now, nested under
25
- # `ProcessManager`, so `.aggregate` alone no longer finds it —
24
+ # S17, ADR 0026 — `Handler` is a genuine entity, nested under
25
+ # `ProcessManager`, so `.aggregate` alone does not find it —
26
26
  # it hangs off some aggregate's own `.entities` instead
27
27
  # (searched recursively, the same reason `Value::Coercion#find_
28
28
  # entity` does: a nested entity, like `Dispatch` inside
@@ -31,7 +31,7 @@ module Hecks
31
31
  # ## What stays hand-written
32
32
  #
33
33
  # The containment — which construct holds which — stays hand-written; it is
34
- # not a field table. The runtime surface is no longer built here at all — the
34
+ # not a field table. The runtime surface is not built here at all — the
35
35
  # door is a per-boot projection, facade/surface.rb.
36
36
  class Assembly
37
37
  # Builds the graph the runtime runs from one chapter's declared hash.
@@ -91,15 +91,18 @@ module Hecks
91
91
  @ports_by_name[port.name] = port
92
92
  end
93
93
 
94
- # A TRANSLATED REACTION IS ALSO DECLARED IN THE HECKSAGON —
94
+ # A translated reaction is also declared in the hecksagon —
95
95
  # `HecksagonBuilder#translates` builds an ordinary `Policy` (same
96
96
  # shape, same `PolicyInterpreter` runtime, as one written inside a
97
97
  # `policy` block in this chapter's own `.bluebook`) and attaches it
98
98
  # here, after the chapter already exists, the same reasoning
99
99
  # `add_port` gives one method up. Kept out of the bluebook itself
100
- # on purpose: which FOREIGN domain's event this chapter reacts to
100
+ # on purpose: which foreign domain's event this chapter reacts to
101
101
  # is a wiring/context-mapping decision, not a fact the domain
102
102
  # states about its own model.
103
+ #
104
+ # @param policy [Bluebook::Policy] the translated reaction to attach
105
+ # @return [Array<Bluebook::Policy>] this chapter's policies, with `policy` appended
103
106
  def add_policy(policy)
104
107
  @policies << policy
105
108
  end
@@ -9,6 +9,24 @@ module Hecks
9
9
  # @return [Array<Symbol>] each declared `group_by` field's name, in declaration order
10
10
  def group_by_fields = @group_by.map { |row| row[:field].to_sym }
11
11
 
12
+ # Reports whether this read model's `group_by` names every identity head
13
+ # of the aggregate it groups (ADR 0061, decision D1).
14
+ #
15
+ # Such a key path cannot be shared by two rows, so it is accepted from the
16
+ # declaration alone and never checked for a collision at dispatch. Any
17
+ # other key path is checked per request.
18
+ #
19
+ # @param aggregate [Bluebook::Aggregate, nil] the aggregate of this read
20
+ # model's one many-side head
21
+ # @return [Boolean] false when `aggregate` is nil or declares no identity,
22
+ # since uniqueness cannot then be shown
23
+ def groups_by_identity?(aggregate)
24
+ return false unless aggregate
25
+
26
+ identity = Array(aggregate.identity_heads).map(&:to_sym)
27
+ !identity.empty? && (identity - group_by_fields).empty?
28
+ end
29
+
12
30
  # `!!` rather than a bare `@count` — the DSL/reconstruction
13
31
  # boundary (ReadModel#initialize) already normalises to
14
32
  # `true`/`nil`, so this is belt and braces against a future
@@ -19,11 +19,11 @@ module Hecks
19
19
  # Who may sign in, and with what role — rust/host's Google-OAuth
20
20
  # provision/member_rows resolve this instead of an env var naming
21
21
  # the aggregate (HECKS_MEMBERSHIP_AGGREGATE). Same declared-
22
- # not-named shape AUTHORIZATION already is for Governance.
22
+ # not-named shape `AUTHORIZATION` already is for Governance.
23
23
  MEMBERSHIP = "membership".freeze
24
24
  # A stable organizational identity, independent of how it was
25
25
  # authenticated — recognised by declaration, not the literal name
26
- # "Identity". Same declared-not-named shape AUTHORIZATION already
26
+ # "Identity". Same declared-not-named shape `AUTHORIZATION` already
27
27
  # is. rust/host does not build this chapter's payloads; any field
28
28
  # mapping lives on the consuming hecksagon's `translates` ACL.
29
29
  IDENTITY = "identity".freeze
@@ -4,7 +4,7 @@ require_relative "../ir"
4
4
 
5
5
  module Hecks
6
6
  module Bluebook
7
- # The chapter, and no longer the namespace it lives in.
7
+ # The chapter, which is not the namespace it lives in.
8
8
  #
9
9
  # `Hecks::Bluebook` was briefly a class, because dropping the
10
10
  # old `IR::` segment would otherwise have produced
@@ -198,7 +198,7 @@ module Hecks
198
198
  # never `[]`). Wave 6 (identity-and-relationships arc) un-deprecates
199
199
  # all three for real: a relationship word now retains the author's
200
200
  # domain concept in IR — still stored as one or more target
201
- # identities, but no longer collapsed to a bare `reference_to`
201
+ # identities, but not collapsed to a bare `reference_to`
202
202
  # during assembly. `MetaValidator.shadow_parsing?` still routes to
203
203
  # `legacy_has_many`/`legacy_has_one` so frozen era text written
204
204
  # under the old (lying/collapsing) meaning still parses the way it
@@ -511,7 +511,7 @@ module Hecks
511
511
  # are this construct's own refusal wording, not shared, since
512
512
  # `declared_by:` only exists here so far. Unresolved (no
513
513
  # candidate yet, or `declared_by:` naming an aggregate that
514
- # hasn't declared it yet) is no longer a fourth branch that
514
+ # hasn't declared it yet) is not a fourth branch that
515
515
  # raises here — see `#pending_chapter_given`, below, for why:
516
516
  # a chapter split across files can genuinely reference a
517
517
  # precondition a later file declares, and "not found among
@@ -77,7 +77,7 @@ module Hecks
77
77
  end
78
78
 
79
79
  def method_missing(verb, *args, **kwargs, &block)
80
- # A BARE CALL — no args, no kwargs, no block — starts (or
80
+ # **A bare call** — no args, no kwargs, no block — starts (or
81
81
  # continues, on `AttributePath` itself below) a Privacy
82
82
  # marking chain: `Registration.attendee.medications.has_phi(
83
83
  # readable_by: "Privacy officer")`. No existing real
@@ -115,10 +115,15 @@ module Hecks
115
115
  @path = path
116
116
  end
117
117
 
118
+ # Records a `has_<category>` marking, or extends the attribute path by one segment.
119
+ #
118
120
  # @param verb [Symbol] `has_<category>` to record the marking and end the chain;
119
121
  # any other bare name to extend the path one segment further
120
122
  # @param readable_by [String] required only for a `has_<category>` call — the
121
123
  # Governance role a read must hold to see this field unredacted
124
+ # @param args [Array] must be empty; a positional argument is refused
125
+ # @param kwargs [Hash] must be empty; any keyword but `readable_by:` is refused
126
+ # @param block [Proc, nil] must be absent; a block is refused
122
127
  # @return [Bluebook::DSL::AttributePath, nil] a longer chain for a plain segment;
123
128
  # `nil` (nothing further to chain) for a `has_<category>` call
124
129
  # @raise [Malformed] if a `has_<category>` call omits `readable_by:`, or any call
@@ -431,9 +431,9 @@ module Hecks
431
431
  # manager's own structural check fails
432
432
  def self.build(name, version: nil, &block)
433
433
  registry = Hecks.current_registry
434
- # WHICH FILE CALLED `Hecks.bluebook`, recorded for
434
+ # Which file called `Hecks.bluebook`, recorded for
435
435
  # `Registry#record_bluebook_source` — two frames up: this
436
- # method's own caller is `Hecks.bluebook` (hecks.rb), and ITS
436
+ # method's own caller is `Hecks.bluebook` (hecks.rb), and its
437
437
  # caller is the real `.bluebook` file's own top-level call site.
438
438
  caller_location = caller_locations(2, 1)&.first
439
439
  registry&.record_bluebook_source(name, caller_location&.path)
@@ -243,7 +243,7 @@ module Hecks
243
243
  # since ADR 0028, `command` only queues a descriptor and actually
244
244
  # builds at `#drain_pending!` time, well after this whole block
245
245
  # (including every `given` in it) has already run, so textual
246
- # order within the block no longer actually matters here; named
246
+ # order within the block does not actually matter here; named
247
247
  # for the reader anyway, since `given`'s own resolution logic
248
248
  # (`CommandBuilder#reference_named_given`) still reads whatever
249
249
  # `@named_givens` holds at the command's own build time, not by
@@ -8,7 +8,7 @@ module Hecks
8
8
  # already shipped) only checks a word's admissibility; this module
9
9
  # executes the safe, mechanical subset of what a word actually
10
10
  # does, read live off the same self-hosted grammar table — so a
11
- # builder method for one of these words no longer needs to be
11
+ # builder method for one of these words need not be
12
12
  # hand-written at all. Called from `WordGate#method_missing`'s own
13
13
  # "admitted here, no builder method for it yet" branch, so it only
14
14
  # ever sees a word whose (context, word) pair the grammar already
@@ -31,7 +31,7 @@ module Hecks
31
31
  @translates = []
32
32
  end
33
33
 
34
- # Marks THIS chapter as a bounded context — a consumer-owned chapter
34
+ # Marks this chapter as a bounded context — a consumer-owned chapter
35
35
  # that `uses_framework` / `uses_embryonaut_bluebook` will not load.
36
36
  # Framework and vendored packages get the mark automatically from
37
37
  # those words; they never write `bounded` in their own bluebooks.
@@ -158,12 +158,12 @@ module Hecks
158
158
  bluebook_ir.add_port(built)
159
159
  end
160
160
 
161
- # A TRANSLATION BOUNDARY, NOT A BUSINESS RULE — `translates "Name"
161
+ # A translation boundary, not a business rule — `translates "Name"
162
162
  # do on Foreign::Domain::SomeEvent; trigger Local.Command; end`
163
- # builds the EXACT SAME `Policy` a `policy` block inside this
163
+ # builds the exact same `Policy` a `policy` block inside this
164
164
  # domain's own `.bluebook` would (same `on_impl`/`trigger_impl`,
165
165
  # same `PolicyInterpreter` runtime — reusing `PolicyBuilder`
166
- # directly, zero new runtime semantics). What's new is only WHERE
166
+ # directly, zero new runtime semantics). What's new is only where
167
167
  # it can be written: a cross-domain reaction is a wiring/context-
168
168
  # mapping decision (this chapter conforming to a foreign chapter's
169
169
  # published event), the same kind of decision `port`/
@@ -171,23 +171,28 @@ module Hecks
171
171
  # model states about itself, which `policy` (inside the bluebook)
172
172
  # remains the right word for.
173
173
  #
174
- # MUST BE A BLOCK, not `translates Event, into: Command` — a flat
175
- # call's arguments are evaluated eagerly, under THIS builder's own
174
+ # Must be a block, not `translates Event, into: Command` — a flat
175
+ # call's arguments are evaluated eagerly, under this builder's own
176
176
  # `ConstShim` resolver (`BindingProxy.namespace`, set up by
177
- # `self.build` below), which mints a `BindingProxy` INSTANCE per
177
+ # `self.build` below), which mints a `BindingProxy` instance per
178
178
  # segment and cannot answer a further `::` (it is not a Module) —
179
179
  # a multi-segment reference like `Deploy::Tenant::TenantProvisioned`
180
180
  # would raise before this method ever ran. `port_impl`'s own
181
181
  # `ConstShim.with(->(const) { const })` swap only works because
182
182
  # its own callers never pass a multi-segment bare constant as an
183
183
  # eagerly-evaluated argument — everything of that shape lives
184
- # inside ITS block, evaluated later, under the swapped resolver.
184
+ # inside its block, evaluated later, under the swapped resolver.
185
185
  # A block defers evaluation the same way; a flat call cannot.
186
+ #
187
+ # @param name [String, Symbol] the reaction's name
188
+ # @yield the `on`/`trigger` block, evaluated later under the scoped resolver
189
+ # @return [Array<Bluebook::Policy>] the chapter's policies, with the new one appended
190
+ # @raise [Malformed] if no bluebook is registered for this domain to attach the reaction to
186
191
  def translates(name, &block)
187
192
  bluebook_ir = Hecks.current_registry.bluebook(@domain) or
188
193
  raise Malformed, "#{@domain} declares no such bluebook — translates needs one to attach its reaction to"
189
194
 
190
- # SAME RESOLVER BluebookBuilder ITSELF uses for a `policy`
195
+ # Same resolver BluebookBuilder itself uses for a `policy`
191
196
  # block's own `on`/`trigger` (bluebook_builder.rb) — a
192
197
  # `ScopedConstant`, not the bare passthrough `port_impl` swaps
193
198
  # to, because `on`/`trigger` here take genuinely multi-segment
@@ -147,8 +147,8 @@ module Hecks
147
147
  .find { |rule| rule.description == description }
148
148
  end
149
149
 
150
- # **Which construct uses which primitive** — no longer a Ruby-only
151
- # Hash (that was this constant's own shape, one round ago): the
150
+ # **Which construct uses which primitive** — not a Ruby-only
151
+ # Hash, because the
152
152
  # user's own correction — "my goal is that if they read the same
153
153
  # table they behave identically" — means a table only Ruby ever
154
154
  # reads cannot deliver that, no matter how faithfully it is
@@ -172,7 +172,7 @@ module Hecks
172
172
  # after `grammar_registry` is fully built and memoized, so reads
173
173
  # the real table, every time, no exception.
174
174
  #
175
- # No longer kept in sync by hand — the same `resolves_via`/
175
+ # Not kept in sync by hand — the same `resolves_via`/
176
176
  # `disambiguator` columns, projected ahead of time into the
177
177
  # committed lib/hecks/bluebook/dsl/bootstrap_table.rb
178
178
  # (bin/project_bootstrap_table, pinned by spec/bootstrap_table_spec.rb).
@@ -268,7 +268,7 @@ module Hecks
268
268
  # Parses a whole `.translation` file into a `Translation` — the
269
269
  # domain's own `from:`/`to:` era pair, its list of `aggregate`
270
270
  # translation blocks (each built by `TranslationAggregateBuilder`
271
- # above), and any `retired` aggregates that no longer exist in the
271
+ # above), and any `retired` aggregates that do not exist in the
272
272
  # destination era.
273
273
  class TranslationBuilder
274
274
  GRAMMAR_CONTEXT = "Translation".freeze
@@ -13,6 +13,11 @@ module Hecks
13
13
 
14
14
  # A call with a block records the block's own settings as a nested Hash, so a bind can
15
15
  # group settings (`preview do ... end`); a call without one records its argument.
16
+ #
17
+ # @param key [Symbol] the setting's name, recorded verbatim
18
+ # @param args [Array] the setting's value: one argument is kept as is, several as an array
19
+ # @param block [Proc, nil] a nested settings block, evaluated on a fresh collector
20
+ # @return [Object] the value recorded under `key`
16
21
  def method_missing(key, *args, &block)
17
22
  @values[key.to_sym] =
18
23
  if block
@@ -302,7 +302,7 @@ module Hecks
302
302
  end
303
303
 
304
304
  # Entity-owned categories have no top-level aggregate for the runtime
305
- # to route a bare verb into any more — `Member`'s own "Pair" reaches
305
+ # to route a bare verb into — `Member`'s own "Pair" reaches
306
306
  # the runtime as `ValueObject.Member.Pair`, and a nested one
307
307
  # (`Dispatch`, inside `Handler`) reaches it as `ProcessManager.
308
308
  # Handler.Dispatch.Bind` — the dotted shape `EntityInterpreter#call`
@@ -567,8 +567,8 @@ module Hecks
567
567
  # Reference its type is Reference<X>, another aggregate's head
568
568
  # Holds its type names an entity this aggregate declares
569
569
  #
570
- # Nothing is skipped any more. Reference and Holds did not exist, so the
571
- # walk dropped both kinds and the meta-domain silently did not contain
570
+ # Nothing is skipped. Without Reference and Holds the walk would drop
571
+ # both kinds and the meta-domain would silently not contain
572
572
  # Account#customer_id or Account#ledger.
573
573
  # Each alternate carries its own map, read from the language. Borrowing the
574
574
  # primary's map dispatched `type:` where Reference declares `points_at:`,
@@ -108,7 +108,7 @@ module Hecks
108
108
  # S17, ADR 0026 — Member/Handler/Dispatch are entities now
109
109
  # (`entity "Member" do ... end`, nested under `ValueObject`/
110
110
  # `ProcessManager`/`Handler`), not their own top-level
111
- # aggregates — so `meta.aggregates` alone no longer finds
111
+ # aggregates — so `meta.aggregates` alone does not find
112
112
  # them the way it always found a standalone `aggregate
113
113
  # "Member"`. Each still needs its own named category here:
114
114
  # `Assembly::Contracts` keeps a bespoke entry for each
@@ -125,8 +125,8 @@ module Hecks
125
125
  # itself ever declares are exactly the three this ADR
126
126
  # names. `entity_owned: true` records why this is a
127
127
  # category at all — the real runtime has no top-level
128
- # aggregate named "Member" to dispatch a bare verb into
129
- # any more, so the judge has to build a dotted one instead
128
+ # aggregate named "Member" to dispatch a bare verb into,
129
+ # so the judge has to build a dotted one instead
130
130
  # (see `Judge#verb_for`).
131
131
  #
132
132
  # Recurses — `Dispatch` nests inside `Handler`, which nests
@@ -158,7 +158,7 @@ module Hecks
158
158
  # Every verb the language declares, spelled as the judge would
159
159
  # dispatch it. S17, ADR 0026 — an entity-owned category (its
160
160
  # own `entity_owned` flag) has no real top-level aggregate the
161
- # runtime can route a bare verb into any more, so it is
161
+ # runtime can route a bare verb into, so it is
162
162
  # spelled dotted here too — `Bluebook::ValueObject.Member.
163
163
  # Declare`, `Bluebook::ProcessManager.Handler.Dispatch.Bind` —
164
164
  # matching `Judge#verb_for`/`#dotted_prefix`'s own build
@@ -364,7 +364,7 @@ module Hecks
364
364
  # same way Shapes#provenance reads it back.
365
365
  return encode_literal(node.provenance) if field == :provenance
366
366
 
367
- # `identified_by` is no longer a field of any declaration — it is a list,
367
+ # `identified_by` is not a field of any declaration — it is a list,
368
368
  # filled by Identify one part at a time, so it is read through `identity_rows`
369
369
  # like every other list rather than special-cased here. What this branch
370
370
  # existed to protect is now structural : a path cannot come back as its head,
@@ -228,7 +228,7 @@ module Hecks
228
228
  # by its own dotted `ValueObject.Member.Pair`. Its data therefore
229
229
  # lives inline on the value object's own dispatched state — same as
230
230
  # any other entity list — not behind a separate `DeclaredIn` query:
231
- # there is no such query any more, because there is no top-level
231
+ # there is no such query, because there is no top-level
232
232
  # "Member" aggregate left to hold one. Pairs are still an open map,
233
233
  # which no value object can hold, so they still come back one pair
234
234
  # at a time.
@@ -330,7 +330,7 @@ module Hecks
330
330
  # Handler.Dispatch`/`...Dispatch.Bind`. Its data therefore lives
331
331
  # inline on the process manager's own dispatched state — same as
332
332
  # any other entity list — not behind a separate `DeclaredIn`
333
- # query : there is no such query any more, the same fix
333
+ # query : there is no such query, the same fix
334
334
  # `Reconstruction#members_of` already made for Member.
335
335
  def process_manager(row)
336
336
  declaration("ProcessManager", row,
@@ -32,7 +32,7 @@ module Hecks
32
32
  default: decode_literal(text(field[:default])),
33
33
  # Read back the same way `list` is — both are booleans about the
34
34
  # attribute, held as text, and dropping either would rebuild a
35
- # bluebook that no longer says what it said.
35
+ # bluebook that says something other than what was written.
36
36
  optional: text(field[:optional]).to_s == "true",
37
37
  pattern: presence(text(field[:pattern])),
38
38
  # The round trip is the only way in. The grammar registry keeps the
@@ -1,5 +1,6 @@
1
1
  require "digest"
2
2
  require "fileutils"
3
+ require_relative "../../cache_dir"
3
4
 
4
5
  module Hecks
5
6
  module Bluebook
@@ -145,13 +146,10 @@ module Hecks
145
146
  #
146
147
  # Fails toward a real boot, never toward a wrong table — same
147
148
  # loud-not-silent discipline this codebase already holds CI to
148
- # (`postgres_io_relevant_changed`'s own header). A missing file, a
149
- # corrupt Marshal blob, a permission error, an unwritable `tmp/` —
150
- # every one of these degrades to "no disk cache today", never to a
151
- # crash or a served-but-wrong table. `tmp/` is already gitignored
152
- # for exactly this kind of local, disposable-but-useful-while-it-
153
- # lasts artifact (`Storehouse::LOG_ROOT`'s own header names the
154
- # same convention).
149
+ # (`postgres_io_relevant_changed`'s own header). A missing file, a corrupt
150
+ # Marshal blob, a permission error or an unwritable cache directory degrades
151
+ # to "no disk cache today", never to a crash or a served-but-wrong table.
152
+ # The cache lives under `Hecks::CacheDir`, never the gem's read-only directory.
155
153
  #
156
154
  # **Atomic write, not a lock** — `qa_sweep --all` spawns up to 4
157
155
  # children at once, any of which could reach a cold cache
@@ -166,7 +164,10 @@ module Hecks
166
164
  # change, the same shape `Storehouse::BOOT_ROOT`'s own
167
165
  # `HECKS_STOREHOUSE_ROOT` override uses, for the day this needs to
168
166
  # be ruled out while debugging something else entirely.
169
- CACHE_DIR = File.expand_path("../../../../tmp/hecks_syntax_boot_cache", __dir__).freeze
167
+ #
168
+ # @return [String] the directory cache entries are written to:
169
+ # `hecks_syntax_boot_cache` under `Hecks::CacheDir`
170
+ def cache_dir = Hecks::CacheDir.path("hecks_syntax_boot_cache")
170
171
 
171
172
  # Reports whether the cross-process disk cache is turned on.
172
173
  #
@@ -203,7 +204,7 @@ module Hecks
203
204
  return unless disk_cache_enabled?
204
205
 
205
206
  path = disk_cache_path(chapters)
206
- FileUtils.mkdir_p(CACHE_DIR)
207
+ FileUtils.mkdir_p(cache_dir)
207
208
  tmp_path = "#{path}.#{Process.pid}.tmp"
208
209
  File.binwrite(tmp_path, Marshal.dump(result))
209
210
  File.rename(tmp_path, path)
@@ -217,7 +218,7 @@ module Hecks
217
218
  # chapter]` pairs to key the cache entry on
218
219
  # @return [String] the disk cache file path for `chapters`
219
220
  def disk_cache_path(chapters)
220
- File.join(CACHE_DIR, "#{disk_cache_key(chapters)}.marshal")
221
+ File.join(cache_dir, "#{disk_cache_key(chapters)}.marshal")
221
222
  end
222
223
 
223
224
  # Builds the cache key for `chapters`.