hecks 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +13 -0
  3. data/lib/hecks/adapters/driven/memory.rb +1 -1
  4. data/lib/hecks/adapters/driven/postgres.rb +1 -1
  5. data/lib/hecks/adapters/driven/prism.rb +1 -1
  6. data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
  7. data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
  8. data/lib/hecks/behaviors/expectations.rb +2 -2
  9. data/lib/hecks/bluebook/assembly/contract.rb +1 -1
  10. data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
  11. data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
  12. data/lib/hecks/bluebook/assembly.rb +1 -1
  13. data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
  14. data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
  15. data/lib/hecks/bluebook/capabilities.rb +2 -2
  16. data/lib/hecks/bluebook/chapter.rb +1 -1
  17. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
  18. data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
  19. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
  20. data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
  21. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
  22. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
  23. data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
  24. data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
  25. data/lib/hecks/bluebook/dsl/world_builder.rb +5 -0
  26. data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
  27. data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
  28. data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
  29. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
  30. data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
  31. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
  32. data/lib/hecks/bluebook/meta_validator.rb +1 -1
  33. data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
  34. data/lib/hecks/bluebook/model_check.rb +12 -13
  35. data/lib/hecks/cache_dir.rb +103 -0
  36. data/lib/hecks/cli/document.rb +104 -0
  37. data/lib/hecks/cli/ir.rb +47 -0
  38. data/lib/hecks/cli/mcp.rb +32 -0
  39. data/lib/hecks/cli/mcp_door.rb +461 -0
  40. data/lib/hecks/cli/model_check.rb +270 -0
  41. data/lib/hecks/cli/project_cli.rb +123 -0
  42. data/lib/hecks/cli/project_diagrams.rb +43 -0
  43. data/lib/hecks/cli/run.rb +219 -0
  44. data/lib/hecks/cli/smoke_test.rb +59 -0
  45. data/lib/hecks/cli/stores.rb +54 -0
  46. data/lib/hecks/cli.rb +166 -0
  47. data/lib/hecks/facade/handle.rb +2 -2
  48. data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
  49. data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
  50. data/lib/hecks/mcp_door_scope.rb +176 -0
  51. data/lib/hecks/ports/persistence/append_only.rb +1 -1
  52. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +1 -1
  53. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
  54. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
  55. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
  56. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
  57. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
  58. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +12 -6
  59. data/lib/hecks/ports/persistence/state_codec.rb +1 -1
  60. data/lib/hecks/projections/deploy/fargate.rb +1 -1
  61. data/lib/hecks/projections/deploy/lambda.rb +3 -3
  62. data/lib/hecks/projections/reference.rb +7 -1
  63. data/lib/hecks/projector/exporter.rb +4 -4
  64. data/lib/hecks/projector/target.rb +1 -1
  65. data/lib/hecks/release/runner/ci_publisher.rb +96 -0
  66. data/lib/hecks/release/runner/commands.rb +68 -0
  67. data/lib/hecks/release/runner/console.rb +52 -0
  68. data/lib/hecks/release/runner/gem_publisher.rb +44 -0
  69. data/lib/hecks/release/runner/git.rb +48 -0
  70. data/lib/hecks/release/runner/npm_publisher.rb +89 -0
  71. data/lib/hecks/release/runner/preflight.rb +119 -0
  72. data/lib/hecks/release/runner/published.rb +55 -0
  73. data/lib/hecks/release/runner/tagger.rb +87 -0
  74. data/lib/hecks/release/runner.rb +244 -0
  75. data/lib/hecks/router/namespace_installer.rb +1 -1
  76. data/lib/hecks/runtime/command_interpreter.rb +3 -3
  77. data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
  78. data/lib/hecks/runtime/entity_element.rb +1 -1
  79. data/lib/hecks/runtime/entity_interpreter.rb +1 -1
  80. data/lib/hecks/runtime/invocation.rb +1 -1
  81. data/lib/hecks/runtime/loader.rb +1 -1
  82. data/lib/hecks/runtime/outbox.rb +1 -1
  83. data/lib/hecks/runtime/reaction_invocation.rb +3 -3
  84. data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
  85. data/lib/hecks/runtime/reference_hop.rb +4 -4
  86. data/lib/hecks/runtime/registry/verification.rb +11 -11
  87. data/lib/hecks/runtime/registry.rb +8 -4
  88. data/lib/hecks/runtime/saga_interpreter.rb +1 -1
  89. data/lib/hecks/runtime/value/coercion.rb +3 -3
  90. data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
  91. data/lib/hecks/storehouse.rb +18 -12
  92. data/lib/hecks/version.rb +1 -1
  93. data/lib/hecks/vocabulary.rb +5 -0
  94. data/lib/hecks.rb +6 -1
  95. metadata +28 -58
  96. data/lib/hecks/bench/cli.rb +0 -74
  97. data/lib/hecks/bench/environment.rb +0 -100
  98. data/lib/hecks/bench/postgres_probe.rb +0 -41
  99. data/lib/hecks/bench/report.rb +0 -116
  100. data/lib/hecks/bench/ruby_runner.rb +0 -151
  101. data/lib/hecks/bench/run.rb +0 -55
  102. data/lib/hecks/bench/rust_runner.rb +0 -165
  103. data/lib/hecks/bench/stats.rb +0 -75
  104. data/lib/hecks/bench/suite.rb +0 -172
  105. data/lib/hecks/bench/workload.rb +0 -137
  106. data/lib/hecks/bench.rb +0 -28
  107. data/lib/hecks/codemod.rb +0 -443
  108. data/lib/hecks/corpus.rb +0 -479
  109. data/lib/hecks/doc/reference.rb +0 -585
  110. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  111. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  112. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  113. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  114. data/lib/hecks/fuzzing/differential.rb +0 -192
  115. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  116. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  117. data/lib/hecks/fuzzing/form_census.rb +0 -285
  118. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  119. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  120. data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
  121. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  122. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  123. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  124. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  125. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  126. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  127. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  128. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  129. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  130. data/lib/hecks/fuzzing/properties.rb +0 -267
  131. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  132. data/lib/hecks/fuzzing/replay.rb +0 -930
  133. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  134. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  135. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  136. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  137. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  138. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  139. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  140. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  141. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  142. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  143. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  144. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  145. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  146. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  147. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  148. data/lib/hecks/fuzzing.rb +0 -23
  149. data/lib/hecks/grammar/evolve.rb +0 -524
  150. data/lib/hecks/query_ir.rb +0 -482
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