hecks 2.6.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 (216) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +13 -0
  3. data/lib/hecks/adapters/driven/google_authentication.rb +1 -1
  4. data/lib/hecks/adapters/driven/lambda/client.rb +1 -1
  5. data/lib/hecks/adapters/driven/memory.rb +1 -1
  6. data/lib/hecks/adapters/driven/postgres.rb +1 -1
  7. data/lib/hecks/adapters/driven/prism.rb +2 -2
  8. data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
  9. data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
  10. data/lib/hecks/behaviors/expectations.rb +2 -2
  11. data/lib/hecks/bluebook/assembly/contract.rb +1 -1
  12. data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
  13. data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
  14. data/lib/hecks/bluebook/assembly.rb +1 -1
  15. data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
  16. data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
  17. data/lib/hecks/bluebook/capabilities.rb +2 -2
  18. data/lib/hecks/bluebook/chapter.rb +1 -1
  19. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
  20. data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
  21. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
  22. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -0
  23. data/lib/hecks/bluebook/dsl/command_builder.rb +3 -3
  24. data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
  25. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
  26. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
  27. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +1 -1
  28. data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
  29. data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
  30. data/lib/hecks/bluebook/dsl/world_builder.rb +47 -3
  31. data/lib/hecks/bluebook/hexagon.rb +15 -7
  32. data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
  33. data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
  34. data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
  35. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
  36. data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
  37. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
  38. data/lib/hecks/bluebook/meta_validator/world_judge.rb +3 -1
  39. data/lib/hecks/bluebook/meta_validator.rb +1 -1
  40. data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
  41. data/lib/hecks/bluebook/model_check.rb +13 -14
  42. data/lib/hecks/cache_dir.rb +103 -0
  43. data/lib/hecks/cli/document.rb +104 -0
  44. data/lib/hecks/cli/ir.rb +47 -0
  45. data/lib/hecks/cli/mcp.rb +32 -0
  46. data/lib/hecks/cli/mcp_door.rb +461 -0
  47. data/lib/hecks/cli/model_check.rb +270 -0
  48. data/lib/hecks/cli/project_cli.rb +123 -0
  49. data/lib/hecks/cli/project_diagrams.rb +43 -0
  50. data/lib/hecks/cli/run.rb +219 -0
  51. data/lib/hecks/cli/smoke_test.rb +59 -0
  52. data/lib/hecks/cli/stores.rb +54 -0
  53. data/lib/hecks/cli.rb +166 -0
  54. data/lib/hecks/embryonaut_bluebook/lock.rb +72 -0
  55. data/lib/hecks/embryonaut_bluebook/shape.rb +47 -0
  56. data/lib/hecks/embryonaut_bluebook/vendor.rb +194 -0
  57. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +88 -0
  58. data/lib/hecks/embryonaut_bluebook.rb +29 -3
  59. data/lib/hecks/facade/handle.rb +2 -2
  60. data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
  61. data/lib/hecks/forms/value_object_shape.rb +1 -1
  62. data/lib/hecks/framework/bluebook/console_settings.bluebook +3 -3
  63. data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
  64. data/lib/hecks/language/world/world.bluebook +32 -2
  65. data/lib/hecks/mcp_door_scope.rb +176 -0
  66. data/lib/hecks/ports/key_vault.rb +1 -1
  67. data/lib/hecks/ports/persistence/append_only.rb +1 -1
  68. data/lib/hecks/ports/persistence/binding_policy.rb +16 -5
  69. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -2
  70. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
  71. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
  72. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +132 -0
  73. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +1 -1
  74. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
  75. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
  76. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
  77. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +13 -7
  78. data/lib/hecks/ports/persistence/plugins/era.rb +7 -4
  79. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +73 -0
  80. data/lib/hecks/ports/persistence/postgres_dump.rb +125 -0
  81. data/lib/hecks/ports/persistence/repository_factory.rb +2 -2
  82. data/lib/hecks/ports/persistence/state_codec.rb +1 -1
  83. data/lib/hecks/ports/persistence.rb +4 -0
  84. data/lib/hecks/projections/deploy/fargate/assembly.rb +193 -0
  85. data/lib/hecks/projections/deploy/fargate/cdn.rb +318 -0
  86. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +87 -0
  87. data/lib/hecks/projections/deploy/fargate/check.rb +172 -0
  88. data/lib/hecks/projections/deploy/fargate/containers.rb +412 -0
  89. data/lib/hecks/projections/deploy/fargate/extras.rb +268 -0
  90. data/lib/hecks/projections/deploy/fargate/monitoring.rb +448 -0
  91. data/lib/hecks/projections/deploy/fargate/settings.rb +235 -0
  92. data/lib/hecks/projections/deploy/fargate/yaml.rb +116 -0
  93. data/lib/hecks/projections/deploy/fargate.rb +188 -139
  94. data/lib/hecks/projections/deploy/lambda.rb +8 -8
  95. data/lib/hecks/projections/deploy/preview/access_yaml.rb +145 -0
  96. data/lib/hecks/projections/deploy/preview/containers.rb +218 -0
  97. data/lib/hecks/projections/deploy/preview/preview.sh.erb +446 -0
  98. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +216 -0
  99. data/lib/hecks/projections/deploy/preview/script.rb +40 -0
  100. data/lib/hecks/projections/deploy/preview/settings.rb +140 -0
  101. data/lib/hecks/projections/deploy/preview/task_yaml.rb +215 -0
  102. data/lib/hecks/projections/deploy/preview/template.rb +247 -0
  103. data/lib/hecks/projections/deploy/preview/yaml_text.rb +48 -0
  104. data/lib/hecks/projections/deploy/preview.rb +103 -0
  105. data/lib/hecks/projections/deploy/scripts/deploy-service.sh.tmpl +182 -0
  106. data/lib/hecks/projections/deploy/scripts/expected-era.header +17 -0
  107. data/lib/hecks/projections/deploy/scripts/hosting.mk.tmpl +54 -0
  108. data/lib/hecks/projections/deploy/scripts/settings.rb +114 -0
  109. data/lib/hecks/projections/deploy/scripts/smoke-after-deploy.sh.tmpl +232 -0
  110. data/lib/hecks/projections/deploy/scripts.rb +171 -0
  111. data/lib/hecks/projections/deploy/smoke/harness.js +366 -0
  112. data/lib/hecks/projections/deploy/smoke/workflow.yml.tmpl +56 -0
  113. data/lib/hecks/projections/deploy/smoke.rb +159 -0
  114. data/lib/hecks/projections/deploy/template_diff/comparer.rb +231 -0
  115. data/lib/hecks/projections/deploy/template_diff/loader.rb +105 -0
  116. data/lib/hecks/projections/deploy/template_diff.rb +173 -0
  117. data/lib/hecks/projections/glossary/sensitivity.rb +1 -1
  118. data/lib/hecks/projections/reference.rb +7 -1
  119. data/lib/hecks/projections.rb +2 -0
  120. data/lib/hecks/projector/exporter.rb +4 -4
  121. data/lib/hecks/projector/target.rb +1 -1
  122. data/lib/hecks/release/gem_pin/published_versions.rb +58 -0
  123. data/lib/hecks/release/gem_pin.rb +112 -0
  124. data/lib/hecks/release/runner/ci_publisher.rb +96 -0
  125. data/lib/hecks/release/runner/commands.rb +68 -0
  126. data/lib/hecks/release/runner/console.rb +52 -0
  127. data/lib/hecks/release/runner/gem_publisher.rb +44 -0
  128. data/lib/hecks/release/runner/git.rb +48 -0
  129. data/lib/hecks/release/runner/npm_publisher.rb +89 -0
  130. data/lib/hecks/release/runner/preflight.rb +119 -0
  131. data/lib/hecks/release/runner/published.rb +55 -0
  132. data/lib/hecks/release/runner/tagger.rb +87 -0
  133. data/lib/hecks/release/runner.rb +244 -0
  134. data/lib/hecks/router/namespace_installer.rb +1 -1
  135. data/lib/hecks/runtime/command_interpreter.rb +3 -3
  136. data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
  137. data/lib/hecks/runtime/command_rules/arithmetic.rb +2 -2
  138. data/lib/hecks/runtime/entity_element.rb +1 -1
  139. data/lib/hecks/runtime/entity_interpreter.rb +1 -1
  140. data/lib/hecks/runtime/invocation.rb +1 -1
  141. data/lib/hecks/runtime/loader.rb +27 -3
  142. data/lib/hecks/runtime/outbox.rb +1 -1
  143. data/lib/hecks/runtime/reaction_invocation.rb +3 -3
  144. data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
  145. data/lib/hecks/runtime/reference_hop.rb +4 -4
  146. data/lib/hecks/runtime/registry/saga_persistence.rb +8 -2
  147. data/lib/hecks/runtime/registry/verification.rb +15 -13
  148. data/lib/hecks/runtime/registry/world_defaults.rb +106 -0
  149. data/lib/hecks/runtime/registry.rb +22 -13
  150. data/lib/hecks/runtime/saga_interpreter.rb +1 -1
  151. data/lib/hecks/runtime/tenant_check.rb +1 -1
  152. data/lib/hecks/runtime/value/coercion.rb +3 -3
  153. data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
  154. data/lib/hecks/storehouse.rb +18 -12
  155. data/lib/hecks/vendoring/git_environment.rb +35 -0
  156. data/lib/hecks/vendoring/git_source.rb +102 -0
  157. data/lib/hecks/vendoring.rb +102 -0
  158. data/lib/hecks/version.rb +1 -1
  159. data/lib/hecks/vocabulary.rb +5 -0
  160. data/lib/hecks.rb +8 -2
  161. metadata +72 -58
  162. data/lib/hecks/bench/cli.rb +0 -74
  163. data/lib/hecks/bench/environment.rb +0 -100
  164. data/lib/hecks/bench/postgres_probe.rb +0 -41
  165. data/lib/hecks/bench/report.rb +0 -116
  166. data/lib/hecks/bench/ruby_runner.rb +0 -151
  167. data/lib/hecks/bench/run.rb +0 -55
  168. data/lib/hecks/bench/rust_runner.rb +0 -165
  169. data/lib/hecks/bench/stats.rb +0 -75
  170. data/lib/hecks/bench/suite.rb +0 -172
  171. data/lib/hecks/bench/workload.rb +0 -137
  172. data/lib/hecks/bench.rb +0 -28
  173. data/lib/hecks/codemod.rb +0 -443
  174. data/lib/hecks/corpus.rb +0 -479
  175. data/lib/hecks/doc/reference.rb +0 -585
  176. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  177. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  178. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  179. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  180. data/lib/hecks/fuzzing/differential.rb +0 -192
  181. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  182. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  183. data/lib/hecks/fuzzing/form_census.rb +0 -285
  184. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  185. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  186. data/lib/hecks/fuzzing/isolated_boot.rb +0 -604
  187. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  188. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  189. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  190. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  191. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  192. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  193. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  194. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  195. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  196. data/lib/hecks/fuzzing/properties.rb +0 -267
  197. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  198. data/lib/hecks/fuzzing/replay.rb +0 -930
  199. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  200. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  201. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  202. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  203. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  204. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  205. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  206. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  207. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  208. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  209. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  210. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  211. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  212. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  213. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  214. data/lib/hecks/fuzzing.rb +0 -23
  215. data/lib/hecks/grammar/evolve.rb +0 -524
  216. data/lib/hecks/query_ir.rb +0 -482
@@ -13,10 +13,10 @@ module Hecks
13
13
  #
14
14
  # This module and its `uses_embryonaut_bluebook`
15
15
  # DSL word (hecksagon_builder.rb) were built on a prior commit of this
16
- # repo (933d1dd), vendored out to a real consumer (lifeadelics/domain,
16
+ # repo (933d1dd), vendored out to a real consumer (a client site's domain,
17
17
  # for embryonaut_bluebooks/payments), and then lost from this repo's own
18
18
  # reachable history — a hard reset or rebase left no branch containing
19
- # that commit. The lifeadelics vendor snapshot (a `git archive` of that
19
+ # that commit. The consumer's vendor snapshot (a `git archive` of that
20
20
  # commit, committed into their repo) was the only surviving copy; this
21
21
  # file is ported forward from it, checked against current `main`'s own
22
22
  # conventions rather than copied wholesale, since the two trees had
@@ -64,7 +64,18 @@ module Hecks
64
64
  # => "Payments"), not a separate ledger. A vendored package's directory
65
65
  # name and its declared `Hecks.bluebook` name are the one convention
66
66
  # this reuses from Framework rather than reinventing.
67
+ #
68
+ # ## Vendoring
69
+ #
70
+ # `vendor!` is the other direction: it pins a package from a local checkout
71
+ # of the registry into a consuming project's own `vendor/` tree, and is the
72
+ # one implementation behind `bin/vendor_bluebook` (see Vendoring and Vendor).
67
73
  module EmbryonautBluebook
74
+ autoload :Lock, File.join(__dir__, "embryonaut_bluebook/lock")
75
+ autoload :Shape, File.join(__dir__, "embryonaut_bluebook/shape")
76
+ autoload :Vendor, File.join(__dir__, "embryonaut_bluebook/vendor")
77
+ autoload :VendorCli, File.join(__dir__, "embryonaut_bluebook/vendor_cli")
78
+
68
79
  # Loads a vendored embryonaut bluebook package's `.bluebook` files, once
69
80
  # per registry.
70
81
  #
@@ -89,10 +100,25 @@ module Hecks
89
100
  if files.empty?
90
101
  raise Runtime::WiringError,
91
102
  "no vendored embryonaut bluebook named #{name.inspect} at #{dir} — " \
92
- "run bin/vendor_embryonaut_bluebooks #{name}"
103
+ "vendor it with bin/vendor_bluebook #{name}"
93
104
  end
94
105
 
95
106
  files.each { |file| Kernel.load(file) }
96
107
  end
108
+
109
+ # Pins one package of the registry into a project's `vendor/` tree.
110
+ #
111
+ # @param name [String, Symbol] the package's directory name, such as `"payments"`
112
+ # @param from [String] path of a local checkout of the registry repository
113
+ # @param ref [String, nil] a release version (`"1.2.0"`), a release tag name, any
114
+ # commit-ish, or nil for the newest release
115
+ # @param root [String] the consuming project's root
116
+ # @param allow_downgrade [Boolean] whether a release older than the vendored one is allowed
117
+ # @return [Vendor::Result] the commit, version and storage shape now vendored
118
+ # @raise [Vendoring::Error] if the source, release or files are unusable, or a
119
+ # version-policy refusal applies (see Vendor)
120
+ def self.vendor!(name, from:, root:, ref: nil, allow_downgrade: false)
121
+ Vendor.new(name, from: from, root: root, ref: ref, allow_downgrade: allow_downgrade).call
122
+ end
97
123
  end
98
124
  end
@@ -142,14 +142,14 @@ module Hecks
142
142
  # privacy.bluebook): a marking's presence is what makes a read
143
143
  # redacted, not a separate flag this class carries itself.
144
144
  #
145
- # ALWAYS THE STRONG CHECK, never the weak string-only fallback
145
+ # Always the strong check, never the weak string-only fallback
146
146
  # `CommandRules::Authorization#refuse_role_mismatch` allows an
147
147
  # unidentified caller — a read gate gone wrong is a leak, not a
148
148
  # refused command, so an ambient caller with no `actor_id` (or no
149
149
  # caller at all) is masked here, full stop, rather than waved
150
150
  # through the way a self-asserted `role` string is for a command.
151
151
  #
152
- # ONE LEVEL OF NESTING ONLY — `attendee.medications` masks inside
152
+ # One level of nesting only — `attendee.medications` masks inside
153
153
  # the returned `Runtime::Value` via its own `#with`; a marking two
154
154
  # levels deep is not supported and is left unmasked rather than
155
155
  # silently mishandled, since nothing in this corpus needs it yet.
@@ -165,7 +165,7 @@ module Hecks
165
165
  end
166
166
 
167
167
  door.define_singleton_method(:method_missing) do |verb, *args, **kwargs, &block|
168
- # A BARE CALL starts a Privacy marking chain — see
168
+ # A bare call starts a Privacy marking chain — see
169
169
  # `Bluebook::DSL::BindingProxy#method_missing`'s own header;
170
170
  # this is the same mechanism, reached when the constant is
171
171
  # already a real, installed door (a second boot in-process)
@@ -2,7 +2,7 @@ module Hecks
2
2
  module Forms
3
3
  # The value-object classification every consumer of a resolved VO
4
4
  # shape needs, spelled once — `field_shape.rb` (a command's own
5
- # form), `ui_schema.rb` (embryonaut_console's table/detail-panel
5
+ # form), `ui_schema.rb` (the console app's table/detail-panel
6
6
  # renderer, a separate app, not this repo), and
7
7
  # `adapters/driven/sql_query_builder.rb` (a query's own ORDER BY/
8
8
  # where compiler) each grew their own copy of "is this VO money-
@@ -1,5 +1,5 @@
1
1
  Hecks.bluebook "ConsoleSettings" do
2
- vision "A generic admin console's own curated UI facts — nav, columns, state tones, which rows a detail panel shows — as real dispatched data instead of a hand-rewritten YAML file. A UI opinion the DOMAIN it's pointed at has no way to answer, never a business record; see embryonaut_console's own presentation_config.rb for the fuller story."
2
+ vision "A generic admin console's own curated UI facts — nav, columns, state tones, which rows a detail panel shows — as real dispatched data instead of a hand-rewritten YAML file. A UI opinion the DOMAIN it's pointed at has no way to answer, never a business record; see the console app's own presentation_config.rb for the fuller story."
3
3
  supporting
4
4
 
5
5
  # A FRAMEWORK MEMBER LIKE IDENTITY/GOVERNANCE — attach with
@@ -7,7 +7,7 @@ Hecks.bluebook "ConsoleSettings" do
7
7
  # `.hecksagon`, plus a `Hecks.hecksagon "ConsoleSettings" do ... end`
8
8
  # persistence binding (Heki is the common case — a small, local,
9
9
  # per-app settings store, no shared database needed). This used to
10
- # boot in a SECOND, independent registry (embryonaut_console's own
10
+ # boot in a SECOND, independent registry (the console app's own
11
11
  # console_settings_domain.rb) specifically so no consuming app needed
12
12
  # any change at all — that property is real and now given up on
13
13
  # purpose, in favor of the one binding mechanism every other
@@ -30,7 +30,7 @@ Hecks.bluebook "ConsoleSettings" do
30
30
  # (Overview, below) and every `extra_json`/`identity_extra_json`
31
31
  # field throughout this chapter were BOTH originally scoped out on an
32
32
  # "confirmed unused in any real presentation.yml" basis that turned
33
- # out to be checked against the wrong file — embryonaut_console's own
33
+ # out to be checked against the wrong file — the console app's own
34
34
  # fixture, never this app's actual, live one. Migrating the real file
35
35
  # found both very much in use.
36
36
 
@@ -339,6 +339,12 @@ Hecks.bluebook "Bluebook" do
339
339
  member refusal: "InvariantViolation", site: "undeclared_set",
340
340
  template: "{name} admits {admits}, which this chapter does not declare — a closed set is named " \
341
341
  "Aggregate::SetName, and it must be one the bluebook actually holds"
342
+ # ADR 0061 decision D1: a group_by leaf holds one row, so two rows
343
+ # reaching the same full key path refuse at dispatch rather than one
344
+ # of them being dropped.
345
+ member refusal: "InvariantViolation", site: "group_by_collision",
346
+ template: "{read_model} groups by {fields}, but rows {ids} share {key} — a group_by " \
347
+ "leaf holds one row; add a field that tells them apart"
342
348
  member refusal: "Unauthorized", site: "tenant_required",
343
349
  template: "{query} declares authorize with tenant: {field} — pass {field}: to name which " \
344
350
  "{field} this ask is scoped to"
@@ -620,6 +626,16 @@ Hecks.bluebook "Bluebook" do
620
626
  shape: "scalar", quoting: "none", separator: "", sorted: "false", when_empty: ""
621
627
  member refusal: "InvariantViolation", site: "undeclared_set", argument: "admits",
622
628
  shape: "scalar", quoting: "none", separator: "", sorted: "false", when_empty: ""
629
+ member refusal: "InvariantViolation", site: "group_by_collision", argument: "read_model",
630
+ shape: "scalar", quoting: "none", separator: "", sorted: "false", when_empty: ""
631
+ member refusal: "InvariantViolation", site: "group_by_collision", argument: "fields",
632
+ shape: "list", quoting: "none", separator: ", ", sorted: "false", when_empty: ""
633
+ member refusal: "InvariantViolation", site: "group_by_collision", argument: "ids",
634
+ shape: "list", quoting: "inspect", separator: ", ", sorted: "true", when_empty: ""
635
+ # "field = value" per key-path level, joined with ", " by the caller:
636
+ # each value is its own `to_s`, the same text the nested Hash keys it by.
637
+ member refusal: "InvariantViolation", site: "group_by_collision", argument: "key",
638
+ shape: "scalar", quoting: "none", separator: "", sorted: "false", when_empty: ""
623
639
  member refusal: "Unauthorized", site: "tenant_required", argument: "query",
624
640
  shape: "scalar", quoting: "none", separator: "", sorted: "false", when_empty: ""
625
641
  member refusal: "Unauthorized", site: "tenant_required", argument: "field",
@@ -4,8 +4,10 @@ Hecks.bluebook "World" do
4
4
 
5
5
  identified_by WorldName, as: :domain
6
6
 
7
- attribute :realm, Realm
8
- attribute :latest, Latest
7
+ attribute :realm, Realm
8
+ attribute :latest, Latest
9
+ attribute :default_database, DefaultDatabase
10
+ attribute :default_adapter, DefaultAdapter
9
11
 
10
12
  value_object "WorldName" do
11
13
  attribute :value, String
@@ -22,6 +24,22 @@ Hecks.bluebook "World" do
22
24
  invariant("a latest version says something") { !value.to_s.empty? }
23
25
  end
24
26
 
27
+ # The connection every chapter's database-taking persistence adapter
28
+ # uses unless that chapter's own world says otherwise — a world's
29
+ # per-chapter `persisted_by ... database` block, written once.
30
+ value_object "DefaultDatabase" do
31
+ attribute :value, String
32
+ invariant("a default database says something") { !value.to_s.empty? }
33
+ end
34
+
35
+ # The persistence adapter every aggregate binds to unless its own
36
+ # chapter's hecksagon binds it — between an explicit bind and the
37
+ # framework's in-memory fallback.
38
+ value_object "DefaultAdapter" do
39
+ attribute :value, String
40
+ invariant("a default adapter names an adapter") { !value.to_s.empty? }
41
+ end
42
+
25
43
  command "Declare" do
26
44
  role "Deployment"
27
45
  goal "Open a world for a domain"
@@ -29,10 +47,14 @@ Hecks.bluebook "World" do
29
47
  attribute :domain, WorldName
30
48
  attribute :realm, Realm, optional: true
31
49
  attribute :latest, Latest, optional: true
50
+ attribute :default_database, DefaultDatabase, optional: true
51
+ attribute :default_adapter, DefaultAdapter, optional: true
32
52
 
33
53
  sets :domain
34
54
  sets :realm
35
55
  sets :latest
56
+ sets :default_database
57
+ sets :default_adapter
36
58
 
37
59
  emits "WorldDeclared"
38
60
  end
@@ -55,6 +77,12 @@ Hecks.bluebook "World" do
55
77
  member word: "world", context: "File", body: "keywords", inner: "World", opens: "World", fills: ""
56
78
  member word: "realm", context: "World", body: "none", inner: "", opens: "", fills: "realm", calls: "realm_impl"
57
79
  member word: "latest", context: "World", body: "none", inner: "", opens: "", fills: "latest", calls: "latest_impl"
80
+ # THE TWO CROSS-CHAPTER DEFAULTS — the same connection string and the
81
+ # same persistence adapter every chapter of a project repeats, written
82
+ # once. A chapter's own world (`persisted_by ... database`) and its own
83
+ # hecksagon bind still win; see docs/implemented/reference/world.md.
84
+ member word: "default_database", context: "World", body: "none", inner: "", opens: "", fills: "default_database", calls: "default_database_impl"
85
+ member word: "default_adapter", context: "World", body: "none", inner: "", opens: "", fills: "default_adapter", calls: "default_adapter_impl"
58
86
 
59
87
  end
60
88
 
@@ -78,6 +106,8 @@ Hecks.bluebook "World" do
78
106
  member keyword: "world", context: "File", at: "1", named: "", kind: "text", required: "true", fills: "domain"
79
107
  member keyword: "realm", context: "World", at: "1", named: "", kind: "text", required: "true", fills: "realm"
80
108
  member keyword: "latest", context: "World", at: "1", named: "", kind: "text", required: "true", fills: "latest"
109
+ member keyword: "default_database", context: "World", at: "1", named: "", kind: "text", required: "true", fills: "default_database"
110
+ member keyword: "default_adapter", context: "World", at: "1", named: "", kind: "text", required: "true", fills: "default_adapter"
81
111
 
82
112
  end
83
113
  end
@@ -0,0 +1,176 @@
1
+ require_relative "runtime/errors"
2
+
3
+ module Hecks
4
+ # What one spawned `bin/hecks_mcp_door` may do: every tool against any domain under
5
+ # `Storehouse::BOOT_ROOT` (the default), or, in reader mode, only the reader tools
6
+ # against the domains its spawner named. ADR 0072, decision 2.
7
+ #
8
+ # ## Reach, not identity
9
+ #
10
+ # This is a capability allowlist, not authentication. It identifies no one and adds no
11
+ # secret: whoever can spawn the door chooses its mode, and whoever can write to its stdin
12
+ # gets whatever that mode allows. What it buys is a smaller surface per agent. A reader
13
+ # door cannot dispatch, cannot run `.behaviors` files, and cannot `Kernel.load` Ruby from
14
+ # a domain its spawner did not name.
15
+ #
16
+ # ## Why `HECKS_DOOR_*` and not `HECKS_MCP_*`
17
+ #
18
+ # `McpStdioGuard` refuses every `HECKS_MCP_*` variable except
19
+ # `HECKS_MCP_TRANSPORT=stdio`, and every argument except `--stdio`, so that no option can
20
+ # ask a server for another transport. Those refusals belong to ADR 0062. The settings
21
+ # here live under their own prefix so the guard stays exactly as that ADR describes it,
22
+ # and an unknown `HECKS_DOOR_*` name is refused here, the same fail-closed way.
23
+ #
24
+ # ## The settings
25
+ #
26
+ # - `HECKS_DOOR_TOOLS=readers` turns reader mode on. Unset means every tool, as before.
27
+ # Any other value refuses to start.
28
+ # - `HECKS_DOOR_DOMAINS` lists the domains a reader door may name, separated by
29
+ # `File::PATH_SEPARATOR` (`:` on Unix), each resolved against `Storehouse::BOOT_ROOT`.
30
+ # Required in reader mode and refused without it.
31
+ #
32
+ # ## The allowed set is named, not loaded
33
+ #
34
+ # The door boots a domain afresh on every call and keeps nothing loaded between calls,
35
+ # so there is no set of already-loaded domains to check against. The allowed set is the
36
+ # list the spawner named, resolved to absolute paths at startup. A call's `domain:` is
37
+ # resolved the same way and compared before anything boots, so a path outside the set
38
+ # never reaches `Kernel.load`. The comparison is on resolved path text: a symlink under a
39
+ # named path is followed when that domain boots, as it is for every domain today.
40
+ class McpDoorScope
41
+ ENV_PREFIX = "HECKS_DOOR_".freeze
42
+ TOOLS_VARIABLE = "HECKS_DOOR_TOOLS".freeze
43
+ DOMAINS_VARIABLE = "HECKS_DOOR_DOMAINS".freeze
44
+ READERS = "readers".freeze
45
+ EXIT_STATUS = 2
46
+
47
+ # The tools a reader door serves. Each reads a domain or the door's own audit log and
48
+ # writes no domain state. `query` and `state` still append their call to that audit
49
+ # log. `validate` boots the domain it is given, so it is held to the allowed set like
50
+ # every other domain-scoped tool. `domains` lists directories and loads nothing.
51
+ READER_TOOLS = %w[query events state catalog describe validate domains history follow].freeze
52
+
53
+ # Reads the door's mode from its environment.
54
+ #
55
+ # @param env [Hash{String => String}] the process environment
56
+ # @return [McpDoorScope] an unrestricted scope when no `HECKS_DOOR_*` variable is set;
57
+ # otherwise a reader scope over the named domains
58
+ # @raise [ArgumentError] with every problem found, one per line, when the settings are
59
+ # not a mode this door knows
60
+ # @raise [Runtime::TypeMismatch] if a named domain resolves outside
61
+ # `Storehouse::BOOT_ROOT`
62
+ def self.from_env(env = ENV)
63
+ problems = setting_problems(env)
64
+ raise ArgumentError, problems.join("\n") unless problems.empty?
65
+ return new(allowed_domains: nil) unless env[TOOLS_VARIABLE]
66
+
67
+ new(allowed_domains: env[DOMAINS_VARIABLE].split(File::PATH_SEPARATOR).reject(&:empty?))
68
+ end
69
+
70
+ # Reads the door's mode, or refuses to start on settings it does not know.
71
+ #
72
+ # Everything goes to `stderr`, because stdout carries the MCP protocol.
73
+ #
74
+ # @param server [String] the server's name, used as the line prefix
75
+ # @param env [Hash{String => String}] the process environment
76
+ # @param stderr [IO] where a refusal is written
77
+ # @return [McpDoorScope] the door's scope
78
+ # @raise [SystemExit] with status `EXIT_STATUS` when `from_env` refuses the settings
79
+ def self.start!(server:, env: ENV, stderr: $stderr)
80
+ from_env(env)
81
+ rescue ArgumentError, Runtime::TypeMismatch => e
82
+ e.message.each_line { |line| stderr.puts("#{server}: refusing to start: #{line.chomp}") }
83
+ exit(EXIT_STATUS)
84
+ end
85
+
86
+ # @api private
87
+ def self.setting_problems(env)
88
+ unknown = env.keys.select { |name| name.start_with?(ENV_PREFIX) } - [TOOLS_VARIABLE, DOMAINS_VARIABLE]
89
+ problems = unknown.map do |name|
90
+ "environment #{name} is not accepted; the door takes only #{TOOLS_VARIABLE} and #{DOMAINS_VARIABLE}"
91
+ end
92
+ problems + mode_problems(env[TOOLS_VARIABLE], env[DOMAINS_VARIABLE])
93
+ end
94
+
95
+ # @api private
96
+ def self.mode_problems(tools, domains)
97
+ if tools.nil?
98
+ domains.nil? ? [] : ["#{DOMAINS_VARIABLE} is set without #{TOOLS_VARIABLE}=#{READERS}; it only narrows a reader door"]
99
+ elsif tools != READERS
100
+ ["#{TOOLS_VARIABLE}=#{tools.inspect} is not accepted; the only value is #{READERS.inspect}"]
101
+ elsif domains.to_s.split(File::PATH_SEPARATOR).none? { |path| !path.empty? }
102
+ ["#{TOOLS_VARIABLE}=#{READERS} needs #{DOMAINS_VARIABLE}, the domains this door may read"]
103
+ else
104
+ []
105
+ end
106
+ end
107
+
108
+ # The named domains, resolved against `Storehouse::BOOT_ROOT`; nil when unrestricted.
109
+ #
110
+ # @return [Array<String>, nil]
111
+ attr_reader :allowed_domains
112
+
113
+ # @param allowed_domains [Array<String>, nil] the domains a reader door may name, as
114
+ # given; nil for an unrestricted door
115
+ # @raise [Runtime::TypeMismatch] if a named domain resolves outside `Storehouse::BOOT_ROOT`
116
+ def initialize(allowed_domains:)
117
+ @allowed_domains = allowed_domains&.map { |path| Storehouse.confine!(path, DOMAINS_VARIABLE) }.freeze
118
+ end
119
+
120
+ # Tells whether this door runs in reader mode.
121
+ #
122
+ # @return [Boolean] true when the spawner set `HECKS_DOOR_TOOLS=readers`
123
+ def restricted?
124
+ !@allowed_domains.nil?
125
+ end
126
+
127
+ # Tells whether this door serves a tool.
128
+ #
129
+ # @param name [String] the tool name from a `tools/call` request
130
+ # @return [Boolean] true for every name on an unrestricted door; on a reader door, true
131
+ # only for `READER_TOOLS`
132
+ def permits_tool?(name)
133
+ !restricted? || READER_TOOLS.include?(name)
134
+ end
135
+
136
+ # Builds the answer to a call for a tool this door does not serve.
137
+ #
138
+ # @param name [String] the tool name from a `tools/call` request
139
+ # @return [Hash{Symbol => Object}] `{ok: false, error:}`, the error naming the mode and
140
+ # the tools it serves
141
+ def refusal(name)
142
+ { ok: false,
143
+ error: "#{name.inspect} is refused: this door runs in reader mode (#{TOOLS_VARIABLE}=#{READERS}), " \
144
+ "which serves only #{READER_TOOLS.join(', ')}" }
145
+ end
146
+
147
+ # Checks a call's `domain:` against the allowed set, before anything boots.
148
+ #
149
+ # @param domain [String, nil] the caller's `domain:` argument
150
+ # @return [String, nil] `domain` unchanged, so an unrestricted door boots exactly what
151
+ # it did before
152
+ # @raise [Runtime::TypeMismatch] on a reader door, if `domain` resolves outside
153
+ # `Storehouse::BOOT_ROOT` or to a path its spawner did not name
154
+ def admit_domain!(domain)
155
+ return domain unless restricted?
156
+ return domain if @allowed_domains.include?(Storehouse.confine!(domain, "domain"))
157
+
158
+ raise Runtime::TypeMismatch,
159
+ "domain: #{domain.inspect} is refused: this door runs in reader mode (#{TOOLS_VARIABLE}=#{READERS}) " \
160
+ "and reads only #{DOMAINS_VARIABLE}: #{@allowed_domains.join(', ')}"
161
+ end
162
+
163
+ # The startup warning lines that describe this door's mode.
164
+ #
165
+ # @return [Array<String>] empty when unrestricted; on a reader door, what it serves,
166
+ # what it refuses, the domains it reads, and that it identifies no one
167
+ def notes
168
+ return [] unless restricted?
169
+
170
+ ["Reader mode (#{TOOLS_VARIABLE}=#{READERS}): serves #{READER_TOOLS.join(', ')}; " \
171
+ "refuses dispatch (with dry_run and steps), behaviors and every other tool.",
172
+ "domain: boots only #{DOMAINS_VARIABLE}: #{@allowed_domains.join(', ')}.",
173
+ "Reader mode limits reach and identifies no one; it is not authentication."]
174
+ end
175
+ end
176
+ end
@@ -47,7 +47,7 @@ module Hecks
47
47
  #
48
48
  # @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
49
49
  # dispatcher both the lookup query and the `Shred` dispatch run through
50
- # @param domain [String] the subject's own domain FQN, e.g. `"Lifeadelics::Registration"`
50
+ # @param domain [String] the subject's own domain FQN, e.g. `"Site::Registration"`
51
51
  # @param subject_id [String] the data subject to erase
52
52
  # @return [Boolean] true when a live key was found and shredded by this call; false
53
53
  # when no `SubjectKey` record exists for this subject, or it was already shredded
@@ -308,7 +308,7 @@ module Hecks
308
308
  #
309
309
  # @param id [Integer] the row id assigned by `outbox_enqueue`
310
310
  # @return [Boolean] true when this call claimed the row; false when it is missing or
311
- # no longer pending
311
+ # not pending
312
312
  def outbox_claim(id) = @adapter.outbox_claim(id)
313
313
 
314
314
  # Records the final status of a claimed outbox row.
@@ -15,6 +15,13 @@ module Hecks
15
15
  # A domain with no hecksagon gets the default in-memory bind. A domain that has one
16
16
  # must bind the aggregate exactly once without a role.
17
17
  #
18
+ # A `default_adapter` a world declares (`Registry#default_adapter_for` — the
19
+ # chapter's own world, else the project's) stands in wherever the hecksagon binds
20
+ # nothing for the aggregate, and replaces the in-memory fallback for a domain
21
+ # with no hecksagon. It is never consulted once the hecksagon binds the aggregate
22
+ # or declares a domain-level default of its own, so an explicit chapter decision
23
+ # always wins.
24
+ #
18
25
  # @param registry [Runtime::Registry] the registry holding the domain's hecksagon
19
26
  # @param domain [String, Symbol] name of the domain the aggregate belongs to
20
27
  # @param aggregate [Bluebook::Aggregate] the aggregate whose binding is wanted
@@ -23,10 +30,12 @@ module Hecks
23
30
  # @raise [Runtime::WiringError] if the hecksagon has no `persisted_by` bind for the
24
31
  # aggregate, more or fewer than one bind without a role, or any bind with a role
25
32
  def resolve(registry, domain, aggregate)
26
- hexagon = registry.hecksagon(domain)
27
- return default_binding(aggregate) unless hexagon
33
+ hexagon = registry.hecksagon(domain)
34
+ declared = registry.default_adapter_for(domain)
35
+ return default_binding(aggregate, declared || DEFAULT_ADAPTER) unless hexagon
28
36
 
29
37
  bindings = hexagon.binds_for(aggregate.hecks_name, VERB)
38
+ bindings = [default_binding(aggregate, declared)] if bindings.empty? && declared
30
39
  raise missing_binding(domain, aggregate) if bindings.empty?
31
40
 
32
41
  authoritative = bindings.select { |bind| bind.role.nil? || bind.role.empty? }
@@ -39,9 +48,11 @@ module Hecks
39
48
  # Builds the bind an aggregate gets when its domain declares no hecksagon.
40
49
  #
41
50
  # @param aggregate [Bluebook::Aggregate] the aggregate to bind
42
- # @return [Bluebook::Bind] a roleless `persisted_by` bind to `DEFAULT_ADAPTER`
43
- def default_binding(aggregate)
44
- Bluebook::Bind.new(aggregate: aggregate.hecks_name, verb: VERB, adapter: DEFAULT_ADAPTER)
51
+ # @param adapter [String] the adapter to bind it to; the framework's in-memory
52
+ # default unless a world declared one
53
+ # @return [Bluebook::Bind] a roleless `persisted_by` bind to `adapter`
54
+ def default_binding(aggregate, adapter = DEFAULT_ADAPTER)
55
+ Bluebook::Bind.new(aggregate: aggregate.hecks_name, verb: VERB, adapter: adapter)
45
56
  end
46
57
 
47
58
  # Builds, without raising, the error for an aggregate a hecksagon leaves unbound.
@@ -4,6 +4,7 @@ require_relative "lineage"
4
4
  require_relative "../../../../naming"
5
5
  require_relative "../../../../framework"
6
6
  require_relative "../../../../runtime/registry"
7
+ require_relative "expected_era"
7
8
 
8
9
  module Hecks
9
10
  module Runtime
@@ -122,7 +123,7 @@ module Hecks
122
123
  #
123
124
  # The domain's own directory first, matched by name — a real app's
124
125
  # directory may hold more than one file once `uses_framework`
125
- # exists, so ".first" alone can no longer be trusted, the exact
126
+ # exists, so ".first" alone cannot be trusted, the exact
126
127
  # way it silently wasn't the day this was found: three domains
127
128
  # booted together, one real source text read once (the domain's
128
129
  # own, ".first"'d), and every other domain's era-1 held that text
@@ -295,7 +296,7 @@ module Hecks
295
296
  "it could be found (checked #{directory.inspect} and the framework registry)"
296
297
  end
297
298
 
298
- settings = registry.world(bluebook.name)&.for_binding(Ports::Persistence::VERB, adapter_name) || {}
299
+ settings = registry.binding_settings(bluebook.name, Ports::Persistence::VERB, adapter_name)
299
300
  registry.adapter_class(adapter_name).era_check!(
300
301
  registry: registry, bluebook: bluebook, current_text: current_text, settings: settings,
301
302
  directory: directory
@@ -131,7 +131,7 @@ module Hecks
131
131
  # Normal parse first, shadow only as a fallback — never shadow-parsing
132
132
  # unconditionally. A handful of DSL
133
133
  # defaults fork on `MetaValidator.shadow_parsing?` for a reason
134
- # that has nothing to do with syntax the live grammar can no longer
134
+ # that has nothing to do with syntax the live grammar cannot
135
135
  # read at all (`identified_by { }`, `belongs_to`, `has_one`,
136
136
  # `has_many` — genuinely removed spellings, exactly what shadow-
137
137
  # parsing exists to keep readable): `reference_to`'s own default
@@ -158,7 +158,7 @@ module Hecks
158
158
  # interpretation, the same one `label_of`/`mint_hash` on the same
159
159
  # source text always computes, whoever's asking. Only on a
160
160
  # `Malformed` refusal — the one signal that actually means "this
161
- # spelling doesn't exist anymore" — fall back to the legacy
161
+ # spelling is not in the live grammar" — fall back to the legacy
162
162
  # grammar. Any other exception (a
163
163
  # genuine syntax error, an unrelated validation refusal) propagates
164
164
  # unchanged; swallowing it here to retry under shadow mode would
@@ -5,7 +5,7 @@ require_relative "storage_shape"
5
5
 
6
6
  module Hecks
7
7
  module Runtime
8
- # The refusal wording for a held era whose digest no longer matches its
8
+ # The refusal wording for a held era whose digest does not match its
9
9
  # frozen text — the digest mismatch alone is what detects tampering (a
10
10
  # plain SHA256 comparison over raw bytes, unrelated to any of this);
11
11
  # this only supplies the wording once that's already fired.
@@ -28,7 +28,7 @@ module Hecks
28
28
  module EraTamper
29
29
  module_function
30
30
 
31
- # Words the boot refusal for a held era text whose digest no longer matches.
31
+ # Words the boot refusal for a held era text whose digest does not match.
32
32
  #
33
33
  # @param domain [String] name of the domain whose era was edited
34
34
  # @param ordinal [Integer] the edited era's ordinal in `hecks_eras`