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
@@ -1,154 +0,0 @@
1
- require "hecks/ports/persistence/plugins/era"
2
-
3
- module Hecks
4
- module Fuzzing
5
- # **The fork-loss class, as a check** — not a fuzz. Every other mode in
6
- # this file generates a sequence and compares two answers to the same
7
- # question; this one asks a single, unconditional question of a
8
- # target's own real, already-configured `PostgresEra` ledger:
9
- # does any ancestor era still hold writes nobody has merged forward?
10
- #
11
- # ## The bug this targets, named exactly
12
- #
13
- # Minting a new era (an attribute
14
- # or aggregate addition — `StorageShape.project`, lib/hecks/ports/
15
- # persistence/plugins/era/storage_shape.rb) advances the readable head
16
- # to a new partition; an old checkout, or a process that boots slower
17
- # than the mint, can keep writing into the era it still believes is
18
- # current. Those writes are not lost — `Lineage#diverged_count`
19
- # (postgres_era/lineage/tail_merge.rb) can always find them — but
20
- # nothing ever asked it automatically. The practice's own ledger found
21
- # exactly this live, twice: once as an operational gap this session's
22
- # Step 0 recovered by hand (`bin/merge_tail`, three conflicting
23
- # records, three eras deep), and once as BUG#24 (a superuser
24
- # connection walking straight through the era write-fence — fixed by
25
- # refusing that connection outright, `Lineage#check_fence_applies!`).
26
- # Both are "a fork happened and nothing said so" — this module is the
27
- # automatic version of the question `bin/merge_tail`'s own diagnostic
28
- # line already answers by hand, run as an ordinary sweep Check instead
29
- # of only when a human remembers to ask.
30
- #
31
- # ## Connection and safety
32
- #
33
- # The target's real database, read-only, never a disposable one — every
34
- # other Postgres-touching mode here (`persistence_parity`,
35
- # `adapter_parity_sqlite`) owns a throwaway schema for the exact
36
- # reason it must never look at what a real deployment actually holds;
37
- # this check exists for what a real deployment actually holds, so it
38
- # connects the same way `bin/merge_tail` itself does — the target's
39
- # own `.world` binding — and never writes anything: `eras`/
40
- # `diverged_count` are both plain `SELECT`s, and `ensure_base!` only
41
- # ever provisions (`CREATE TABLE IF NOT EXISTS`) when this connection
42
- # is the database's own provisioning owner, the identical idempotent
43
- # call every ordinary boot already makes. A target with no real,
44
- # reachable database configured (nothing provisioned locally, say) is
45
- # reported `checked: false` with the reason — an operational note this
46
- # mode's own caller surfaces once, never a crash and never silently
47
- # "clean".
48
- module EraBoundary
49
- module_function
50
-
51
- # Reports whether any ancestor `PostgresEra` era still holds writes the
52
- # head has never merged forward, for one target's real database.
53
- #
54
- # `{ checked: true, diverged_total:, breakdown: [{ordinal:, diverged:}] }`
55
- # or `{ checked: false, kind:, reason: "..." }`. A `true` with
56
- # `diverged_total.positive?` is the finding this module exists to
57
- # surface: real post-cut writes an ancestor era is still holding.
58
- #
59
- # `kind:` distinguishes the two different reasons `checked: false` can
60
- # occur — without it, `bin/qa_sweep` would log a refused connection
61
- # and a `Lineage` defect exactly like an audit that ran and found
62
- # nothing, counting either toward `sweep.made` and the target's clean
63
- # streak. `:not_applicable` is a target with no lineage to audit at
64
- # all; `:error` is the audit failing to run, which the caller
65
- # surfaces as a finding rather than holding.
66
- #
67
- # @param domain_path [String] filesystem path to the target domain's bluebook
68
- # directory, or a directory containing one
69
- # @return [Hash{Symbol => Object}] on success, `{checked: true, era_count: Integer,
70
- # breakdown: Array<Hash{ordinal: Integer, diverged: Integer}>, diverged_total: Integer}`;
71
- # otherwise `{checked: false, kind: Symbol, reason: String}`
72
- def diverged_ancestor_writes(domain_path)
73
- registry, directory = load_registry(domain_path)
74
- bluebook = registry.bluebooks.values.first
75
- return failed("no bluebook in #{directory}") unless bluebook
76
-
77
- first = bluebook.aggregates.first
78
- return not_applicable("#{bluebook.name} declares no aggregates") unless first
79
-
80
- adapter_name = Hecks::Ports::Persistence::BindingPolicy.resolve(registry, bluebook.name, first).adapter
81
- unless adapter_name == "PostgresEra"
82
- return not_applicable("#{bluebook.name} is bound to #{adapter_name}, not PostgresEra")
83
- end
84
-
85
- settings = registry.world(bluebook.name)&.for_binding(Hecks::Ports::Persistence::VERB, adapter_name) || {}
86
- db = Hecks::Adapters::PostgresEra.connect_for(bluebook.name, settings)
87
- begin
88
- lineage = Hecks::Adapters::PostgresEra::Lineage.new(db, bluebook.name)
89
- lineage.ensure_base!
90
- eras = lineage.eras
91
- # `bin/merge_tail`'s own arithmetic, restated read-only: every
92
- # ancestor era (every ordinal strictly before the head's own) may
93
- # still hold post-cut writes the head has never interleaved.
94
- breakdown = if eras.size > 1
95
- (1...eras.last[:ordinal]).map do |ordinal|
96
- { ordinal: ordinal, diverged: lineage.diverged_count(ordinal) }
97
- end
98
- else
99
- []
100
- end
101
- { checked: true, era_count: eras.size, breakdown: breakdown, diverged_total: breakdown.sum { |b| b[:diverged] } }
102
- ensure
103
- db.close
104
- end
105
- rescue StandardError => e
106
- # Could not audit is not the same as nothing to audit — a refused
107
- # connection, a `Lineage` defect, malformed `.world` settings. Each
108
- # of these answers `:error`, not the benign `:not_applicable` shape,
109
- # so `bin/qa_sweep` surfaces it as a finding rather than holding it
110
- # as a clean audit.
111
- failed("#{e.class}: #{e.message}")
112
- end
113
-
114
- # Builds the `checked: false` result for a target with no PostgresEra
115
- # lineage to audit — no ancestor era can carry post-cut writes when
116
- # there is no lineage at all. Not a finding, and not evidence of
117
- # anything either: the caller logs no Check at all.
118
- #
119
- # @param reason [String] human-readable explanation of why no lineage applies
120
- # @return [Hash{Symbol => Object}] `{checked: false, kind: :not_applicable, reason: String}`
121
- def not_applicable(reason) = { checked: false, kind: :not_applicable, reason: reason }
122
-
123
- # Builds the `checked: false` result for an audit that could not run
124
- # — reported as a finding, never held.
125
- #
126
- # @param reason [String] human-readable explanation of what failed, typically
127
- # `"#{exception.class}: #{exception.message}"`
128
- # @return [Hash{Symbol => Object}] `{checked: false, kind: :error, reason: String}`
129
- def failed(reason) = { checked: false, kind: :error, reason: reason }
130
-
131
- # Boots a fresh registry for the domain at `domain_path` — the same
132
- # load `bin/merge_tail` itself performs (that script's own top half),
133
- # never the ledger's own registry (this asks about the swept target's
134
- # lineage, not QualityControl's own).
135
- #
136
- # @param domain_path [String] filesystem path to the target domain's bluebook
137
- # directory, or a directory containing one
138
- # @return [Array(Runtime::Registry, String)] the booted registry and the resolved
139
- # bluebook directory path
140
- # @raise [Errno::ENOENT] if `domain_path` names no existing domain directory
141
- def load_registry(domain_path)
142
- loading = Hecks::Ports::Loading.bootstrap
143
- directory = loading.bluebook_directory(domain_path)
144
- registry = Hecks::Runtime::Registry.new(root: File.dirname(directory))
145
- Hecks.with_registry(registry) do
146
- loading.load_library
147
- loading.load_project(loading.shared_root(nil, directory))
148
- loading.load_domain(directory)
149
- end
150
- [registry, directory]
151
- end
152
- end
153
- end
154
- end
@@ -1,285 +0,0 @@
1
- require "json"
2
- require_relative "../corpus"
3
-
4
- module Hecks
5
- module Fuzzing
6
- # What an aggregate can exhibit, and which pairs it puts together.
7
- #
8
- # Extracted from `spec/combination_coverage_spec.rb`'s own pairwise
9
- # table so it has exactly two consumers that can never drift: that
10
- # spec (the golden corpus, held to every pair) and `bin/qa_domain_
11
- # novelty` (a candidate stress domain, measured against every path
12
- # the QA ledger already sweeps — see that script's own header for why
13
- # a new domain has to name the pair no existing target meets before
14
- # it earns a place in the rotation).
15
- #
16
- # **The unit is one aggregate**. Two forms in the same chapter but
17
- # different heads never meet at dispatch; two forms on one head do
18
- # — that spec's own header has the four defects that argument came
19
- # from. Each entry below is a form the language declares and a
20
- # runtime has to handle, chosen because it has produced a defect or
21
- # sits one step from one; adding one here is how a new form joins
22
- # both gates at once, and it will name its own uncovered pairs on the
23
- # first run of each.
24
- #
25
- # **One flat table, on purpose** — each entry is an independent boolean
26
- # check against the same string-keyed aggregate IR hash (the shape
27
- # `spec/golden/ir/*.json` carries and `Projector::Exporter.call`
28
- # round-trips to through JSON), laid out so every declared form can
29
- # be read, and added to, at a glance.
30
- module FormCensus
31
- # A `given`'s own lookup path crossing at least two references —
32
- # `member.sponsor.standing`, `source.customer.status`: the
33
- # `CommandRules::References#dereference` recursion, walked only
34
- # on a fresh command argument (S12, ADR 0025) and hydrated one
35
- # repository lookup per hop.
36
- TWO_HOP_GIVEN_PATH_LENGTH = 3
37
-
38
- FORMS = {
39
- "composite_id" => ->(a) { (a["identified_by"] || []).size >= 2 },
40
- "has_entity" => ->(a) { entities(a).any? },
41
- "two_entities" => ->(a) { entities(a).size >= 2 },
42
- "composite_piece" => ->(a) { entities(a).any? { |piece| (piece["identified_by"] || []).size >= 2 } },
43
- "multi_emit" => ->(a) { commands(a).any? { |verb| (verb["emits"] || []).size >= 2 } },
44
- "lifecycle" => ->(a) { !a["lifecycle"].nil? },
45
- "piece_lifecycle" => ->(a) { entities(a).any? { |piece| !piece["lifecycle"].nil? } },
46
- "has_query" => ->(a) { queries(a).any? },
47
- "list_attr" => ->(a) { attributes(a).any? { |held| held["list"] } },
48
- "reference_attr" => ->(a) { attributes(a).any? { |held| reference?(held) } },
49
- "closed_set" => ->(a) { (a["value_objects"] || []).any? { |shape| shape["closed_set"] } },
50
- "has_default" => ->(a) { attributes(a).any? { |held| !held["default"].nil? } },
51
- "has_optional" => ->(a) { commands(a).any? { |verb| (verb["attributes"] || []).any? { |held| held["optional"] } } },
52
- # The reference-hop family (angle-2, qa/bluebook ledger) — the
53
- # forms `qa/stress_domains/referral_chain` exists for, absent
54
- # from the census until that domain named them. Each is one
55
- # step from a catalogued gap: `two_hop_given` is `dereference`'s
56
- # own recursion (`DEREFERENCE_DEPTH`); `multi_hop_where` is a
57
- # `/`-chain `HopPath` walks for real and `rust/project/queries.
58
- # rb` structurally refuses (D2 of the equivalence-gap plan);
59
- # `revalued_reference` is ADR 0037 Finding 5's exact trigger — a
60
- # command redeclaring the aggregate's own reference field under
61
- # a non-reference type, so only `resolve_state_references` (never
62
- # ported) can catch a dangling id.
63
- # A retroactive correction, and a role-gated command — both are
64
- # declared forms this census could not see, on aggregates it was
65
- # already measuring: `examples/banking` has carried `corrects`
66
- # mutations and `role`-bearing commands the whole time, and
67
- # `qa/stress_domains/corrections` exists for the first. Three
68
- # stress domains' own NOTES.md record `bin/qa_domain_novelty`
69
- # answering "no new pair" for a domain whose whole point was a
70
- # shape this table did not name (case_escalation's is the
71
- # bluntest: "read that as a gap in the census, not in this
72
- # domain"). Entity commands count for both — BUG#30-33 and
73
- # BUG#31 were all entity-level `corrects`.
74
- "corrects" => ->(a) { every_command(a).any? { |verb| corrects?(verb) } },
75
- "role_gated" => ->(a) { every_command(a).any? { |verb| !verb["role"].to_s.empty? } },
76
- "two_hop_given" => ->(a) { two_hop_given?(a) },
77
- "multi_hop_where" => ->(a) { multi_hop_where?(a) },
78
- "revalued_reference" => ->(a) { revalued_reference?(a) }
79
- }.freeze
80
-
81
- module_function
82
-
83
- # Reads an aggregate IR hash's own declared entities.
84
- #
85
- # @param aggregate [Hash] a string-keyed aggregate IR hash
86
- # @return [Array<Hash>] the aggregate's own declared entities ("pieces"); `[]`
87
- # if it declares none
88
- def entities(aggregate) = aggregate["entities"] || []
89
-
90
- # Reads an aggregate IR hash's own declared commands.
91
- #
92
- # @param aggregate [Hash] a string-keyed aggregate IR hash
93
- # @return [Array<Hash>] the aggregate's own declared commands; `[]` if it
94
- # declares none
95
- def commands(aggregate) = aggregate["commands"] || []
96
-
97
- # One aggregate's commands, its pieces' included — a form carried by
98
- # an entity command is carried by the aggregate that owns it, the
99
- # same way `composite_piece`/`piece_lifecycle` already read pieces.
100
- #
101
- # @param aggregate [Hash] a string-keyed aggregate IR hash
102
- # @return [Array<Hash>] `aggregate`'s own commands, plus every entity's own
103
- def every_command(aggregate) = commands(aggregate) + entities(aggregate).flat_map { |piece| commands(piece) }
104
-
105
- # Answers whether a command IR hash declares a `corrects` mutation.
106
- #
107
- # @param verb [Hash] a string-keyed command IR hash
108
- # @return [Boolean] true if `verb` declares a `corrects` mutation
109
- def corrects?(verb) = (verb["mutations"] || []).any? { |change| change["op"].to_s == "corrects" }
110
-
111
- # Reads an aggregate IR hash's own declared attributes.
112
- #
113
- # @param aggregate [Hash] a string-keyed aggregate IR hash
114
- # @return [Array<Hash>] the aggregate's own declared attributes; `[]` if it
115
- # declares none
116
- def attributes(aggregate) = aggregate["attributes"] || []
117
-
118
- # Reads an aggregate IR hash's own declared queries.
119
- #
120
- # @param aggregate [Hash] a string-keyed aggregate IR hash
121
- # @return [Array<Hash>] the aggregate's own declared queries; `[]` if it
122
- # declares none
123
- def queries(aggregate) = aggregate["queries"] || []
124
-
125
- # Answers whether an attribute IR hash's own declared type is a reference.
126
- #
127
- # @param attribute [Hash] a string-keyed attribute IR hash
128
- # @return [Boolean] true if `attribute`'s own declared type is a reference
129
- def reference?(attribute) = attribute["type"].to_s.start_with?("Reference<")
130
-
131
- # Answers whether any of an aggregate's own commands crosses a two-hop given.
132
- #
133
- # @param aggregate [Hash] a string-keyed aggregate IR hash
134
- # @return [Boolean] true if any command's own given crosses at least two
135
- # references (`TWO_HOP_GIVEN_PATH_LENGTH`)
136
- def two_hop_given?(aggregate)
137
- commands(aggregate).any? { |verb| (verb["givens"] || []).any? { |given| deep_lookup?(given["ast"]) } }
138
- end
139
-
140
- # A `where` whose field crosses two `/` — `member/sponsor/standing`.
141
- #
142
- # @param aggregate [Hash] a string-keyed aggregate IR hash
143
- # @return [Boolean] true if any query's own where clause crosses at least
144
- # two `/` hops
145
- def multi_hop_where?(aggregate)
146
- queries(aggregate).any? { |query| (query["wheres"] || []).any? { |where| where["field"].to_s.count("/") >= 2 } }
147
- end
148
-
149
- # A command attribute sharing a name with one of the aggregate's
150
- # own reference-typed attributes while carrying a different, non-
151
- # reference type — `attribute :member, Handle` against
152
- # `reference_to Member`.
153
- #
154
- # @param aggregate [Hash] a string-keyed aggregate IR hash
155
- # @return [Boolean] true if any command redeclares one of the aggregate's
156
- # own reference-typed attribute names under a non-reference type
157
- def revalued_reference?(aggregate)
158
- references = attributes(aggregate).select { |held| reference?(held) }.to_set { |held| held["name"].to_s }
159
- commands(aggregate).any? do |verb|
160
- (verb["attributes"] || []).any? { |held| references.include?(held["name"].to_s) && !reference?(held) }
161
- end
162
- end
163
-
164
- # Walks a given's own exported AST for any `lookup` whose path is
165
- # long enough to have crossed two references.
166
- #
167
- # @param node [Hash, Array, Object] an AST node (or subtree) from an
168
- # exported given's `"ast"`
169
- # @return [Boolean] true if any `lookup` node's own path is at least
170
- # `TWO_HOP_GIVEN_PATH_LENGTH` long
171
- def deep_lookup?(node)
172
- case node
173
- when Hash
174
- return true if node["op"] == "lookup" && Array(node["path"]).size >= TWO_HOP_GIVEN_PATH_LENGTH
175
-
176
- node.each_value.any? { |child| deep_lookup?(child) }
177
- when Array
178
- node.any? { |child| deep_lookup?(child) }
179
- else
180
- false
181
- end
182
- end
183
-
184
- # Every form, answered for one aggregate — the table above, applied.
185
- #
186
- # @param aggregate [Hash] a string-keyed aggregate IR hash
187
- # @return [Hash{String => Boolean}] every `FORMS` name mapped to whether
188
- # `aggregate` exhibits it
189
- def properties(aggregate)
190
- FORMS.transform_values { |form| form.call(aggregate) }
191
- end
192
-
193
- # Every unordered pair of forms, each rendered "left + right" in
194
- # alphabetical order — the key both gates' excuse tables use.
195
- #
196
- # @return [Array<String>] every unordered pair of `FORMS` names, as
197
- # `"left + right"`
198
- def pairs
199
- FORMS.keys.combination(2).map { |pair| pair_key(*pair) }
200
- end
201
-
202
- # Renders two form names as one sorted pair key.
203
- #
204
- # @param left [String] a form name
205
- # @param right [String] a form name
206
- # @return [String] `left`/`right`, alphabetically ordered, joined as
207
- # `"left + right"`
208
- def pair_key(left, right) = [left, right].sort.join(" + ")
209
-
210
- # `held` is `[[aggregate_name, properties], ...]`. Answers which
211
- # pairs are met on one aggregate, and by which — a Hash from pair
212
- # key to the names carrying it, so a caller can say who.
213
- #
214
- # @param held [Array<Array(String, Hash)>] `[aggregate_name, properties]`
215
- # pairs, `properties` as returned by `#properties`
216
- # @return [Hash{String => Array<String>}] every met pair key mapped to the
217
- # aggregate names that carry it
218
- def covered_pairs(held)
219
- held.each_with_object(Hash.new { |h, k| h[k] = [] }) do |(name, shows), covered|
220
- shows.select { |_, present| present }.keys.combination(2).each do |left, right|
221
- covered[pair_key(left, right)] << name
222
- end
223
- end
224
- end
225
-
226
- # `[[\"Chapter::Aggregate\", properties], ...]` for every aggregate
227
- # a string-keyed chapter IR declares — the same walk the golden
228
- # spec makes over `spec/golden/ir/*.json`.
229
- #
230
- # @param chapter_ir [Hash] a string-keyed chapter IR hash, with `"name"` and
231
- # `"aggregates"`
232
- # @return [Array<Array(String, Hash)>] `["Chapter::Aggregate", properties]`
233
- # pairs, one per declared aggregate, `properties` as returned by
234
- # `#properties`
235
- def aggregates_in(chapter_ir)
236
- (chapter_ir["aggregates"] || []).map do |aggregate|
237
- ["#{chapter_ir['name']}::#{aggregate['name']}", properties(aggregate)]
238
- end
239
- end
240
-
241
- # Where a domain path keeps its bluebooks — see
242
- # `Hecks::Corpus.bluebook_files`, the one definition every corpus
243
- # walk shares. `nil` when neither shape holds a bluebook.
244
- #
245
- # @param domain_path [String] path to a domain directory
246
- # @return [Array<String>, nil] the domain's own `.bluebook` file paths, or
247
- # `nil` if it holds none
248
- def bluebook_files(domain_path)
249
- Hecks::Corpus.bluebook_files(domain_path)
250
- end
251
-
252
- # The same census over a domain on disk, booted the lightweight
253
- # way `bin/model_check` and `Hecks::Codemod.load_bluebook` already
254
- # do (ports and the two in-process adapters, no `Hecks.boot`, no
255
- # live database, no `.hecksagon`: the census reads declared shape,
256
- # and a framework chapter a `.hecksagon` would attach is not this
257
- # domain's own). Only the domain's own chapter is measured — the
258
- # first bluebook loaded, the same "target chapter is always first"
259
- # fact `bin/project_rust` relies on.
260
- # @param domain_path [String] path to a domain directory
261
- # @return [Array<Array(String, Hash)>] `["Chapter::Aggregate", properties]`
262
- # pairs, one per aggregate declared on `domain_path`'s own first-loaded
263
- # chapter (see `Hecks::Projector::Exporter.call`)
264
- # @raise [ArgumentError] if `domain_path` has no bluebook files to measure
265
- def census(domain_path)
266
- root = File.expand_path("../../..", __dir__)
267
- files = bluebook_files(domain_path)
268
- raise ArgumentError, "#{domain_path} has no bluebook/*.bluebook (or *.bluebook) to measure" if files.nil?
269
-
270
- registry = Hecks::Runtime::Registry.new(root: File.expand_path(domain_path))
271
- Hecks.with_registry(registry) do
272
- Kernel.load(File.join(root, "lib/hecks/ports/persistence.port"))
273
- Kernel.load(File.join(root, "lib/hecks/ports/extraction.port"))
274
- Kernel.load(File.join(root, "lib/hecks/adapters/driven/memory.adapter"))
275
- Kernel.load(File.join(root, "lib/hecks/adapters/driven/prism.adapter"))
276
- files.each { |file| Kernel.load(file) }
277
- end
278
-
279
- chapter_name = registry.bluebooks.keys.first
280
- exported = JSON.parse(JSON.generate(Hecks::Projector::Exporter.call(registry).fetch(chapter_name)))
281
- aggregates_in(exported)
282
- end
283
- end
284
- end
285
- end
@@ -1,171 +0,0 @@
1
- require "json"
2
- require_relative "isolated_boot"
3
- require_relative "sequence_generator"
4
- require_relative "replay"
5
- require_relative "shrinker"
6
- require_relative "differential"
7
-
8
- module Hecks
9
- module Fuzzing
10
- # **One generated domain, checked** — the child-process half of
11
- # `bin/qa_generated_domains` (one domain per process: every generated
12
- # domain is named `QaGenerated`, see DomainGenerator's header).
13
- #
14
- # The same comparisons `bin/qa_sweep` runs on a rotation target, over
15
- # sequences generated against a domain nobody wrote: with a compiled
16
- # binary, `Differential.diff` (Ruby vs Rust, self-consistency, declared
17
- # properties); without one, a Ruby-only replay (properties,
18
- # self-consistency, an interpreter crash). A domain that does not even
19
- # boot is `invalid` — a generator defect, not a finding, reported apart.
20
- #
21
- # `match:` is how domain-level shrinking asks "does this smaller domain
22
- # still show the same finding?": only a divergence in the same mode
23
- # whose `Shrinker.signature` contains the original's counts. Verb names
24
- # survive a removal that did not touch them, so the signature does too.
25
- module GeneratedDomainCheck
26
- DIFFERENTIAL_MODES = %i[differential self_consistency properties_in_differential].freeze
27
-
28
- module_function
29
-
30
- # Checks one generated domain over up to `seeds` generated sequences, stopping
31
- # at the first divergence found.
32
- #
33
- # @param domain_path [String] path to the generated domain directory
34
- # @param seeds [Integer] maximum number of seeds to try before declaring clean
35
- # @param steps [Integer] steps to generate per seed
36
- # @param adversarial [Float] fraction of command steps to mutate adversarially
37
- # @param binary [String, nil] path to a compiled Rust conformance binary to
38
- # diff against, or `nil` for a Ruby-only check
39
- # @param differ [Object, nil] duck-typed comparison helper answering
40
- # `RustConformanceHelpers`' interface (adapter-defined; constructed ad hoc
41
- # by callers such as bin/qa_generated_domains), or `nil`
42
- # @param match [Hash, nil] `{"mode" =>, "signature" =>}` from a parent
43
- # domain's own finding — only a divergence matching it counts, for
44
- # domain-level shrinking; `nil` accepts the first divergence found
45
- # @param shrink_budget [Integer] step-level shrink attempts to spend on a
46
- # found finding; `0` skips shrinking
47
- # @return [Hash] `{"status" => "invalid", "error" =>}` if the domain does not
48
- # boot; `{"status" => "clean", "seeds_run" =>}` if no seed diverged;
49
- # otherwise a finding merged with `{"status" => "found", "seeds_run" =>}`
50
- # (see `#finding`), plus `"shrunk_steps"` when `shrink_budget` is positive
51
- def run(domain_path, seeds:, steps:, adversarial:, binary: nil, differ: nil, match: nil, shrink_budget: 0)
52
- error = boot_error(domain_path)
53
- return { "status" => "invalid", "error" => error } if error
54
-
55
- (1..seeds).each do |seed|
56
- finding = check_seed(domain_path, seed, steps, adversarial, binary, differ, match)
57
- next unless finding
58
-
59
- finding["shrunk_steps"] = shrink(domain_path, finding, binary, differ, shrink_budget) if shrink_budget.positive?
60
- return finding.merge("status" => "found", "seeds_run" => seed)
61
- end
62
- { "status" => "clean", "seeds_run" => seeds }
63
- end
64
-
65
- # Boots `domain_path` in an isolated copy, to find out whether it boots at all.
66
- #
67
- # @param domain_path [String] path to the domain directory to boot
68
- # @return [String, nil] `nil` if the boot succeeds; otherwise the raising
69
- # exception's class and first message line
70
- def boot_error(domain_path)
71
- IsolatedBoot.call(domain_path) { |copy| Hecks.boot(copy) }
72
- nil
73
- rescue StandardError, ScriptError => e
74
- "#{e.class}: #{e.message.lines.first&.strip}"
75
- end
76
-
77
- # Generates one sequence for `seed` and checks it for a divergence.
78
- #
79
- # @param domain_path [String] path to the domain directory
80
- # @param seed [Integer] RNG seed for `SequenceGenerator.generate`
81
- # @param steps [Integer] steps to generate
82
- # @param adversarial [Float] fraction of command steps to mutate adversarially
83
- # @param binary [String, nil] path to a compiled Rust conformance binary, or
84
- # `nil` for a Ruby-only check
85
- # @param differ [Object, nil] duck-typed comparison helper answering
86
- # `RustConformanceHelpers`' interface (adapter-defined), or `nil`
87
- # @param match [Hash, nil] `{"mode" =>, "signature" =>}` a divergence must
88
- # match to count, or `nil` to accept the first one found
89
- # @return [Hash, nil] a finding (see `#finding`) if a matching divergence was
90
- # found, or if generation itself crashed; `nil` if the sequence was clean
91
- def check_seed(domain_path, seed, steps, adversarial, binary, differ, match)
92
- sequence = SequenceGenerator.generate(domain_path, seed: seed, steps: steps, adversarial: adversarial)
93
- rescue StandardError => e
94
- finding(seed, :generator, [{ field: "generator_crash", detail: "#{e.class}: #{e.message}" }], [], match)
95
- else
96
- outcomes(domain_path, sequence, binary, differ).each do |mode, divergences|
97
- found = finding(seed, mode, divergences, sequence, match)
98
- return found if found
99
- end
100
- nil
101
- end
102
-
103
- # Runs the comparison appropriate to whether a compiled binary is available:
104
- # `Differential.diff` against Rust when `binary` is given, otherwise a
105
- # Ruby-only replay checked for property and self-consistency divergences.
106
- #
107
- # @param domain_path [String] path to the domain directory
108
- # @param sequence [Array<Hash>] the generated step sequence to replay
109
- # @param binary [String, nil] path to a compiled Rust conformance binary, or
110
- # `nil` for a Ruby-only check
111
- # @param differ [Object, nil] duck-typed comparison helper answering
112
- # `RustConformanceHelpers`' interface (adapter-defined), or `nil`
113
- # @return [Hash{Symbol => Array<Hash>}] one divergence list per mode checked;
114
- # `{ruby_only: [{field: "crash", detail:}]}` if the Ruby-only replay itself
115
- # raised
116
- def outcomes(domain_path, sequence, binary, differ)
117
- return Differential.diff(differ, domain_path, sequence, binary, modes: DIFFERENTIAL_MODES) if binary
118
-
119
- history = Replay.call(domain_path, sequence, self_consistency: true)
120
- { ruby_only: Differential.property_divergences(history),
121
- self_consistency: Differential.self_consistency_divergences(history) }
122
- rescue StandardError => e
123
- { ruby_only: [{ field: "crash", detail: "#{e.class}: #{e.message}" }] }
124
- end
125
-
126
- # Builds a reported finding from a mode's divergences, unless `divergences`
127
- # is empty or `match` names a different signature.
128
- #
129
- # @param seed [Integer] the seed that produced `sequence`
130
- # @param mode [Symbol] the comparison mode the divergences came from
131
- # @param divergences [Array<Hash>] divergence entries for `mode`
132
- # @param sequence [Array<Hash>] the generated step sequence
133
- # @param match [Hash, nil] `{"mode" =>, "signature" =>}` the finding must
134
- # match, or `nil` to accept any
135
- # @return [Hash, nil] `{"seed" =>, "mode" =>, "signature" =>, "steps" =>,
136
- # "divergences" =>}` if `divergences` is non-empty and matches `match`;
137
- # `nil` otherwise
138
- def finding(seed, mode, divergences, sequence, match)
139
- return nil if divergences.empty?
140
-
141
- signature = Shrinker.signature(divergences)
142
- return nil if match && !(match["mode"] == mode.to_s && Set.new(match["signature"]).subset?(signature))
143
-
144
- { "seed" => seed, "mode" => mode.to_s, "signature" => signature.to_a.sort, "steps" => sequence,
145
- "divergences" => JSON.parse(JSON.generate(divergences)) }
146
- end
147
-
148
- # The step-level half of shrinking, in this same process: the domain
149
- # is already as small as the parent could make it.
150
- #
151
- # @param domain_path [String] path to the (already domain-shrunk) domain
152
- # directory
153
- # @param found [Hash] the finding to shrink, as returned by `#finding`
154
- # @param binary [String, nil] path to a compiled Rust conformance binary, or
155
- # `nil` for a Ruby-only check
156
- # @param differ [Object, nil] duck-typed comparison helper answering
157
- # `RustConformanceHelpers`' interface (adapter-defined), or `nil`
158
- # @param budget [Integer] step-level shrink attempts to spend
159
- # @return [Array<Hash>] the smallest step list found that still reproduces
160
- # `found`'s divergence signature
161
- def shrink(domain_path, found, binary, differ, budget)
162
- original = Set.new(found["signature"])
163
- mode = found["mode"].to_sym
164
- Shrinker.call(found["steps"], budget: budget) do |candidate|
165
- divergences = outcomes(domain_path, candidate, binary, differ)[mode] || []
166
- Shrinker.reproduces?(original, divergences)
167
- end.steps
168
- end
169
- end
170
- end
171
- end