hecks 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +65 -0
  3. data/lib/hecks/adapters/driven/folder.rb +73 -0
  4. data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
  5. data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
  6. data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
  7. data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
  8. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  9. data/lib/hecks/adapters/driven/heki.rb +93 -2
  10. data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
  12. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  13. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  14. data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
  15. data/lib/hecks/adapters/driven/lambda.rb +43 -2
  16. data/lib/hecks/adapters/driven/local_storage.rb +67 -1
  17. data/lib/hecks/adapters/driven/memory.rb +13 -13
  18. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
  19. data/lib/hecks/adapters/driven/prism.rb +46 -7
  20. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  21. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  22. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  23. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  24. data/lib/hecks/adapters/driven.rb +2 -0
  25. data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
  26. data/lib/hecks/behaviors/dsl.rb +58 -0
  27. data/lib/hecks/behaviors/expectations.rb +168 -7
  28. data/lib/hecks/behaviors/ir.rb +11 -0
  29. data/lib/hecks/behaviors/rspec.rb +8 -0
  30. data/lib/hecks/behaviors/runner.rb +19 -0
  31. data/lib/hecks/behaviors.rb +8 -0
  32. data/lib/hecks/bluebook/aggregate.rb +30 -0
  33. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
  34. data/lib/hecks/bluebook/assembly/build.rb +25 -4
  35. data/lib/hecks/bluebook/assembly/contract.rb +66 -14
  36. data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
  37. data/lib/hecks/bluebook/assembly/marks.rb +143 -14
  38. data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
  39. data/lib/hecks/bluebook/assembly.rb +25 -7
  40. data/lib/hecks/bluebook/attribute.rb +17 -3
  41. data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
  42. data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
  43. data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
  44. data/lib/hecks/bluebook/behaviour/command.rb +33 -3
  45. data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
  46. data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
  47. data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
  48. data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
  49. data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
  50. data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
  51. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  52. data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
  53. data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
  54. data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
  55. data/lib/hecks/bluebook/chapter.rb +23 -0
  56. data/lib/hecks/bluebook/command.rb +53 -8
  57. data/lib/hecks/bluebook/domain_port.rb +25 -0
  58. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
  59. data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
  60. data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
  62. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
  63. data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
  64. data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
  65. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
  66. data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
  67. data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
  68. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
  69. data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
  70. data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
  71. data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
  72. data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
  73. data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
  74. data/lib/hecks/bluebook/entity.rb +29 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
  77. data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
  78. data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
  79. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
  80. data/lib/hecks/bluebook/expression/resolver.rb +320 -77
  81. data/lib/hecks/bluebook/hexagon.rb +34 -0
  82. data/lib/hecks/bluebook/lifecycle.rb +11 -0
  83. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
  84. data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
  85. data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
  86. data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
  88. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
  90. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
  91. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
  92. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
  93. data/lib/hecks/bluebook/meta_validator.rb +180 -84
  94. data/lib/hecks/bluebook/model_check.rb +268 -24
  95. data/lib/hecks/bluebook/pattern_subset.rb +23 -1
  96. data/lib/hecks/bluebook/process_manager.rb +13 -0
  97. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  98. data/lib/hecks/bluebook/project_loader.rb +40 -0
  99. data/lib/hecks/bluebook/project_register.rb +44 -0
  100. data/lib/hecks/bluebook/query.rb +27 -0
  101. data/lib/hecks/bluebook/read_model.rb +21 -1
  102. data/lib/hecks/bluebook/reference.rb +21 -8
  103. data/lib/hecks/bluebook/smoke_test.rb +29 -6
  104. data/lib/hecks/bluebook/synthesizer.rb +34 -0
  105. data/lib/hecks/bluebook/translation.rb +30 -1
  106. data/lib/hecks/bluebook/value_object.rb +23 -5
  107. data/lib/hecks/bluebook.rb +3 -4
  108. data/lib/hecks/codemod.rb +107 -20
  109. data/lib/hecks/construct.rb +15 -1
  110. data/lib/hecks/corpus.rb +146 -25
  111. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  112. data/lib/hecks/doc/reference.rb +185 -16
  113. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  114. data/lib/hecks/facade/handle.rb +76 -3
  115. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  116. data/lib/hecks/forms/field_shape.rb +3 -0
  117. data/lib/hecks/forms/page.rb +14 -0
  118. data/lib/hecks/forms/port_argument.rb +12 -0
  119. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  120. data/lib/hecks/forms/record_renderer.rb +58 -0
  121. data/lib/hecks/forms/record_table.rb +27 -0
  122. data/lib/hecks/forms/reference_options.rb +24 -0
  123. data/lib/hecks/forms/value_object_shape.rb +10 -0
  124. data/lib/hecks/fqn.rb +58 -0
  125. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  126. data/lib/hecks/framework/oidc.json +15 -0
  127. data/lib/hecks/framework.rb +43 -20
  128. data/lib/hecks/freezer.rb +17 -1
  129. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  130. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  131. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  132. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  133. data/lib/hecks/fuzzing/differential.rb +34 -0
  134. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  135. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  136. data/lib/hecks/fuzzing/form_census.rb +86 -0
  137. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  138. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  139. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  140. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  141. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  142. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  143. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  144. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  145. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  146. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  147. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  148. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  149. data/lib/hecks/fuzzing/properties.rb +24 -15
  150. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  151. data/lib/hecks/fuzzing/replay.rb +137 -29
  152. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  153. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  154. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  155. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  156. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  157. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  158. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  159. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  160. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  161. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  162. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  163. data/lib/hecks/grammar/evolve.rb +178 -2
  164. data/lib/hecks/grammar.rb +46 -0
  165. data/lib/hecks/ir.rb +38 -7
  166. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  167. data/lib/hecks/literal.rb +32 -0
  168. data/lib/hecks/naming.rb +88 -7
  169. data/lib/hecks/ports/access_control.rb +5 -10
  170. data/lib/hecks/ports/authorization.rb +3 -6
  171. data/lib/hecks/ports/identity_assignment.rb +1 -2
  172. data/lib/hecks/ports/identity_resolution.rb +1 -2
  173. data/lib/hecks/ports/key_vault.port +6 -0
  174. data/lib/hecks/ports/key_vault.rb +58 -0
  175. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  176. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  177. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  178. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  179. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  180. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  182. data/lib/hecks/ports/query/in_memory.rb +39 -6
  183. data/lib/hecks/ports/query/ordering.rb +15 -0
  184. data/lib/hecks/ports.rb +1 -0
  185. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  186. data/lib/hecks/projections/diagrams.rb +243 -7
  187. data/lib/hecks/projections/glossary/html.rb +88 -0
  188. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  189. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  190. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  191. data/lib/hecks/projections/glossary.rb +161 -19
  192. data/lib/hecks/projections/model/deviations.rb +44 -0
  193. data/lib/hecks/projections/model.rb +51 -1
  194. data/lib/hecks/projections/oidc.rb +40 -3
  195. data/lib/hecks/projections/parser_table.rb +49 -8
  196. data/lib/hecks/projections/reference.rb +12 -0
  197. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  198. data/lib/hecks/projections/shape.rb +8 -0
  199. data/lib/hecks/projections/statements.rb +63 -16
  200. data/lib/hecks/projections/vocabulary.rb +17 -0
  201. data/lib/hecks/projector/cli_projector.rb +218 -10
  202. data/lib/hecks/projector/docs_projector.rb +145 -19
  203. data/lib/hecks/projector/exporter.rb +65 -11
  204. data/lib/hecks/projector/ir_projector.rb +6 -0
  205. data/lib/hecks/projector/narrate_projector.rb +136 -15
  206. data/lib/hecks/projector/target.rb +29 -5
  207. data/lib/hecks/projector.rb +74 -6
  208. data/lib/hecks/query_ir.rb +47 -0
  209. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  210. data/lib/hecks/rendering.rb +6 -0
  211. data/lib/hecks/router/namespace_installer.rb +13 -0
  212. data/lib/hecks/router.rb +55 -0
  213. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  214. data/lib/hecks/runtime/boot_gates.rb +18 -0
  215. data/lib/hecks/runtime/caller.rb +32 -0
  216. data/lib/hecks/runtime/capability_graph.rb +11 -0
  217. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  218. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  219. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  220. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  221. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  222. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  223. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  224. data/lib/hecks/runtime/dispatcher.rb +28 -50
  225. data/lib/hecks/runtime/entity_element.rb +161 -8
  226. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  227. data/lib/hecks/runtime/errors.rb +18 -4
  228. data/lib/hecks/runtime/event.rb +10 -5
  229. data/lib/hecks/runtime/identity.rb +71 -3
  230. data/lib/hecks/runtime/instance.rb +67 -7
  231. data/lib/hecks/runtime/interpreting.rb +13 -5
  232. data/lib/hecks/runtime/invocation.rb +118 -36
  233. data/lib/hecks/runtime/loader.rb +94 -8
  234. data/lib/hecks/runtime/outbox.rb +145 -7
  235. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  236. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  237. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  238. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  239. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  240. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  241. data/lib/hecks/runtime/reference_hop.rb +42 -0
  242. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  243. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  244. data/lib/hecks/runtime/registry/verification.rb +119 -4
  245. data/lib/hecks/runtime/registry.rb +157 -4
  246. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  247. data/lib/hecks/runtime/routing.rb +27 -2
  248. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  249. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  250. data/lib/hecks/runtime/tenant_check.rb +26 -6
  251. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  252. data/lib/hecks/runtime/value/coercion.rb +255 -33
  253. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  254. data/lib/hecks/runtime/value.rb +50 -0
  255. data/lib/hecks/runtime.rb +32 -0
  256. data/lib/hecks/storehouse.rb +305 -9
  257. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  258. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  259. data/lib/hecks/version.rb +1 -1
  260. data/lib/hecks.rb +79 -1
  261. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  262. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  263. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  264. metadata +11 -5
  265. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  266. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  267. data/lib/hecks/deprecation.rb +0 -95
@@ -19,7 +19,7 @@ module Hecks
19
19
  GENERATED_END = "<!-- generated:end -->".freeze
20
20
  TODO_SENTINEL = "<!-- TODO: document this word -->".freeze
21
21
 
22
- # A PAGE'S own hand-written opening, harvested under a key no word
22
+ # A page's own hand-written opening, harvested under a key no word
23
23
  # can ever collide with (words are strings off the Syntax chapter;
24
24
  # this is a Symbol). It exists so a page can boot once — load a real
25
25
  # corpus chapter, wire its hexagon — and have every word's example
@@ -31,18 +31,36 @@ module Hecks
31
31
 
32
32
  module_function
33
33
 
34
+ # The marker opening one word's generated region.
35
+ #
36
+ # @param word [String, Symbol, #to_s] the word this section documents
37
+ # @return [String] the HTML comment marking that word's generated region open
34
38
  def generated_begin(word) = "<!-- generated:begin word=#{word} -->"
35
39
 
36
40
  # Keyed by region rather than by word — the same marker convention,
37
41
  # used for the parts of a page that are not about one word: a
38
42
  # page's generated lede here, README's generated indexes below.
43
+ #
44
+ # @param id [String, Symbol, #to_s] the region's id, such as `"page"` or `"tools"`
45
+ # @return [String] the HTML comment marking that region's generated region open
39
46
  def region_begin(id) = "<!-- generated:begin id=#{id} -->"
40
47
 
48
+ # The language's own Syntax aggregate, read off the judged grammar chapter.
49
+ #
50
+ # @return [Bluebook::Aggregate, nil] the Syntax aggregate, or nil if the
51
+ # grammar's Bluebook chapter declares none by that name
41
52
  def syntax
42
53
  meta = Bluebook::MetaValidator.grammar_registry.bluebook("Bluebook")
43
54
  meta.aggregates.find { |aggregate| aggregate.hecks_name == "Syntax" }
44
55
  end
45
56
 
57
+ # Reads one closed-set value object's declared members off the Syntax
58
+ # aggregate, as string-valued Hashes.
59
+ #
60
+ # @param name [String, Symbol, #to_s] the value object's `hecks_name`,
61
+ # such as `"Keyword"`
62
+ # @return [Array<Hash{Symbol => String}>] each declared member's fields,
63
+ # values stringified
46
64
  def rows(name)
47
65
  syntax.value_objects.find { |vo| vo.hecks_name == name }
48
66
  .members.map { |row| row.to_h.transform_values(&:to_s) }
@@ -53,31 +71,60 @@ module Hecks
53
71
  # really is a lifecycle. `SyntaxBoot.call` discovers the static
54
72
  # aggregate-local seed rows (`KeywordSeed`/`ArgumentSeed`), dispatches each
55
73
  # one through the real admission/lifecycle door, and hands back the
56
- # same shape `rows` used to produce — nothing below this needed to
57
- # change.
58
- #
59
- # No separate `@keywords ||=` here anymore. This module used to
60
- # memoize its own copy on top of `SyntaxBoot.call`'s own memo — a
61
- # double cache with no way to invalidate either half, and a real
62
- # bug: whichever call in the whole process happened to land first
63
- # got locked in forever, even one caught mid-build missing every
64
- # Paging-attached word (limit/offset/cursor/nulls). `SyntaxBoot.call`
65
- # now carries the one cache that matters (keyed on the grammar
66
- # registry's own chapter set — see its comment) ; this delegates
67
- # straight through instead of shadowing it.
74
+ # same shape `rows`, above, already produces — nothing below this
75
+ # needed to change.
76
+ #
77
+ # This module holds no separate `@keywords ||=` memo of its own —
78
+ # delegates straight through to `SyntaxBoot.call`'s own cache
79
+ # (keyed on the grammar registry's own chapter set — see its
80
+ # comment) rather than shadowing it. A second, local cache on top
81
+ # of that one would double-cache with no way to invalidate either
82
+ # half: whichever call happened to land first would lock in
83
+ # forever, even one caught mid-build missing every Paging-attached
84
+ # word (limit/offset/cursor/nulls).
85
+ #
86
+ # @return [Array<Hash{Symbol => String}>] every declared Keyword row
68
87
  def keywords = Bluebook::MetaValidator::SyntaxBoot.call[:keywords]
88
+
89
+ # Every declared Argument row.
90
+ #
91
+ # @return [Array<Hash{Symbol => String}>] every declared Argument row
69
92
  def arguments = Bluebook::MetaValidator::SyntaxBoot.call[:arguments]
70
93
 
94
+ # Reads a row's declared status, defaulting when it declared none.
95
+ #
96
+ # @param row [Hash{Symbol => String}] a Keyword or Argument row
97
+ # @return [String] the row's declared status, defaulting to `"admitted"`
98
+ # when it declared none
71
99
  def status_of(row) = row[:status].to_s.empty? ? "admitted" : row[:status].to_s
100
+
101
+ # Whether a row is still current enough to appear in the reference.
102
+ #
103
+ # @param row [Hash{Symbol => String}] a Keyword or Argument row
104
+ # @return [Boolean] true if the row's status is `"admitted"` or `"deprecated"`
72
105
  def live?(row) = %w[admitted deprecated].include?(status_of(row))
73
106
 
107
+ # Every distinct context a keyword is declared in.
108
+ #
109
+ # @return [Array<String>] every distinct context a keyword is declared in
74
110
  def contexts = keywords.map { |row| row[:context] }.uniq
75
111
 
112
+ # Derives a context's reference page filename.
113
+ #
114
+ # @param context [String, Symbol, #to_s] a context name, such as `"File"`
115
+ # @return [String] the reference page's filename for that context
76
116
  def page_name(context) = "#{Naming.snake(context)}.md"
77
117
 
78
118
  # Every reference page, rendered fresh — prose carried over from
79
119
  # the committed pages, new words seeded with the sentinel, orphaned
80
120
  # prose refused.
121
+ #
122
+ # @param directory [String] path to the directory holding the committed
123
+ # reference pages, read for their hand-written prose
124
+ # @return [Hash{String => String}] every page's filename (plus `"index.md"`)
125
+ # mapped to its freshly rendered Markdown content
126
+ # @raise [RuntimeError] if a committed page carries prose for a word the
127
+ # language no longer declares in that context
81
128
  def pages(directory)
82
129
  contexts.each_with_object({}) do |context, pages|
83
130
  path = File.join(directory, page_name(context))
@@ -86,13 +133,22 @@ module Hecks
86
133
  end.merge("index.md" => render_index)
87
134
  end
88
135
 
89
- # A WORD admitting two forms has two rows — syntax.bluebook's own
136
+ # A word admitting two forms has two rows — syntax.bluebook's own
90
137
  # stated rule, and `identified_by` (a block, or a bare argument and
91
138
  # none) is the case that made it real again. One section per word all
92
139
  # the same: the prose is the word's rather than the form's, and the
93
140
  # argument rows join by (word, context) and so already cover every
94
141
  # form. Grouped rather than rendered per row, or a reader would meet
95
142
  # the same heading and the same paragraph twice.
143
+ #
144
+ # @param context [String] the context name, such as `"File"`
145
+ # @param prose [Hash{String, Symbol => String}] hand-written prose harvested
146
+ # from the committed page, keyed by word (or `PREAMBLE` for the page's lede)
147
+ # @param path [String] the page's file path, used only in the orphan-refusal
148
+ # message below
149
+ # @return [String] the page's full rendered Markdown
150
+ # @raise [RuntimeError] if `prose` carries a key for a word the language no
151
+ # longer declares in `context`
96
152
  def render_page(context, prose, path)
97
153
  words = keywords.select { |row| row[:context] == context }.group_by { |row| row[:word] }
98
154
  orphans = prose.keys - words.keys - [PREAMBLE]
@@ -119,6 +175,10 @@ module Hecks
119
175
  PAGE
120
176
  end
121
177
 
178
+ # The one-line description of where a context's words are typed.
179
+ #
180
+ # @param context [String] the context name, such as `"File"` or `"Command"`
181
+ # @return [String] a sentence naming where words in this context are typed
122
182
  def context_lede(context)
123
183
  openers = keywords.select { |row| row[:opens] == context }
124
184
  return "Words available at the top of a file." if context == "File"
@@ -131,6 +191,12 @@ module Hecks
131
191
  # One spelling per form, everything else off the first row — the
132
192
  # columns that differ between two forms of one word are `body` (which
133
193
  # is what the spelling shows) and nothing else.
194
+ #
195
+ # @param forms [Array<Hash{Symbol => String}>] one word's Keyword rows, one
196
+ # per admitted form
197
+ # @param prose [String, nil] the word's hand-written prose, or nil if none
198
+ # was harvested
199
+ # @return [String] the word's rendered section, generated table plus prose
134
200
  def render_word(forms, prose)
135
201
  row = forms.first
136
202
  table = argument_table(row)
@@ -152,15 +218,30 @@ module Hecks
152
218
  WORD
153
219
  end
154
220
 
221
+ # Falls back to the TODO sentinel when a word has no prose yet.
222
+ #
223
+ # @param prose [String, nil] a word's hand-written prose, or nil if none
224
+ # was harvested
225
+ # @return [String] `prose` stripped, or the TODO sentinel if it is blank
155
226
  def prose_or_sentinel(prose)
156
227
  text = prose.to_s.strip
157
228
  text.empty? ? TODO_SENTINEL : text
158
229
  end
159
230
 
231
+ # Finds the Argument rows declared for one Keyword row's word.
232
+ #
233
+ # @param row [Hash{Symbol => String}] a Keyword row
234
+ # @return [Array<Hash{Symbol => String}>] the Argument rows declared for
235
+ # this row's word, in this row's context
160
236
  def word_arguments(row)
161
237
  arguments.select { |arg| arg[:keyword] == row[:word] && arg[:context] == row[:context] }
162
238
  end
163
239
 
240
+ # Builds the call spelling shown for one word.
241
+ #
242
+ # @param row [Hash{Symbol => String}] a Keyword row
243
+ # @return [String] the word's call spelling, positional arguments then
244
+ # named ones, with a trailing `do ... end` unless its body is `"none"`
164
245
  def signature(row)
165
246
  positional = word_arguments(row).reject { |arg| arg[:at].to_s.empty? }
166
247
  .sort_by { |arg| arg[:at].to_i }
@@ -172,6 +253,11 @@ module Hecks
172
253
  row[:body].to_s == "none" ? base : "#{base} do ... end"
173
254
  end
174
255
 
256
+ # Renders one word's arguments as a Markdown table.
257
+ #
258
+ # @param row [Hash{Symbol => String}] a Keyword row
259
+ # @return [String] a Markdown table of the row's arguments, or `""` if it
260
+ # declares none
175
261
  def argument_table(row)
176
262
  args = word_arguments(row)
177
263
  return "" if args.empty?
@@ -184,6 +270,10 @@ module Hecks
184
270
  "#{lines.join("\n")}\n"
185
271
  end
186
272
 
273
+ # Renders the reference index page.
274
+ #
275
+ # @return [String] the reference index page's rendered Markdown, one
276
+ # linked entry per context
187
277
  def render_index
188
278
  listed = contexts.map do |context|
189
279
  count = keywords.select { |row| row[:context] == context }.map { |row| row[:word] }.uniq.size
@@ -204,7 +294,7 @@ module Hecks
204
294
  # Prose keyed by word: everything between a section's generated
205
295
  # region and the next `## ` heading (or end of file).
206
296
  #
207
- # Starts on PREAMBLE rather than nil so the text between the PAGE's
297
+ # Starts on `PREAMBLE` rather than nil so the text between the page's
208
298
  # own generated lede and its first word heading is carried over too
209
299
  # instead of being silently dropped. A page written before that
210
300
  # region existed has no generated marker ahead of its first `## `,
@@ -214,6 +304,11 @@ module Hecks
214
304
  # buffer/in_fence) — each branch mutates shared local state that
215
305
  # carries into the next iteration, so splitting per branch would
216
306
  # mean passing all four back and forth by reference every line.
307
+ #
308
+ # @param text [String] a committed reference page's full Markdown source
309
+ # @return [Hash{String, Symbol => String}] hand-written prose, keyed by
310
+ # word (or `PREAMBLE` for the page's lede); empty and TODO-sentinel-only
311
+ # entries are dropped
217
312
  # rubocop:disable-next Metrics/PerceivedComplexity
218
313
  def harvest(text)
219
314
  prose = {}
@@ -246,6 +341,13 @@ module Hecks
246
341
  prose.reject { |_word, text_| text_.empty? || text_ == TODO_SENTINEL }
247
342
  end
248
343
 
344
+ # Renders every reference page and writes each to `directory`.
345
+ #
346
+ # @param directory [String] path to the directory to write pages into,
347
+ # created if it does not exist
348
+ # @return [void]
349
+ # @raise [RuntimeError] if a committed page carries prose for a word the
350
+ # language no longer declares in that context
249
351
  def write!(directory)
250
352
  FileUtils.mkdir_p(directory)
251
353
  pages(directory).each do |name, content|
@@ -257,6 +359,10 @@ module Hecks
257
359
  # reference page, keyed by region id instead of a word, so the
258
360
  # index a reader lands on first can't drift from what actually
259
361
  # exists on disk either.
362
+ #
363
+ # @param root [String] the repository root
364
+ # @return [Hash{String => String}] each region id mapped to its freshly
365
+ # rendered content
260
366
  def readme_regions(root)
261
367
  {
262
368
  "guides" => guide_index(root),
@@ -267,6 +373,11 @@ module Hecks
267
373
  }
268
374
  end
269
375
 
376
+ # Lists every committed guide, linked and titled by its own heading.
377
+ #
378
+ # @param root [String] the repository root
379
+ # @return [String] a Markdown list linking every committed guide, titled
380
+ # by its own `# ` heading
270
381
  def guide_index(root)
271
382
  paths = Dir.glob(File.join(root, "docs/implemented/guides/*.md"))
272
383
  .reject { |p| %w[AUTHORING.md].include?(File.basename(p)) }
@@ -278,6 +389,11 @@ module Hecks
278
389
  lines.join("\n")
279
390
  end
280
391
 
392
+ # Links the reference index, with its context count.
393
+ #
394
+ # @param _root [String] unused; kept for the shape `readme_regions` calls
395
+ # every region renderer with
396
+ # @return [String] one line linking the reference index, with its context count
281
397
  def reference_index(_root)
282
398
  count = contexts.size
283
399
  "[The DSL reference](docs/implemented/reference/index.md) — #{count} contexts, generated from " \
@@ -285,6 +401,11 @@ module Hecks
285
401
  "`spec/reference_golden_spec.rb`."
286
402
  end
287
403
 
404
+ # Lists every `bin/` script that opens with a comment, one row each.
405
+ #
406
+ # @param root [String] the repository root
407
+ # @return [String] a Markdown table of every `bin/` script with an opening
408
+ # comment, one row each
288
409
  def tool_table(root)
289
410
  scripts = Dir.glob(File.join(root, "bin/*")).select { |p| File.file?(p) }.sort
290
411
  rows = scripts.filter_map { |path| [path, tool_summary(path)] }.select { |_, desc| desc }
@@ -298,6 +419,10 @@ module Hecks
298
419
  # little long and says "...". A code-bearing comment (`field.name`,
299
420
  # `pattern:`) makes naive sentence-splitting on "." or ":" cut in
300
421
  # the wrong place, so this truncates on length alone.
422
+ #
423
+ # @param path [String] a `bin/` script's path
424
+ # @return [String, nil] its opening comment paragraph, truncated to 140
425
+ # characters, or nil if the script opens with no comment
301
426
  def tool_summary(path)
302
427
  comment_lines = []
303
428
  started = false
@@ -324,6 +449,10 @@ module Hecks
324
449
  # own drift check; this just quotes its own output, so the two
325
450
  # can't independently drift from each other either — a stale
326
451
  # Order_lifecycle.mmd fails that spec long before this one runs.
452
+ #
453
+ # @param root [String] the repository root
454
+ # @return [String] a Markdown section showcasing the generated diagram
455
+ # tooling, quoting `examples/pizzas`'s own committed Order lifecycle diagram
327
456
  def diagram_showcase(root)
328
457
  lifecycle = File.read(File.join(root, "docs/generated/diagrams/pizzas/Order_lifecycle.mmd")).strip
329
458
  <<~MARKDOWN.strip
@@ -337,6 +466,11 @@ module Hecks
337
466
  MARKDOWN
338
467
  end
339
468
 
469
+ # Lists every example domain, with its own declared vision.
470
+ #
471
+ # @param root [String] the repository root
472
+ # @return [String] a Markdown list of every example domain with a
473
+ # `.bluebook` file, each with its own declared `vision` text
340
474
  def corpus_roster(root)
341
475
  dirs = Dir.glob(File.join(root, "examples/*/"))
342
476
  lines = dirs.filter_map do |dir|
@@ -351,6 +485,12 @@ module Hecks
351
485
  lines.join("\n")
352
486
  end
353
487
 
488
+ # Replaces every generated region inside `text` with its freshly rendered
489
+ # content, leaving the hand-written parts of README untouched.
490
+ #
491
+ # @param root [String] the repository root
492
+ # @param text [String] the README's current full text
493
+ # @return [String] the README's text, with each generated region refreshed
354
494
  def render_readme(root, text)
355
495
  readme_regions(root).reduce(text) do |current, (id, content)|
356
496
  pattern = /#{Regexp.escape(region_begin(id))}.*?#{Regexp.escape(GENERATED_END)}/m
@@ -358,6 +498,10 @@ module Hecks
358
498
  end
359
499
  end
360
500
 
501
+ # Regenerates README's generated regions in place.
502
+ #
503
+ # @param root [String] the repository root
504
+ # @return [void]
361
505
  def write_readme!(root)
362
506
  path = File.join(root, "README.md")
363
507
  File.write(path, render_readme(root, File.read(path)))
@@ -371,9 +515,14 @@ module Hecks
371
515
  # gate exists to refuse.
372
516
  EXAMPLE_FENCE = /^```ruby(?: bluebook| boot)?[ \t]*$/
373
517
 
518
+ # Whether a word's prose carries a runnable example.
519
+ #
520
+ # @param prose [String, nil] a word's hand-written prose, or nil
521
+ # @return [Boolean] true if `prose` contains a runnable `ruby` or
522
+ # `ruby bluebook`/`ruby boot` fenced example
374
523
  def exemplified?(prose) = prose.to_s.match?(EXAMPLE_FENCE)
375
524
 
376
- # Every live WORD, paired with its prose. Both coverage gates ask a
525
+ # Every live word, paired with its prose. Both coverage gates ask a
377
526
  # question about this same walk and differ only in what they ask of
378
527
  # the prose, so they share it rather than each re-deriving the page
379
528
  # set — the two are meant to move together, and one drifting past
@@ -382,6 +531,12 @@ module Hecks
382
531
  #
383
532
  # `harvest` already rejects empty prose and the TODO sentinel, so a
384
533
  # word with nothing written for it arrives here with a nil.
534
+ #
535
+ # @param directory [String] path to the directory holding the committed
536
+ # reference pages
537
+ # @return [Array(String, String, String), Array(String, String, nil)]
538
+ # `[word, context, prose]` for every live (admitted or deprecated) word,
539
+ # `prose` nil if none was harvested
385
540
  def live_words(directory)
386
541
  rows = contexts.flat_map do |context|
387
542
  path = File.join(directory, page_name(context))
@@ -392,9 +547,18 @@ module Hecks
392
547
  rows.uniq { |word, context, _| [word, context] }
393
548
  end
394
549
 
550
+ # Disambiguates a word by the context it is declared in.
551
+ #
552
+ # @param word [String] a word
553
+ # @param context [String] the context it is declared in
554
+ # @return [String] the word, disambiguated by its context
395
555
  def name_of(word, context) = "#{word} (#{context})"
396
556
 
397
557
  # The coverage gate's question: every live word with no prose yet.
558
+ #
559
+ # @param directory [String] path to the directory holding the committed
560
+ # reference pages
561
+ # @return [Array<String>] each undocumented live word, named by `name_of`
398
562
  def undocumented(directory)
399
563
  live_words(directory).reject { |_word, _context, prose| prose }
400
564
  .map { |word, context, _| name_of(word, context) }
@@ -407,6 +571,11 @@ module Hecks
407
571
  # shipped twice (`read_model`'s where/order_by/limit/offset, and
408
572
  # `role`/`goal` on a command). An example that runs is the only
409
573
  # documentation that can go red.
574
+ #
575
+ # @param directory [String] path to the directory holding the committed
576
+ # reference pages
577
+ # @return [Array<String>] each live word with prose but no runnable
578
+ # example, named by `name_of`
410
579
  def unexemplified(directory)
411
580
  live_words(directory).reject { |_word, _context, prose| exemplified?(prose) }
412
581
  .map { |word, context, _| name_of(word, context) }
@@ -1,12 +1,17 @@
1
1
  module Hecks
2
- # **A vendored, external bluebook** — same shape as Framework (framework.rb),
3
- # for members that don't ship inside hecks's own lib/ at all: a
4
- # separate, independently-versioned package
5
- # (github.com/chrisyoung/embryonaut_bluebooks) that a consuming project
6
- # vendors into its own checkout, the same way a project already vendors
7
- # hecks itself (bin/vendor_hecks, vendor/hecks/).
2
+ # A vendored, external bluebook.
8
3
  #
9
- # **Recovered, not rebuilt** — this module and its `uses_embryonaut_bluebook`
4
+ # ## What it is
5
+ #
6
+ # Same shape as Framework (framework.rb), for members that don't ship
7
+ # inside hecks's own lib/ at all: a separate, independently-versioned
8
+ # package (github.com/chrisyoung/embryonaut_bluebooks) that a consuming
9
+ # project vendors into its own checkout, the same way a project already
10
+ # vendors hecks itself (bin/vendor_hecks, vendor/hecks/).
11
+ #
12
+ # ## Recovered, not rebuilt
13
+ #
14
+ # This module and its `uses_embryonaut_bluebook`
10
15
  # DSL word (hecksagon_builder.rb) were built on a prior commit of this
11
16
  # repo (933d1dd), vendored out to a real consumer (lifeadelics/domain,
12
17
  # for embryonaut_bluebooks/payments), and then lost from this repo's own
@@ -17,6 +22,8 @@ module Hecks
17
22
  # conventions rather than copied wholesale, since the two trees had
18
23
  # otherwise diverged for weeks in both directions.
19
24
  #
25
+ # ## Resolution
26
+ #
20
27
  # Resolved from the consuming registry's own root, not this gem's
21
28
  # __dir__ — Framework::ROOT can be a fixed, `__dir__`-relative constant
22
29
  # because framework members ship inside this gem; an embryonaut bluebook
@@ -27,6 +34,8 @@ module Hecks
27
34
  # itself only runs at hecksagon-build time, when a real registry is
28
35
  # current.
29
36
  #
37
+ # ## Load order
38
+ #
30
39
  # Every `.bluebook` file in the package, sorted — not just one. Unlike a
31
40
  # framework member (one file, named by its own stem), a vendored package
32
41
  # can span several bluebook files that reopen the same `Hecks.bluebook`
@@ -37,7 +46,9 @@ module Hecks
37
46
  # payment < payments < policies, the same reason that package's own
38
47
  # files are named to fall in that order in the first place.
39
48
  #
40
- # **Only the bluebook files** — same restriction Framework draws, same
49
+ # ## Scope
50
+ #
51
+ # Only the bluebook files — same restriction Framework draws, same
41
52
  # reason: a `.hecksagon`/`.port`/`.adapter` is a wiring decision
42
53
  # (persistence, which processor adapter is bound) that belongs to
43
54
  # whoever is deploying, never baked into the vendored package itself.
@@ -46,12 +57,24 @@ module Hecks
46
57
  # `Hecks.hecksagon "Payments" do ... end` to bind real storage/adapters
47
58
  # — see Framework's own comment for the fuller reasoning, identical here.
48
59
  #
49
- # Idempotent the same way Framework.load! Is — checked against the
60
+ # ## Idempotency
61
+ #
62
+ # Idempotent the same way `Framework.load!` is — checked against the
50
63
  # bluebook this package actually declares (`Naming.pascal("payments")`
51
64
  # => "Payments"), not a separate ledger. A vendored package's directory
52
65
  # name and its declared `Hecks.bluebook` name are the one convention
53
66
  # this reuses from Framework rather than reinventing.
54
67
  module EmbryonautBluebook
68
+ # Loads a vendored embryonaut bluebook package's `.bluebook` files, once
69
+ # per registry.
70
+ #
71
+ # @param name [String, Symbol] the vendored package's directory name, such
72
+ # as `"payments"`
73
+ # @param registry [Runtime::Registry, nil] the registry to vendor into and
74
+ # check for an existing load; defaults to the current boot registry
75
+ # @return [void]
76
+ # @raise [Runtime::WiringError] if `registry` has no root, or no vendored
77
+ # package named `name` is checked out
55
78
  def self.load!(name, registry: Hecks.current_registry)
56
79
  unless registry&.root
57
80
  raise Runtime::WiringError,
@@ -1,4 +1,6 @@
1
1
  require_relative "../naming"
2
+ require_relative "../runtime/caller"
3
+ require_relative "../ports/authorization"
2
4
 
3
5
  module Hecks
4
6
  module Facade
@@ -41,7 +43,7 @@ module Hecks
41
43
  # @param key [Symbol, String] the attribute name
42
44
  # @return [Object, nil] the value held in state (a scalar, a value object, a list,
43
45
  # or a referenced record's id); `nil` when the field is unset or not in state
44
- def [](key) = @state[key.to_sym]
46
+ def [](key) = redacted(key.to_sym)
45
47
 
46
48
  # Answers the record's state as a plain Hash with the bare identity under `:id`.
47
49
  #
@@ -59,7 +61,7 @@ module Hecks
59
61
  #
60
62
  # @return [Hash{Symbol => Object}] a new Hash of every state field by attribute
61
63
  # name, plus `:id` holding the identity String
62
- def to_h = @state.merge(id: @id)
64
+ def to_h = @state.to_h { |key, _| [key, redacted(key)] }.merge(id: @id)
63
65
 
64
66
  # Names the aggregate this record belongs to, in the form every dispatch verb and
65
67
  # event is addressed by.
@@ -120,7 +122,7 @@ module Hecks
120
122
  # written yet
121
123
  # @raise [NoMethodError] if `name` is neither a key in state nor a declared field
122
124
  def method_missing(name, *args, **kwargs, &)
123
- return @state[name] if @state.key?(name) || reader?(name)
125
+ return redacted(name) if @state.key?(name) || reader?(name)
124
126
 
125
127
  super
126
128
  end
@@ -133,6 +135,77 @@ module Hecks
133
135
 
134
136
  def repository = @dispatcher.registry.repository(@domain, @ir)
135
137
 
138
+ # One field's value, with any Privacy::Marking-flagged leaf masked
139
+ # out unless the ambient caller holds a live Governance grant of
140
+ # the marking's own `readable_by` — the read-side half of the
141
+ # Privacy framework member (lib/hecks/framework/bluebook/
142
+ # privacy.bluebook): a marking's presence is what makes a read
143
+ # redacted, not a separate flag this class carries itself.
144
+ #
145
+ # ALWAYS THE STRONG CHECK, never the weak string-only fallback
146
+ # `CommandRules::Authorization#refuse_role_mismatch` allows an
147
+ # unidentified caller — a read gate gone wrong is a leak, not a
148
+ # refused command, so an ambient caller with no `actor_id` (or no
149
+ # caller at all) is masked here, full stop, rather than waved
150
+ # through the way a self-asserted `role` string is for a command.
151
+ #
152
+ # ONE LEVEL OF NESTING ONLY — `attendee.medications` masks inside
153
+ # the returned `Runtime::Value` via its own `#with`; a marking two
154
+ # levels deep is not supported and is left unmasked rather than
155
+ # silently mishandled, since nothing in this corpus needs it yet.
156
+ #
157
+ # @param field [Symbol] the state key being read
158
+ # @return [Object, nil] `@state[field]`, or a copy with the marked leaf replaced by
159
+ # the literal String `"[redacted]"` when the caller is not authorized to see it
160
+ def redacted(field)
161
+ raw = @state[field]
162
+ rows = marked_paths.select { |row| row[:attribute_path][:value].to_s.split(".", 2).first == field.to_s }
163
+ return raw if rows.empty?
164
+
165
+ rows.each do |row|
166
+ path = row[:attribute_path][:value].to_s
167
+ next if authorized_for?(row[:readable_by][:value].to_s)
168
+
169
+ segments = path.split(".", 2)
170
+ if segments.size == 1
171
+ raw = "[redacted]"
172
+ elsif raw.is_a?(Runtime::Value)
173
+ raw = raw.with(segments[1], "[redacted]")
174
+ end
175
+ end
176
+
177
+ raw
178
+ end
179
+
180
+ # Every Privacy::Marking declared for this record's own aggregate — `[]` when the
181
+ # Privacy framework member is not attached in this boot at all, checked once and
182
+ # cheaply rather than paying for a query dispatch every read on a domain that never
183
+ # attached Privacy.
184
+ #
185
+ # @return [Array<Hash>] `Privacy::Marking.ForDomain`'s own rows for this `fqn`
186
+ def marked_paths
187
+ return @marked_paths if defined?(@marked_paths)
188
+ return @marked_paths = [] unless @dispatcher.registry.bluebook("Privacy")
189
+
190
+ @marked_paths = @dispatcher.query("Privacy::Marking.ForDomain", domain: fqn)
191
+ end
192
+
193
+ # Whether the ambient caller holds a live Governance grant of `role`, over this
194
+ # record's own domain — `false` outright for an unidentified caller or a domain
195
+ # with no authorization provider attached, never the weak fallback a command's own
196
+ # role check allows (see `redacted`'s own header for why).
197
+ #
198
+ # @param role [String] the marking's own `readable_by`
199
+ # @return [Boolean] true only for an identified caller holding a live grant
200
+ def authorized_for?(role)
201
+ caller = Runtime::Caller.current
202
+ return false unless caller&.actor_id
203
+ return false unless @dispatcher.registry.authorization_provider_for(@domain)
204
+
205
+ Ports::Authorization.holds_role?(@dispatcher.registry, actor_id: caller.actor_id, role: role,
206
+ as_of: caller.as_of, scope: caller.scope)
207
+ end
208
+
136
209
  def reader?(name)
137
210
  !@ir.attribute(name).nil? || @ir.lifecycle&.field&.to_sym == name
138
211
  end
@@ -1,6 +1,7 @@
1
1
  require_relative "../../bluebook/dsl/hecksagon_builder"
2
2
  require_relative "../../bluebook/dsl/domain_port_builder"
3
3
  require_relative "../../bluebook/dsl/const_shim"
4
+ require_relative "../../bluebook/dsl/binding_proxy"
4
5
  require_relative "../../bluebook/hexagon"
5
6
  require_relative "../handle"
6
7
  require_relative "../../naming"
@@ -164,6 +165,13 @@ module Hecks
164
165
  end
165
166
 
166
167
  door.define_singleton_method(:method_missing) do |verb, *args, **kwargs, &block|
168
+ # A BARE CALL starts a Privacy marking chain — see
169
+ # `Bluebook::DSL::BindingProxy#method_missing`'s own header;
170
+ # this is the same mechanism, reached when the constant is
171
+ # already a real, installed door (a second boot in-process)
172
+ # rather than a `.hecksagon`-parse-time `BindingProxy`.
173
+ return Bluebook::DSL::AttributePath.new(fqn, [verb.to_s]) if args.empty? && kwargs.empty? && !block
174
+
167
175
  collector = Bluebook::DSL::HecksagonBuilder.collector
168
176
  return super(verb, *args, **kwargs, &block) unless collector
169
177
 
@@ -4,6 +4,9 @@ require_relative "../naming"
4
4
  require_relative "value_object_shape"
5
5
 
6
6
  module Hecks
7
+ # The forms surface's own namespace — the `expose` DSL, the IR->HTML
8
+ # renderers, and the Rack app bridging them; see `forms.rb`'s own header,
9
+ # its primary opening, for the fuller design this implements.
7
10
  module Forms
8
11
  # One resolved field, ready for a renderer to turn into markup. A leaf
9
12
  # scalar carries `kind`/`options`/etc; a `:group` or `:list` carries
@@ -9,6 +9,15 @@ module Hecks
9
9
  # `prefers-color-scheme` alone; nothing here reads a cookie or a query
10
10
  # param for it, so it is never wrong for the browser rendering it.
11
11
  module Page
12
+ # Wraps one page's own body HTML in the shared shell: doctype, head, nav, footer,
13
+ # inline styles and script.
14
+ #
15
+ # @param title [String] the page title, escaped into `<title>` and the browser tab
16
+ # @param body [String] the page's own body markup, inserted unescaped inside `<main>`
17
+ # @param breadcrumbs [Array<Array(String, String), Array(String, nil)>] each
18
+ # `[label, href]` pair, in order; the last pair's `href` should be `nil` for the
19
+ # current page
20
+ # @return [String] the complete HTML document
12
21
  def self.render(title:, body:, breadcrumbs: [])
13
22
  <<~HTML
14
23
  <!doctype html>
@@ -36,6 +45,11 @@ module Hecks
36
45
  HTML
37
46
  end
38
47
 
48
+ # Renders the breadcrumb trail as a `<nav>`, each crumb a link except the last.
49
+ #
50
+ # @param crumbs [Array<Array(String, String), Array(String, nil)>] each
51
+ # `[label, href]` pair; a `nil` href renders as the current, unlinked page
52
+ # @return [String] the `<nav>` markup, HTML-escaped; `""` when `crumbs` is empty
39
53
  def self.breadcrumbs_html(crumbs)
40
54
  return "" if crumbs.empty?
41
55