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
data/lib/hecks/corpus.rb DELETED
@@ -1,479 +0,0 @@
1
- require_relative "fuzzing/target_capabilities"
2
-
3
- module Hecks
4
- # **The corpus, discovered** — every place in this repo that holds a real
5
- # domain, named once.
6
- #
7
- # One table here, rather than each consumer spelling out its own copy —
8
- # spec/corpus_spec.rb, spec/model_check_spec.rb, bin/model_check,
9
- # spec/parser_parity_spec.rb, bin/fuzz, Fuzzing::CombinationMiner all did,
10
- # and the copies had already drifted: only the model checker saw
11
- # `qa/bluebook`, only parser parity saw `spec/fixtures`, and nothing but
12
- # bin/fuzz's own sweep ever saw `qa/stress_domains`. Each consumer names
13
- # the `KINDS` it walks rather than re-deriving where those kinds live.
14
- #
15
- # Plain Dir/File only — bin/ scripts require this before (or without)
16
- # booting anything.
17
- module Corpus
18
- ROOT = File.expand_path("../..", __dir__).freeze
19
-
20
- Member = Struct.new(:stem, :kind, :path)
21
-
22
- # **One domain per directory** — its bluebooks sit in `<dir>/bluebook/`
23
- # or directly in `<dir>` (see `bluebook_files`). Stemmed by directory.
24
- DIRECTORY_KINDS = {
25
- example: "examples/*",
26
- stress: "qa/stress_domains/*",
27
- semantics: "spec/corpus/semantics/domains/*",
28
- # A PACKAGE VENDORED VIA `uses_embryonaut_bluebook` — same real
29
- # mechanism `uses_framework` is (`lib/hecks/embryonaut_bluebook.rb`'s
30
- # own header: "same shape as Framework"), one level further out: not
31
- # a chapter shipped inside THIS gem's own `lib/hecks/framework/
32
- # bluebook/` (the `:framework` FILE_KIND, below), but a directory
33
- # nested inside the CONSUMING example's own checkout
34
- # (`<domain>/vendor/embryonaut_bluebooks/<name>/bluebook/`,
35
- # EmbryonautBluebook.load!'s own resolution path) — hence its own
36
- # DIRECTORY_KIND rather than reuse of `:framework`'s shape. First
37
- # real member: docs/decisions/0058's own
38
- # examples/embryonaut_vendoring_demo/vendor/embryonaut_bluebooks/widgets.
39
- vendored: "examples/*/vendor/embryonaut_bluebooks/*"
40
- }.freeze
41
-
42
- # **One chapter per file**. Stemmed by the path below the glob's fixed
43
- # prefix, so a nested fixture keeps its subdirectory (`eras/base`)
44
- # and never collides with a same-named file elsewhere in the kind.
45
- FILE_KINDS = {
46
- grammar: "lib/hecks/grammar/*.bluebook",
47
- framework: "lib/hecks/framework/bluebook/*.bluebook",
48
- qa: "qa/bluebook/*.bluebook",
49
- language: "lib/hecks/language/**/*.bluebook",
50
- deploy: "lib/hecks/deploy/bluebook/*.bluebook",
51
- tenancy: "lib/hecks/tenancy/bluebook/*.bluebook",
52
- fixture: "spec/fixtures/**/*.bluebook"
53
- }.freeze
54
-
55
- KINDS = (DIRECTORY_KINDS.keys + FILE_KINDS.keys).freeze
56
-
57
- # Where a bluebook the sweep does not boot goes instead. Not a filter:
58
- # nothing leaves `sweepable_domains` without naming the check that owns
59
- # it, and spec/corpus_accounting_spec.rb proves each destination exists
60
- # and actually exercises what is routed to it.
61
- #
62
- # check: :named_in — `destination` names every routed file
63
- # (`names: :each_file`) or the given text
64
- # check: :gitignored — `destination` holds the ignore rule `names`,
65
- # and nothing matching is committed
66
- # check: :gap — no check exercises these yet. Listed so the
67
- # gap is visible; the accounting spec keeps it
68
- # pending and fails the moment one appears.
69
- #
70
- # Ordered — a bluebook belongs to the first route it matches, so a
71
- # specific destination sits above the catch-all for its shape.
72
- Route = Struct.new(:pattern, :check, :destination, :names, :why)
73
-
74
- ROUTES = [
75
- Route.new(%r{\Atmp/}, :gitignored, ".gitignore", "tmp/",
76
- "scratch output — generated and mined candidate domains, fuzz failures — never committed"),
77
- Route.new(%r{/\.aws-sam/}, :gitignored, ".gitignore", ".aws-sam/",
78
- "SAM build output — a vendored copy of real sources, never committed"),
79
- Route.new(%r{/data/eras/}, :gitignored, ".gitignore", "**/data/eras/",
80
- "era snapshots a file adapter writes at runtime, never committed"),
81
- Route.new(%r{\Arust/}, :named_in, "rust/parser/tests/gates.rs", :each_file,
82
- "the Rust parser's own fixtures, each loaded by its gate tests"),
83
- Route.new(%r{/translations/}, :named_in, "spec/translation/committed_edges_spec.rb", "bluebook/translations",
84
- "translation edges, not chapters; each must load, chain era to era, and end at the storage " \
85
- "shape its bluebook declares today"),
86
- Route.new(%r{\Alib/hecks/forms/examples/}, :named_in, "spec/forms/app_spec.rb", :each_file,
87
- "a Forms presentation config wearing the .bluebook extension"),
88
- Route.new(%r{\Aspec/fixtures/eras/}, :named_in, "spec/runtime/storage_shape_spec.rb", "fixtures/eras",
89
- "deliberately conflicting versions of one domain; each must classify as the verdict " \
90
- "its filename declares (bump_* / same_*)"),
91
- Route.new(%r{\Aspec/fixtures/model_check/}, :named_in, "spec/model_check_spec.rb", :each_file,
92
- "domains broken on purpose; each must produce exactly the finding kinds it is built to trigger"),
93
- # bin/fuzz sweeps it too, once Fuzzing::Replay coerces value-object
94
- # args before recomputing givens — today its oracle reads raw args
95
- # while dispatch coerces, so checkout_fixture's VO-reading given
96
- # reads as wrongly admitted.
97
- Route.new(%r{\Aspec/fixtures/rust_host/}, :named_in, "rust/host/src/web.rs", "checkout_fixture",
98
- "the Rust host's checkout fixture, pinned by its web and /api tests")
99
- ].freeze
100
-
101
- module_function
102
-
103
- # Lists corpus members of the given kinds (every kind, by default).
104
- #
105
- # @param kinds [Array<Symbol>] corpus kinds to include, from `KINDS`; every kind
106
- # when empty
107
- # @param root [String] repository root to search under
108
- # @return [Array<Member>] matching members
109
- # @raise [ArgumentError] if `kinds` names a kind not in `KINDS`
110
- def members(*kinds, root: ROOT)
111
- kinds = KINDS if kinds.empty?
112
- kinds.flat_map do |kind|
113
- if (glob = DIRECTORY_KINDS[kind])
114
- Dir.glob(File.join(root, glob)).select { |path| File.directory?(path) }.sort
115
- .map { |dir| Member.new(File.basename(dir), kind, dir) }
116
- elsif (glob = FILE_KINDS[kind])
117
- prefix = File.join(root, glob[/\A[^*]*/].chomp("/"))
118
- Dir.glob(File.join(root, glob))
119
- .map { |file| Member.new(file.delete_prefix("#{prefix}/").delete_suffix(".bluebook"), kind, file) }
120
- else
121
- raise ArgumentError, "unknown corpus kind #{kind.inspect} — known: #{KINDS.join(', ')}"
122
- end
123
- end
124
- end
125
-
126
- # What a boot loads for a member: a directory kind's bluebook
127
- # directory, a file kind's own file. `nil` for a directory holding no
128
- # bluebook at all.
129
- #
130
- # @param member [Member] the corpus member
131
- # @return [String, nil] the path to boot, or nil when a directory member holds
132
- # no bluebook
133
- def source_of(member)
134
- DIRECTORY_KINDS.key?(member.kind) ? bluebook_dir(member.path) : member.path
135
- end
136
-
137
- # Where a domain path keeps its bluebooks — `<domain>/bluebook/*.bluebook`
138
- # (every example and stress domain), or the directory itself
139
- # (`qa/bluebook`). `nil` when neither holds a bluebook.
140
- #
141
- # @param domain_path [String] path to a domain directory
142
- # @return [Array<String>, nil] `.bluebook` file paths found, or nil when none
143
- def bluebook_files(domain_path)
144
- [File.join(domain_path, "bluebook"), domain_path].each do |dir|
145
- files = Dir[File.join(dir, "*.bluebook")]
146
- return files unless files.empty?
147
- end
148
- nil
149
- end
150
-
151
- # The directory holding `domain_path`'s bluebooks.
152
- #
153
- # @param domain_path [String] path to a domain directory
154
- # @return [String, nil] directory of the first `.bluebook` file found, or nil
155
- # when `domain_path` holds none
156
- def bluebook_dir(domain_path)
157
- files = bluebook_files(domain_path)
158
- files && File.dirname(files.first)
159
- end
160
-
161
- # The route a repo-relative path takes instead of the sweep — the
162
- # first one it matches — or `nil` when the sweep boots it.
163
- #
164
- # @param relative_path [String] a corpus member's path, relative to the repo root
165
- # @return [Route, nil] the first matching route, or nil when the sweep boots it
166
- def route_for(relative_path)
167
- ROUTES.find { |route| route.pattern.match?(relative_path) }
168
- end
169
-
170
- # What bin/model_check and spec/model_check_spec.rb walk — every kind,
171
- # less the language (examined as one judged chapter, not file by file)
172
- # and deploy chapters (the SAM projector's own inputs), and less any
173
- # member a route already sends to a destination of its own: the
174
- # broken-on-purpose model_check fixtures must produce their findings
175
- # there, so a clean-corpus gate here would be the wrong check for them.
176
- MODEL_CHECK_KINDS = %i[example grammar framework vendored qa stress fixture].freeze
177
-
178
- # Every corpus member `bin/model_check` and `spec/model_check_spec.rb` walk.
179
- #
180
- # @param root [String] repository root to search under
181
- # @return [Array<Member>] members of `MODEL_CHECK_KINDS`, less any routed elsewhere
182
- def model_check_members(root: ROOT)
183
- members(*MODEL_CHECK_KINDS, root: root).reject { |member| route_for(member.path.delete_prefix("#{root}/")) }
184
- end
185
-
186
- # **The ledger sweeps itself** — its own chapter is a domain like any
187
- # other, and the one rotation member that is neither an example nor a
188
- # stress domain.
189
- ROTATION_LEDGER = { "quality_control" => "qa/bluebook" }.freeze
190
-
191
- # What the QA rotation is made of — every example and stress domain
192
- # this repository owns, plus the ledger, as `reference => repo-relative
193
- # path`: exactly the shape `Target.path` is stored in.
194
- #
195
- # Derived, because the hand-kept version silently went stale.
196
- # `bin/qa_seed_targets` carried a literal list naming three of the
197
- # thirteen stress domains; the other ten were authored, argued for in
198
- # their own NOTES.md, several promoted by `bin/qa_generated_domains
199
- # --promote` — and never swept once, because a `Target` row is what
200
- # puts a domain in the rotation and nothing tied that list to the
201
- # corpus. Promotion only ever printed the `target.identify` line for a
202
- # human to run.
203
- #
204
- # @param root [String] repository root to search under
205
- # @return [Hash{String => String}] each rotation member's stem/reference mapped
206
- # to its repo-relative path
207
- def rotation_targets(root: ROOT)
208
- members(:example, :stress, root: root)
209
- .to_h { |member| [member.stem, member.path.delete_prefix("#{root}/")] }
210
- .merge(ROTATION_LEDGER)
211
- end
212
-
213
- # Every bootable domain in the project, not a hand-kept list — any
214
- # directory holding a `.bluebook` no route sends elsewhere, a
215
- # `bluebook/` folder standing for the domain directory around it.
216
- #
217
- # @param root [String] repository root to search under
218
- # @return [Array<String>] absolute paths of every sweepable domain directory
219
- def sweepable_domains(root = ROOT)
220
- Dir.chdir(root) do
221
- Dir.glob("**/*.bluebook")
222
- .reject { |path| route_for(path) }
223
- .map { |path| File.dirname(path) }
224
- .map { |dir| File.basename(dir) == "bluebook" ? File.dirname(dir) : dir }
225
- .uniq.sort
226
- .map { |dir| File.join(root, dir) }
227
- end
228
- end
229
-
230
- # The domain directory a member stands for, spelled the way
231
- # `sweepable_domains` spells it: a `bluebook/` folder is its parent.
232
- #
233
- # @param member [Member] the corpus member
234
- # @return [String] the member's owning domain directory path
235
- def domain_dir_of(member)
236
- dir = File.directory?(member.path) ? member.path : File.dirname(member.path)
237
- File.basename(dir) == "bluebook" ? File.dirname(dir) : dir
238
- end
239
-
240
- # ── The Rust-facing corpus ─────────────────────────────────────────
241
- #
242
- # Every Rust-facing list (the fuzz bridge, the codegen drift check,
243
- # rust coverage, codegen parity) reads `rust/Cargo.toml`'s `[features]`
244
- # as its one source, rather than being typed out by hand per consumer
245
- # and drifting from it — hand-typed, fuzzing once saw only 8 of 20
246
- # features. The `rust/src/generated/` modules split into buckets, and
247
- # spec/corpus_rust_spec.rb proves every Cargo feature and every
248
- # generated module lands in exactly one of them:
249
- #
250
- # rust_domains an in-repo domain directory with a Cargo feature
251
- # of its own: fuzzed, regenerated, coverage- and
252
- # parity-checked
253
- # framework chapters no feature and no merged.rs; written as a side
254
- # effect of every `uses_framework` domain's regen
255
- # RUST_ELSEWHERE a feature with no in-repo domain directory, and
256
- # the check that owns it instead
257
- GENERATED_DIR = "rust/src/generated".freeze
258
- RUST_DOMAIN_KINDS = %i[example stress fixture].freeze
259
-
260
- RustDomain = Struct.new(:feature, :dir, :kind)
261
-
262
- # `check: :named_in` — `destination` must name `names`.
263
- Elsewhere = Struct.new(:check, :destination, :names, :why)
264
-
265
- RUST_ELSEWHERE = {
266
- "meta" => Elsewhere.new(:named_in, "spec/codegen_parity_spec.rb", "bluebook_language",
267
- "the self-hosted grammar (lib/hecks/language), not a domain directory — every " \
268
- "bin/project_rust run rewrites it (so the drift check diffs it), codegen parity " \
269
- "checks it as bluebook_language, and there is no directory to fuzz")
270
- }.freeze
271
-
272
- # Every place a Rust-facing domain's own `uses_framework`/
273
- # `uses_embryonaut_bluebook` attachment could be declared — every
274
- # in-repo Rust domain's own hecksagon files.
275
- #
276
- # @param root [String] repository root to search under
277
- # @return [String] every reachable hecksagon file's own text, joined by newlines
278
- def rust_attachment_hecksagon_text(root: ROOT)
279
- rust_domains(root: root).flat_map { |domain| Dir.glob(File.join(domain.dir, "**", "*.hecksagon")) }
280
- .map { |path| File.read(path) }.join("\n")
281
- end
282
-
283
- # **Shrink-only**. A generated module `bin/rust_coverage` still reports a
284
- # gap for. `bin/corpus --rust-coverage` requires each of these to
285
- # still fail, so an entry that starts passing breaks the build until
286
- # it is deleted here.
287
- RUST_COVERAGE_PENDING = {}.freeze
288
-
289
- # The Cargo `[features]` table's raw text.
290
- #
291
- # @param root [String] repository root to search under
292
- # @return [String] the table's text, or `""` when `rust/Cargo.toml` has none
293
- def cargo_features_table(root: ROOT)
294
- File.read(File.join(root, "rust/Cargo.toml"))[Fuzzing::TargetCapabilities::FEATURES_TABLE] || ""
295
- end
296
-
297
- # Every feature name Cargo declares with no dependency array of its own.
298
- #
299
- # @param root [String] repository root to search under
300
- # @return [Array<String>] feature names, in file order
301
- def cargo_features(root: ROOT)
302
- cargo_features_table(root: root).scan(/^(\w+)\s*=\s*\[\]/).flatten
303
- end
304
-
305
- # The feature `bin/project_rust` last wrote as Cargo's `default`.
306
- #
307
- # @param root [String] repository root to search under
308
- # @return [String, nil] the default feature's name, or nil when Cargo.toml
309
- # declares none
310
- def cargo_default(root: ROOT)
311
- cargo_features_table(root: root)[/^default\s*=\s*\["(\w+)"\]/, 1]
312
- end
313
-
314
- # Every in-repo domain directory whose name is a Cargo feature, sorted
315
- # by path. When the module is already generated, its metadata.rs stamp
316
- # decides which directory it came from — a directory name alone is not
317
- # enough: spec/fixtures/qa_discover_external_domains vendors a second
318
- # `examples/pizzas` that no Cargo feature was ever generated from.
319
- #
320
- # @param root [String] repository root to search under
321
- # @return [Array<RustDomain>] each Rust-facing domain, sorted by directory path
322
- def rust_domains(root: ROOT)
323
- features = cargo_features(root: root)
324
- members(*RUST_DOMAIN_KINDS, root: root)
325
- .map { |member| [domain_dir_of(member), member.kind] }
326
- .uniq(&:first)
327
- .select { |dir, _| rust_domain_dir?(dir, features, root) }
328
- .sort_by(&:first)
329
- .map { |dir, kind| RustDomain.new(File.basename(dir).downcase, dir, kind) }
330
- end
331
-
332
- # Tells whether `dir` is the real source directory for its Cargo feature.
333
- #
334
- # @param dir [String] a candidate domain directory
335
- # @param features [Array<String>] known Cargo feature names
336
- # @param root [String] repository root `dir` is rooted under
337
- # @return [Boolean]
338
- def rust_domain_dir?(dir, features, root)
339
- feature = File.basename(dir).downcase
340
- source = generated_source(feature, root: root)
341
- features.include?(feature) && (source.nil? || source == dir.delete_prefix("#{root}/"))
342
- end
343
-
344
- # Where a generated module came from, read off the stamp bin/project_rust
345
- # writes into its metadata.rs — `examples/pizzas`, `/abs/path/embryonaut`,
346
- # `the self-hosted language (lib/hecks/language/bluebook)`, with any
347
- # ` (uses_framework "X")` or ` (uses_embryonaut_bluebook "X")` suffix
348
- # dropped. `nil` when not generated.
349
- SOURCE_STAMP = %r{
350
- GENERATED\ by\ bin/project_rust\ —\ (.+?)
351
- (?:\ \((?:uses_framework|uses_embryonaut_bluebook)\ "\w+"\))?
352
- 's\ own\ canonical\ IR,
353
- }x
354
-
355
- # Where a generated module came from, read off the stamp `bin/project_rust`
356
- # writes into its metadata.rs.
357
- #
358
- # @param module_name [String] a generated module's name
359
- # @param root [String] repository root to search under
360
- # @return [String, nil] the source path the stamp names, or nil when the module
361
- # is not generated
362
- def generated_source(module_name, root: ROOT)
363
- metadata = File.join(root, GENERATED_DIR, module_name, "metadata.rs")
364
- File.file?(metadata) ? File.read(metadata)[SOURCE_STAMP, 1] : nil
365
- end
366
-
367
- # Every module directory under `rust/src/generated`.
368
- #
369
- # @param root [String] repository root to search under
370
- # @return [Array<String>] generated module names, sorted
371
- def generated_modules(root: ROOT)
372
- Dir.children(File.join(root, GENERATED_DIR)).select { |name| File.directory?(File.join(root, GENERATED_DIR, name)) }.sort
373
- end
374
-
375
- # Tells whether `feature` has already been generated (has a merged.rs).
376
- #
377
- # @param feature [String] a Cargo feature / domain name
378
- # @param root [String] repository root to search under
379
- # @return [Boolean]
380
- def generated?(feature, root: ROOT)
381
- File.file?(File.join(root, GENERATED_DIR, feature, "merged.rs"))
382
- end
383
-
384
- # Chapters attached via `kind` (`:framework` or `:vendored`) that
385
- # `bin/project_rust` writes a module for as a side effect of the
386
- # attaching domain — a module, no merged.rs of its own. Both kinds
387
- # share this shape: `bin/project_rust`'s own generated module name is
388
- # always `chapter_name.downcase` (rust/src/generated/mod.rs's own
389
- # convention), and for either kind that downcased name is always the
390
- # original lowercase directory stem again — confirmed live for a
391
- # framework member and for a vendored package alike (a vendored
392
- # package's own bluebook stem doesn't have to equal its declared
393
- # chapter name the way a framework member's does — `EmbryonautBluebook.
394
- # load!` resolves it as `Naming.pascal(dir_name)`, e.g. "widgets" ->
395
- # "Widgets" — but downcasing a `Naming.pascal`-derived name always
396
- # returns the original lowercase stem again).
397
- #
398
- # @param kind [Symbol] :framework or :vendored
399
- # @param root [String] repository root to search under
400
- # @return [Array<String>] stems with a generated module but no merged.rs
401
- # of their own
402
- def rust_side_chapters(kind, root: ROOT)
403
- modules = generated_modules(root: root)
404
- members(kind, root: root).map(&:stem)
405
- .select do |stem|
406
- module_name = Naming.pascal(stem).downcase
407
- modules.include?(module_name) && !generated?(module_name, root: root)
408
- end
409
- end
410
-
411
- # The generated module name a `:framework`/`:vendored` stem writes
412
- # under — `Naming.pascal(stem).downcase` (`bin/project_rust`'s own
413
- # convention, see `rust_side_chapters`'s header); a chapter whose
414
- # stem holds an underscore ("console_settings") writes to a
415
- # different, underscore-free module name ("consolesettings"), so a
416
- # bucket-membership check against `generated_modules` (a directory
417
- # name) needs this mapping rather than the raw stem
418
- # `rust_framework_chapters`/`rust_vendored_chapters` still return
419
- # (every other caller wants the stem, to build a bluebook file path).
420
- #
421
- # @param stem [String] a `:framework`/`:vendored` corpus member's stem
422
- # @return [String] the generated module's own directory name
423
- def rust_side_module_name(stem)
424
- Naming.pascal(stem).downcase
425
- end
426
-
427
- def rust_framework_chapters(root: ROOT)
428
- rust_side_chapters(:framework, root: root)
429
- end
430
-
431
- def rust_vendored_chapters(root: ROOT)
432
- rust_side_chapters(:vendored, root: root)
433
- end
434
-
435
- # Chapters an in-repo Rust domain carries beside its own bluebook (a
436
- # sibling `.bluebook` in the domain's directory) that `bin/project_rust`
437
- # writes a module for as a side effect — a module, no merged.rs of its
438
- # own. Today: `payments`, beside spec/fixtures/rust_host/checkout_fixture.
439
- #
440
- # @param root [String] repository root to search under
441
- # @return [Array<String>] generated module names with no merged.rs of their own
442
- def rust_sibling_chapters(root: ROOT)
443
- modules = generated_modules(root: root)
444
- rust_domains(root: root)
445
- .flat_map { |domain| Dir.glob(File.join(domain.dir, "bluebook", "*.bluebook")) }
446
- .map { |path| Naming.pascal(File.basename(path, ".bluebook")).downcase }
447
- .uniq
448
- .select { |name| modules.include?(name) && !generated?(name, root: root) }
449
- end
450
-
451
- # THE REGENERATION ORDER the drift check runs. Sorted by path, so
452
- # which domain runs last — and so wins Cargo's `default`, mod.rs's cfg
453
- # comments and the shared framework modules' attribution stamp — is a
454
- # fact of the sorted list, not a hand-picked order: today that is
455
- # `has_many_fixture`. spec/corpus_rust_spec.rb pins last == default.
456
- #
457
- # @param root [String] repository root to search under
458
- # @return [Array<RustDomain>] already-generated Rust domains, in regeneration order
459
- def rust_regen_order(root: ROOT)
460
- rust_domains(root: root).select { |domain| generated?(domain.feature, root: root) }
461
- end
462
-
463
- # The chapter a bluebook declares — `Hecks.bluebook "<Name>"`, read
464
- # off the file rather than guessed from its name (a grammar chapter's
465
- # file is named after its role, `aggregate.bluebook`, while its
466
- # chapter is always "Bluebook"). Scans the whole file: a framework
467
- # member's header comment can run past any fixed line cap.
468
- #
469
- # @param bluebook_path [String, Array<String>] a `.bluebook` file path, or an
470
- # array whose first element is used
471
- # @return [String, nil] the declared chapter name, or nil when the file never
472
- # declares one
473
- def chapter_name_of(bluebook_path)
474
- bluebook_path = Array(bluebook_path).first
475
- header = File.foreach(bluebook_path).find { |line| line =~ /\A\s*Hecks\.bluebook\s+"([^"]+)"/ }
476
- header && Regexp.last_match(1)
477
- end
478
- end
479
- end