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.
- checksums.yaml +4 -4
- data/exe/hecks +13 -0
- data/lib/hecks/adapters/driven/memory.rb +1 -1
- data/lib/hecks/adapters/driven/postgres.rb +1 -1
- data/lib/hecks/adapters/driven/prism.rb +1 -1
- data/lib/hecks/adapters/driven/sqlite/projection.rb +1 -1
- data/lib/hecks/adapters/driven/tenant_provisioner.rb +31 -10
- data/lib/hecks/behaviors/expectations.rb +2 -2
- data/lib/hecks/bluebook/assembly/contract.rb +1 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +5 -5
- data/lib/hecks/bluebook/assembly/specializer.rb +2 -2
- data/lib/hecks/bluebook/assembly.rb +1 -1
- data/lib/hecks/bluebook/behaviour/chapter.rb +5 -2
- data/lib/hecks/bluebook/behaviour/read_model.rb +18 -0
- data/lib/hecks/bluebook/capabilities.rb +2 -2
- data/lib/hecks/bluebook/chapter.rb +1 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +6 -1
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +2 -2
- data/lib/hecks/bluebook/dsl/entity_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +1 -1
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +14 -9
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -3
- data/lib/hecks/bluebook/dsl/translation_builder.rb +1 -1
- data/lib/hecks/bluebook/dsl/world_builder.rb +5 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -4
- data/lib/hecks/bluebook/meta_validator/readings.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +2 -2
- data/lib/hecks/bluebook/meta_validator/shapes.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +11 -10
- data/lib/hecks/bluebook/meta_validator.rb +1 -1
- data/lib/hecks/bluebook/model_check/client_profile.rb +4 -70
- data/lib/hecks/bluebook/model_check.rb +12 -13
- data/lib/hecks/cache_dir.rb +103 -0
- data/lib/hecks/cli/document.rb +104 -0
- data/lib/hecks/cli/ir.rb +47 -0
- data/lib/hecks/cli/mcp.rb +32 -0
- data/lib/hecks/cli/mcp_door.rb +461 -0
- data/lib/hecks/cli/model_check.rb +270 -0
- data/lib/hecks/cli/project_cli.rb +123 -0
- data/lib/hecks/cli/project_diagrams.rb +43 -0
- data/lib/hecks/cli/run.rb +219 -0
- data/lib/hecks/cli/smoke_test.rb +59 -0
- data/lib/hecks/cli/stores.rb +54 -0
- data/lib/hecks/cli.rb +166 -0
- data/lib/hecks/facade/handle.rb +2 -2
- data/lib/hecks/facade/surface/aggregate_door.rb +1 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +16 -0
- data/lib/hecks/mcp_door_scope.rb +176 -0
- data/lib/hecks/ports/persistence/append_only.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +12 -6
- data/lib/hecks/ports/persistence/state_codec.rb +1 -1
- data/lib/hecks/projections/deploy/fargate.rb +1 -1
- data/lib/hecks/projections/deploy/lambda.rb +3 -3
- data/lib/hecks/projections/reference.rb +7 -1
- data/lib/hecks/projector/exporter.rb +4 -4
- data/lib/hecks/projector/target.rb +1 -1
- data/lib/hecks/release/runner/ci_publisher.rb +96 -0
- data/lib/hecks/release/runner/commands.rb +68 -0
- data/lib/hecks/release/runner/console.rb +52 -0
- data/lib/hecks/release/runner/gem_publisher.rb +44 -0
- data/lib/hecks/release/runner/git.rb +48 -0
- data/lib/hecks/release/runner/npm_publisher.rb +89 -0
- data/lib/hecks/release/runner/preflight.rb +119 -0
- data/lib/hecks/release/runner/published.rb +55 -0
- data/lib/hecks/release/runner/tagger.rb +87 -0
- data/lib/hecks/release/runner.rb +244 -0
- data/lib/hecks/router/namespace_installer.rb +1 -1
- data/lib/hecks/runtime/command_interpreter.rb +3 -3
- data/lib/hecks/runtime/command_rules/admissibility.rb +2 -2
- data/lib/hecks/runtime/entity_element.rb +1 -1
- data/lib/hecks/runtime/entity_interpreter.rb +1 -1
- data/lib/hecks/runtime/invocation.rb +1 -1
- data/lib/hecks/runtime/loader.rb +1 -1
- data/lib/hecks/runtime/outbox.rb +1 -1
- data/lib/hecks/runtime/reaction_invocation.rb +3 -3
- data/lib/hecks/runtime/read_model_interpreter.rb +37 -15
- data/lib/hecks/runtime/reference_hop.rb +4 -4
- data/lib/hecks/runtime/registry/verification.rb +11 -11
- data/lib/hecks/runtime/registry.rb +8 -4
- data/lib/hecks/runtime/saga_interpreter.rb +1 -1
- data/lib/hecks/runtime/value/coercion.rb +3 -3
- data/lib/hecks/runtime/value/entity_list_coercion.rb +1 -1
- data/lib/hecks/storehouse.rb +18 -12
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +5 -0
- data/lib/hecks.rb +6 -1
- metadata +28 -58
- data/lib/hecks/bench/cli.rb +0 -74
- data/lib/hecks/bench/environment.rb +0 -100
- data/lib/hecks/bench/postgres_probe.rb +0 -41
- data/lib/hecks/bench/report.rb +0 -116
- data/lib/hecks/bench/ruby_runner.rb +0 -151
- data/lib/hecks/bench/run.rb +0 -55
- data/lib/hecks/bench/rust_runner.rb +0 -165
- data/lib/hecks/bench/stats.rb +0 -75
- data/lib/hecks/bench/suite.rb +0 -172
- data/lib/hecks/bench/workload.rb +0 -137
- data/lib/hecks/bench.rb +0 -28
- data/lib/hecks/codemod.rb +0 -443
- data/lib/hecks/corpus.rb +0 -479
- data/lib/hecks/doc/reference.rb +0 -585
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
- data/lib/hecks/fuzzing/combination_miner.rb +0 -178
- data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
- data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
- data/lib/hecks/fuzzing/differential.rb +0 -192
- data/lib/hecks/fuzzing/domain_generator.rb +0 -871
- data/lib/hecks/fuzzing/era_boundary.rb +0 -154
- data/lib/hecks/fuzzing/form_census.rb +0 -285
- data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
- data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
- data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
- data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
- data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
- data/lib/hecks/fuzzing/properties/guards.rb +0 -398
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
- data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
- data/lib/hecks/fuzzing/properties/querying.rb +0 -260
- data/lib/hecks/fuzzing/properties.rb +0 -267
- data/lib/hecks/fuzzing/qa_settings.rb +0 -164
- data/lib/hecks/fuzzing/replay.rb +0 -930
- data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
- data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
- data/lib/hecks/fuzzing/self_consistency.rb +0 -825
- data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
- data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
- data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
- data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
- data/lib/hecks/fuzzing/shrinker.rb +0 -309
- data/lib/hecks/fuzzing/structural_skips.rb +0 -53
- data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
- data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
- data/lib/hecks/fuzzing/value_generator.rb +0 -340
- data/lib/hecks/fuzzing.rb +0 -23
- data/lib/hecks/grammar/evolve.rb +0 -524
- data/lib/hecks/query_ir.rb +0 -482
data/lib/hecks/query_ir.rb
DELETED
|
@@ -1,482 +0,0 @@
|
|
|
1
|
-
require_relative "bluebook"
|
|
2
|
-
require_relative "codemod"
|
|
3
|
-
require_relative "projections/model"
|
|
4
|
-
require_relative "fuzzing/properties"
|
|
5
|
-
|
|
6
|
-
module Hecks
|
|
7
|
-
# The shared core behind `bin/query_ir` (a text CLI) and
|
|
8
|
-
# `bin/hecks_query_ir_mcp` (an MCP server exposing the same two
|
|
9
|
-
# queries as tools) — one implementation, two front ends, the same
|
|
10
|
-
# reason `Hecks::Codemod` exists once rather than per-script.
|
|
11
|
-
# Every method here returns structured data (Hashes/Arrays/Structs),
|
|
12
|
-
# never formatted text — formatting is each front end's own job.
|
|
13
|
-
module QueryIR
|
|
14
|
-
Codemod = Hecks::Codemod
|
|
15
|
-
Deviations = Hecks::Projections::Model::Deviations
|
|
16
|
-
|
|
17
|
-
# The same mapping spec/model_shape_conformance_spec.rb's own
|
|
18
|
-
# MODEL_CONSTRUCTS holds — kept here rather than shared from the
|
|
19
|
-
# spec (a spec file is not a library other code should require),
|
|
20
|
-
# matching Deviations' own doc comment: "the generator and the gate
|
|
21
|
-
# read one source" — Deviations is that one source; this table is
|
|
22
|
-
# small and genuinely construct-list bookkeeping, not a rule that
|
|
23
|
-
# can drift silently the way a Deviations entry could.
|
|
24
|
-
CONSTRUCTS = {
|
|
25
|
-
"Bluebook" => Hecks::Bluebook::Chapter,
|
|
26
|
-
"Aggregate" => Hecks::Bluebook::Aggregate,
|
|
27
|
-
"Command" => Hecks::Bluebook::Command,
|
|
28
|
-
"Entity" => Hecks::Bluebook::Entity,
|
|
29
|
-
"ValueObject" => Hecks::Bluebook::ValueObject,
|
|
30
|
-
"Policy" => Hecks::Bluebook::Policy,
|
|
31
|
-
"Query" => Hecks::Bluebook::Query,
|
|
32
|
-
"ReadModel" => Hecks::Bluebook::ReadModel,
|
|
33
|
-
"ProcessManager" => Hecks::Bluebook::ProcessManager
|
|
34
|
-
}.freeze
|
|
35
|
-
|
|
36
|
-
Rule = Struct.new(:kind, :description, :canonical, :location, keyword_init: true)
|
|
37
|
-
|
|
38
|
-
module_function
|
|
39
|
-
|
|
40
|
-
# Reads the fields the language declares for one construct kind, from the grammar itself.
|
|
41
|
-
#
|
|
42
|
-
# @param name [String] a `CONSTRUCTS` key, such as `"Aggregate"`
|
|
43
|
-
# @return [Array<Symbol>] the attribute names the self-hosted meta-domain
|
|
44
|
-
# grammar declares for `name`
|
|
45
|
-
def meta_declared(name)
|
|
46
|
-
Hecks::Bluebook::MetaValidator.grammar_registry
|
|
47
|
-
.bluebook("Bluebook").aggregate(name).attributes.map(&:name)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# The real structural diff between what a Ruby IR class emits
|
|
51
|
-
# (Class.ir_spec.keys) and what the self-hosted meta-domain declares
|
|
52
|
-
# for it — the same comparison spec/model_shape_conformance_spec.rb
|
|
53
|
-
# makes, reusing its own Deviations data so this can never silently
|
|
54
|
-
# drift from what that gate actually checks.
|
|
55
|
-
#
|
|
56
|
-
# @param name [String] a `CONSTRUCTS` key, such as `"Aggregate"`
|
|
57
|
-
# @return [Hash{Symbol => Object}] `:name`, `:declared`, `:emitted`, plus
|
|
58
|
-
# `:missing_from_ruby` and `:unaccounted_in_ruby` — each an `Array<Symbol>`
|
|
59
|
-
# @raise [ArgumentError] if `name` is not a `CONSTRUCTS` key
|
|
60
|
-
def construct_diff(name)
|
|
61
|
-
klass = CONSTRUCTS.fetch(name) do
|
|
62
|
-
raise ArgumentError, "no such construct #{name.inspect} — known: #{CONSTRUCTS.keys.join(', ')}"
|
|
63
|
-
end
|
|
64
|
-
declared = meta_declared(name)
|
|
65
|
-
emitted = klass.ir_spec.keys
|
|
66
|
-
|
|
67
|
-
accounted = declared.reject { |field| Deviations.parent_ref?(field) } -
|
|
68
|
-
Deviations.judge_only(name) -
|
|
69
|
-
Deviations.folded(name).values.flatten -
|
|
70
|
-
Deviations.off_the_wire(name) -
|
|
71
|
-
Deviations.dynamic_tail(name) -
|
|
72
|
-
Deviations.unpacked(name).keys
|
|
73
|
-
|
|
74
|
-
unaccounted = emitted -
|
|
75
|
-
declared -
|
|
76
|
-
Deviations.contained(name) -
|
|
77
|
-
Deviations.folded(name).keys -
|
|
78
|
-
Deviations.computed(name) -
|
|
79
|
-
Deviations.unpacked(name).values.flatten
|
|
80
|
-
|
|
81
|
-
{ name: name, declared: declared, emitted: emitted,
|
|
82
|
-
missing_from_ruby: accounted - emitted, unaccounted_in_ruby: unaccounted }
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Diffs one or every construct kind at once.
|
|
86
|
-
#
|
|
87
|
-
# @param names [Array<String>] `CONSTRUCTS` keys to diff, every construct when empty
|
|
88
|
-
# @return [Array<Hash>] one `construct_diff` result per name
|
|
89
|
-
def constructs(names = [])
|
|
90
|
-
targets = names.empty? ? CONSTRUCTS.keys : names
|
|
91
|
-
targets.map { |name| construct_diff(name) }
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
# Every given/ensures/invariant declaration reachable from a booted
|
|
95
|
-
# registry, walked recursively — every owner's own `.preconditions`/
|
|
96
|
-
# `.invariants` (block-declared rules), every value object's own
|
|
97
|
-
# `.invariants`, and every command's own `.givens`/`.ensures`
|
|
98
|
-
# (`.givens` too, not just `.ensures` — a command's own local
|
|
99
|
-
# `given("x") { block }` not yet hoisted to its owner, round 4's own
|
|
100
|
-
# starting shape, would otherwise be invisible).
|
|
101
|
-
#
|
|
102
|
-
# Not keyed by object identity (a real, hard-won correction — see
|
|
103
|
-
# the comment on `duplicates`' own dedup below for why: a bare
|
|
104
|
-
# `given("x")` reference and its owner's own block declaration are
|
|
105
|
-
# the same Ruby object at DSL build time, but `MetaValidator.call`
|
|
106
|
-
# (S14 — every bluebook is judged by dispatching its own IR into the
|
|
107
|
-
# self-hosted grammar, then reconstructed via `Assembly.call` from
|
|
108
|
-
# flat rows) rebuilds the whole graph fresh from there. By the time
|
|
109
|
-
# any caller reads `chapter.aggregates`, every given/invariant is
|
|
110
|
-
# already a distinct object, whether it was block-declared or
|
|
111
|
-
# bare-referenced — object identity carries no signal past that
|
|
112
|
-
# point, for any construct, not just this one).
|
|
113
|
-
#
|
|
114
|
-
# @param registry [Runtime::Registry] the booted registry to walk
|
|
115
|
-
# @param chapter_name [String, nil] one chapter to walk, every booted chapter when nil
|
|
116
|
-
# @return [Array<Rule>]
|
|
117
|
-
def collect_rules(registry, chapter_name = nil)
|
|
118
|
-
rules = []
|
|
119
|
-
chapters = chapter_name ? [registry.bluebook(chapter_name)] : registry.bluebooks.values
|
|
120
|
-
|
|
121
|
-
chapters.each do |chapter|
|
|
122
|
-
chapter.aggregates.each do |aggregate|
|
|
123
|
-
walk_construct_rules(aggregate, aggregate.hecks_name, rules)
|
|
124
|
-
aggregate.value_objects.each do |vo|
|
|
125
|
-
vo.invariants.each do |rule|
|
|
126
|
-
rules << Rule.new(kind: "invariant", description: rule.description, canonical: rule.canonical,
|
|
127
|
-
location: "#{aggregate.hecks_name}::#{vo.hecks_name} (declared)")
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
rules
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# The recursive walk `collect_rules` drives — pulled out of that method
|
|
136
|
-
# (pure extraction, identical traversal and Rule shapes) as its own
|
|
137
|
-
# named, self-recursive method rather than a lambda closing over the
|
|
138
|
-
# same locals. `rules` is the one piece of state every call shares —
|
|
139
|
-
# threaded as a parameter and mutated in place, the same accumulator
|
|
140
|
-
# role the lambda's own closure played.
|
|
141
|
-
def walk_construct_rules(construct, path, rules)
|
|
142
|
-
(construct.respond_to?(:preconditions) ? construct.preconditions : []).each do |rule|
|
|
143
|
-
rules << Rule.new(kind: "given", description: rule.description, canonical: rule.canonical,
|
|
144
|
-
location: "#{path} (declared)")
|
|
145
|
-
end
|
|
146
|
-
(construct.respond_to?(:invariants) ? construct.invariants : []).each do |rule|
|
|
147
|
-
rules << Rule.new(kind: "invariant", description: rule.description, canonical: rule.canonical,
|
|
148
|
-
location: "#{path} (declared)")
|
|
149
|
-
end
|
|
150
|
-
construct.commands.each do |command|
|
|
151
|
-
command.givens.each do |rule|
|
|
152
|
-
rules << Rule.new(kind: "given", description: rule.description, canonical: rule.canonical,
|
|
153
|
-
location: "#{path}.#{command.hecks_name}")
|
|
154
|
-
end
|
|
155
|
-
command.ensures.each do |rule|
|
|
156
|
-
rules << Rule.new(kind: "ensures", description: rule.description, canonical: rule.canonical,
|
|
157
|
-
location: "#{path}.#{command.hecks_name}")
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
return unless construct.respond_to?(:entities)
|
|
161
|
-
|
|
162
|
-
construct.entities.each do |piece|
|
|
163
|
-
walk_construct_rules(piece, "#{path}.#{piece.hecks_name}", rules)
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
private_class_method :walk_construct_rules
|
|
167
|
-
|
|
168
|
-
# A rule's owner — the construct path a "(declared)" location names
|
|
169
|
-
# directly, or (for a command-level `.givens`/`.ensures` entry) the
|
|
170
|
-
# path with its trailing `.CommandName` segment stripped. Two rules
|
|
171
|
-
# sharing an owner are the same declaration read twice (an owner's
|
|
172
|
-
# own precondition, and a command under it referencing that
|
|
173
|
-
# precondition by name) — not two independent ones.
|
|
174
|
-
#
|
|
175
|
-
# Public, not a `duplicates`-only internal — `bin/codemod_hoist_
|
|
176
|
-
# local_givens` reads it directly to group `collect_rules`' own
|
|
177
|
-
# output by owner itself, the same reading `duplicates`' own
|
|
178
|
-
# `declaration_count` makes.
|
|
179
|
-
#
|
|
180
|
-
# @param location [String] a `Rule#location`
|
|
181
|
-
# @return [String] the owning construct's path, with `" (declared)"` or a trailing
|
|
182
|
-
# `.CommandName` segment stripped
|
|
183
|
-
def owner_of(location)
|
|
184
|
-
return location.sub(/ \(declared\)\z/, "") if location.end_with?(" (declared)")
|
|
185
|
-
|
|
186
|
-
location.rpartition(".").first
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
# Grouped by (kind, description, canonical), not canonical text
|
|
190
|
-
# alone — a generic one-liner like `!value.to_s.empty?` legitimately
|
|
191
|
-
# recurs dozens of times for unrelated fields; the real signal is
|
|
192
|
-
# the same rule (same description, same predicate), which is also
|
|
193
|
-
# exactly what the given/invariant reference mechanism itself
|
|
194
|
-
# resolves on.
|
|
195
|
-
#
|
|
196
|
-
# Deduped by owner, not object identity (`collect_rules`' own
|
|
197
|
-
# comment has the full story — identity is gone by the time this
|
|
198
|
-
# reads the registry). Within a group, every command-level rule
|
|
199
|
-
# whose owner already has its own "(declared)" entry in the same
|
|
200
|
-
# group is just that declaration read again through a reference —
|
|
201
|
-
# `Account.Open`/`Account.Credit`/etc. all naming `Account`'s own
|
|
202
|
-
# `given("customer is active")` count as Account's one declaration,
|
|
203
|
-
# not nine. A command-level rule with no matching owner declaration
|
|
204
|
-
# (a local, not-yet-hoisted `given("x") { block }`) counts as its
|
|
205
|
-
# own standalone declaration — two different commands independently
|
|
206
|
-
# writing the identical local predicate is two declarations, a real
|
|
207
|
-
# hoisting opportunity. A group is reported only when it adds up to
|
|
208
|
-
# more than one real declaration this way.
|
|
209
|
-
#
|
|
210
|
-
# `domains: []` means "the self-hosted meta-domain only" — pass real
|
|
211
|
-
# domain directories explicitly to include them, or `nil` (the
|
|
212
|
-
# default) for meta-domain plus every real example.
|
|
213
|
-
#
|
|
214
|
-
# @param domains [Array<String>, nil] domain root directories to scan; every real
|
|
215
|
-
# example (`Codemod::EXAMPLE_ROOTS`) when nil, none beyond the meta-domain when `[]`
|
|
216
|
-
# @param include_meta [Boolean] whether to also scan the self-hosted meta-domain
|
|
217
|
-
# @return [Array<Hash>] one entry per duplicate group: `:kind`, `:description`,
|
|
218
|
-
# `:canonical`, and `:locations` (`Array<String>`)
|
|
219
|
-
def duplicates(domains: nil, include_meta: true)
|
|
220
|
-
domains ||= Codemod::EXAMPLE_ROOTS
|
|
221
|
-
all_rules = []
|
|
222
|
-
all_rules.concat(collect_rules(Codemod.meta_registry)) if include_meta
|
|
223
|
-
|
|
224
|
-
domains.each do |domain_dir|
|
|
225
|
-
bluebook_files = Dir.glob(File.join(domain_dir, "bluebook", "*.bluebook"))
|
|
226
|
-
next if bluebook_files.empty?
|
|
227
|
-
|
|
228
|
-
registry = Codemod.load_bluebook(bluebook_files)
|
|
229
|
-
all_rules.concat(collect_rules(registry))
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
all_rules.group_by { |r| [r.kind, r.description, r.canonical] }
|
|
233
|
-
.map { |key, rules| [key, rules, declaration_count(rules)] }
|
|
234
|
-
.select { |_, _, count| count > 1 }
|
|
235
|
-
.map do |(kind, description, canonical), rules, _|
|
|
236
|
-
{ kind: kind, description: description, canonical: canonical,
|
|
237
|
-
locations: rules.map(&:location) }
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
# `given` only (not `invariant`/`ensures`) also covers cross-entity
|
|
242
|
-
# coverage — one piece's own entity-level declaration, shared with
|
|
243
|
-
# any other piece nested under the same root aggregate (real corpus
|
|
244
|
-
# this closes: SafeDepositBox's own `Visit`/`KeyIssuance`, two
|
|
245
|
-
# different pieces on one head). A rule owned by a nested entity
|
|
246
|
-
# (its own owner path has more than one segment) is covered when
|
|
247
|
-
# some "(declared)" entry exists anywhere under that same root
|
|
248
|
-
# aggregate — not just under its own exact owner — matching the
|
|
249
|
-
# DSL's own pool, threaded unchanged through an aggregate's whole
|
|
250
|
-
# entity tree (`AggregateBuilder#entity`'s own comment).
|
|
251
|
-
#
|
|
252
|
-
# A known, accepted gap this does not (and structurally cannot)
|
|
253
|
-
# close: chapter-wide given sharing (`AggregateBuilder#given`'s own
|
|
254
|
-
# bare form, `docs/implemented/resolution-rules/chapter-given.md`) — `Account`,
|
|
255
|
-
# `SafeDepositBox`, and `OnboardingCase` each still show as their
|
|
256
|
-
# own "(declared)" owner here even after `SafeDepositBox`/
|
|
257
|
-
# `OnboardingCase` were converted to bare chapter-wide references,
|
|
258
|
-
# because a referenced given still write-throughs into its own
|
|
259
|
-
# aggregate's `@named_givens` — the same reason `collect_rules`'
|
|
260
|
-
# own top comment already gives for why object identity carries no
|
|
261
|
-
# signal past a bluebook's own build: the exported IR cannot tell
|
|
262
|
-
# "I declared this myself" apart from "I referenced someone else's
|
|
263
|
-
# declaration," because by the time anything reads `chapter.
|
|
264
|
-
# aggregates`, both look identical. Closing this would mean reading
|
|
265
|
-
# source text (bare `given(desc)` vs. block `given(desc) { ... }`),
|
|
266
|
-
# not the built IR this query is deliberately built on — a
|
|
267
|
-
# different, source-level tool, not a fix to this one. Treat a
|
|
268
|
-
# still-flagged group naming multiple aggregates as "verify by
|
|
269
|
-
# hand whether this is already a chapter-wide reference before
|
|
270
|
-
# assuming it's fresh duplication," not as an automatic signal
|
|
271
|
-
# either way.
|
|
272
|
-
#
|
|
273
|
-
# The identical gap, one level down: chapter-wide entity-scoped
|
|
274
|
-
# sharing (`EntityBuilder#given`'s own bare form,
|
|
275
|
-
# `docs/implemented/resolution-rules/chapter-entity-given.md`) hits this same wall for
|
|
276
|
-
# the same structural reason — `SafeDepositBox.Visit` still shows
|
|
277
|
-
# as its own "(declared)" owner here even after becoming a bare
|
|
278
|
-
# reference to `Account.LedgerEntry`'s declaration, because a piece
|
|
279
|
-
# resolving a chapter-wide reference still write-throughs the
|
|
280
|
-
# resolved `Given` into its own `@named_givens` (so its own
|
|
281
|
-
# commands can read it back locally without a second hop). This is
|
|
282
|
-
# not a new limitation this feature introduces — it is the exact
|
|
283
|
-
# same IR-cannot-distinguish-declared-from-referenced fact, one
|
|
284
|
-
# scope wider. `bin/query_ir duplicates` confirms this directly:
|
|
285
|
-
# `Account.LedgerEntry (declared)` and `SafeDepositBox.Visit
|
|
286
|
-
# (declared)` both appear under the same `given: "customer is
|
|
287
|
-
# active"` group — verify by hand, same as the aggregate-level
|
|
288
|
-
# case above, before assuming a group naming two pieces under
|
|
289
|
-
# different aggregates is fresh duplication rather than an already-
|
|
290
|
-
# resolved chapter-wide reference.
|
|
291
|
-
#
|
|
292
|
-
# @param rules [Array<Rule>] one duplicate-key group's own rules
|
|
293
|
-
# @return [Integer] how many of `rules` are real, independent declarations
|
|
294
|
-
def declaration_count(rules)
|
|
295
|
-
declared = rules.select { |r| r.location.end_with?(" (declared)") }
|
|
296
|
-
declared_owners = declared.to_set { |r| owner_of(r.location) }
|
|
297
|
-
declared_given_roots = declared.select { |r| r.kind == "given" }
|
|
298
|
-
.to_set { |r| owner_of(r.location).split(".").first }
|
|
299
|
-
|
|
300
|
-
standalone = rules.reject do |r|
|
|
301
|
-
owner = owner_of(r.location)
|
|
302
|
-
next true if declared_owners.include?(owner)
|
|
303
|
-
next true if r.kind == "given" && owner.include?(".") && declared_given_roots.include?(owner.split(".").first)
|
|
304
|
-
|
|
305
|
-
false
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
declared_owners.size + standalone.size
|
|
309
|
-
end
|
|
310
|
-
private_class_method :declaration_count
|
|
311
|
-
|
|
312
|
-
# Shared text formatting — both `bin/query_ir` (a text CLI) and
|
|
313
|
-
# `bin/hecks_query_ir_mcp` (an MCP tool result, itself a text
|
|
314
|
-
# block) want the identical human-readable rendering; only the
|
|
315
|
-
# outer framing differs (plain stdout vs. a JSON-RPC content array).
|
|
316
|
-
# @param diffs [Array<Hash>] `constructs`' own output
|
|
317
|
-
# @return [String] the human-readable rendering
|
|
318
|
-
def format_constructs(diffs)
|
|
319
|
-
diffs.map do |diff|
|
|
320
|
-
lines = ["== #{diff[:name]} =="]
|
|
321
|
-
lines << " emits: #{diff[:emitted].join(', ')}"
|
|
322
|
-
lines << " declares: #{diff[:declared].join(', ')}"
|
|
323
|
-
if diff[:missing_from_ruby].empty? && diff[:unaccounted_in_ruby].empty?
|
|
324
|
-
lines << " clean — every declared field is emitted (or a named deviation), nothing emitted is undeclared"
|
|
325
|
-
else
|
|
326
|
-
unless diff[:missing_from_ruby].empty?
|
|
327
|
-
lines << " MISSING FROM RUBY (declared, not emitted, not a named deviation): " \
|
|
328
|
-
"#{diff[:missing_from_ruby].join(', ')}"
|
|
329
|
-
end
|
|
330
|
-
unless diff[:unaccounted_in_ruby].empty?
|
|
331
|
-
lines << " UNACCOUNTED IN RUBY (emitted, not declared, not a named deviation): " \
|
|
332
|
-
"#{diff[:unaccounted_in_ruby].join(', ')}"
|
|
333
|
-
end
|
|
334
|
-
end
|
|
335
|
-
lines.join("\n")
|
|
336
|
-
end.join("\n\n")
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
# **One hand-typed construct-name per reconstruction method** — the only
|
|
340
|
-
# two `MetaValidator::Reconstruction` methods not driven generically
|
|
341
|
-
# through `Assembly::Contracts`' own table (its own header explains
|
|
342
|
-
# why: `aggregate(row)`/`entity(row)` predate the table and were
|
|
343
|
-
# never migrated). `impact_preview`'s own touchpoint 4 is checked
|
|
344
|
-
# only for these two — every other construct is read generically, so
|
|
345
|
-
# asking "does Command's own reconstruction method mention this
|
|
346
|
-
# field" is a question with no method to check.
|
|
347
|
-
RECONSTRUCTION_METHODS = { "Aggregate" => :aggregate, "Entity" => :entity }.freeze
|
|
348
|
-
|
|
349
|
-
# The six touchpoints `.claude/skills/bluebook-construct-creator/
|
|
350
|
-
# SKILL.md` walks in prose, checked structurally instead of by hand
|
|
351
|
-
# — for a construct/field pair not yet fully propagated (typically
|
|
352
|
-
# mid-round, deciding what's left), or as a sanity check before the
|
|
353
|
-
# final gate sweep of a round already believed done. Every check
|
|
354
|
-
# here is best-effort and advisory, not a gate: a `false` does not
|
|
355
|
-
# always mean "not yet done" (a field can be legitimately exempt —
|
|
356
|
-
# `Deviations`' own named categories, `GUARANTEED_BY_CONSTRUCTION`,
|
|
357
|
-
# or `META_DOMAIN_KNOWN_GAPS`, the last of which lives in
|
|
358
|
-
# spec/fuzzing/meta_domain_coverage_spec.rb, a spec file this
|
|
359
|
-
# module deliberately never requires — see `CONSTRUCTS`' own
|
|
360
|
-
# comment on the same principle). Read the touchpoint's own
|
|
361
|
-
# existing gate (`model_shape_conformance_spec.rb`,
|
|
362
|
-
# `assembly_spec.rb`, `meta_domain_coverage_spec.rb`) before trusting
|
|
363
|
-
# a `false` here as a real gap.
|
|
364
|
-
# @param name [String] a `CONSTRUCTS` key, such as `"Aggregate"`
|
|
365
|
-
# @param field [String, Symbol] the declared field to check propagation for
|
|
366
|
-
# @return [Hash{Symbol => Object}] `:name`, `:field`, and `:touchpoints` — an
|
|
367
|
-
# `Array<Hash>` of `:touchpoint` (String) and `:present` (Boolean, or nil when the
|
|
368
|
-
# touchpoint does not apply to `name`)
|
|
369
|
-
# @raise [ArgumentError] if `name` is not a `CONSTRUCTS` key
|
|
370
|
-
def impact_preview(name, field)
|
|
371
|
-
CONSTRUCTS.fetch(name) { raise ArgumentError, "no such construct #{name.inspect} — known: #{CONSTRUCTS.keys.join(', ')}" }
|
|
372
|
-
field = field.to_s
|
|
373
|
-
|
|
374
|
-
{
|
|
375
|
-
name: name,
|
|
376
|
-
field: field,
|
|
377
|
-
touchpoints: [
|
|
378
|
-
{ touchpoint: "meta-domain grammar declares it", present: meta_declared(name).map(&:to_s).include?(field) },
|
|
379
|
-
{ touchpoint: "docs/resolution-rules/ names it", present: resolution_rule_mentions?(field) },
|
|
380
|
-
{ touchpoint: "Assembly::Contracts consumes it", present: contract_consumes?(name, field) },
|
|
381
|
-
{ touchpoint: "Reconstruction's hand-typed method reads it", present: reconstruction_reads?(name, field) },
|
|
382
|
-
{ touchpoint: "fuzzer FEATURE_COVERAGE/GUARANTEED_BY_CONSTRUCTION claims it", present: fuzzer_claims?(name, field) },
|
|
383
|
-
{ touchpoint: "Rust mirror (rust/parser/src/parse/*.rs) mentions it", present: rust_mentions?(field) }
|
|
384
|
-
]
|
|
385
|
-
}
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
def resolution_rule_mentions?(field)
|
|
389
|
-
paths = Dir.glob(File.join(Codemod::ROOT, "docs/resolution-rules/*.md")) +
|
|
390
|
-
Dir.glob(File.join(Codemod::ROOT, "docs/implemented/resolution-rules/*.md"))
|
|
391
|
-
paths.any? { |path| File.read(path).include?(field) }
|
|
392
|
-
end
|
|
393
|
-
private_class_method :resolution_rule_mentions?
|
|
394
|
-
|
|
395
|
-
def contract_consumes?(name, field)
|
|
396
|
-
contract = Hecks::Bluebook::Assembly.contract(name)
|
|
397
|
-
contract.fields.key?(field.to_sym) || contract.derived.key?(field.to_sym)
|
|
398
|
-
rescue KeyError
|
|
399
|
-
false
|
|
400
|
-
end
|
|
401
|
-
private_class_method :contract_consumes?
|
|
402
|
-
|
|
403
|
-
# `Method#source_location` finds where the hand-typed method starts;
|
|
404
|
-
# the method's own body ends at the next line indented no deeper
|
|
405
|
-
# than its own `def` — the same boundary Ruby itself uses, read back
|
|
406
|
-
# textually because there is no live AST here, only a file to grep a
|
|
407
|
-
# slice of. `nil` (not `false`) for every other construct — this
|
|
408
|
-
# touchpoint genuinely does not apply to them (`RECONSTRUCTION_
|
|
409
|
-
# METHODS` only names the two hand-typed methods), and collapsing
|
|
410
|
-
# "does not apply" into "not done" would misreport a construct that
|
|
411
|
-
# was never supposed to have this touchpoint at all.
|
|
412
|
-
def reconstruction_reads?(name, field)
|
|
413
|
-
method_name = RECONSTRUCTION_METHODS[name]
|
|
414
|
-
# rubocop:disable-next Style/ReturnNilInPredicateMethodDefinition -- nil vs
|
|
415
|
-
# false is a deliberate distinction here: nil means "not applicable" (no
|
|
416
|
-
# hand-typed method to check), false means "applicable, and it fails" —
|
|
417
|
-
# see the spec's own "not-applicable (nil), not false" example.
|
|
418
|
-
return nil unless method_name
|
|
419
|
-
|
|
420
|
-
file, start_line = Hecks::Bluebook::MetaValidator::Reconstruction.instance_method(method_name).source_location
|
|
421
|
-
lines = File.readlines(file)
|
|
422
|
-
def_line = lines[start_line - 1]
|
|
423
|
-
indent = def_line[/\A\s*/]
|
|
424
|
-
|
|
425
|
-
body = lines[start_line..].take_while do |line|
|
|
426
|
-
line.strip.empty? || line[/\A\s*/].size > indent.size || !line.lstrip.start_with?("def ")
|
|
427
|
-
end
|
|
428
|
-
body.join.include?("#{field}:")
|
|
429
|
-
end
|
|
430
|
-
private_class_method :reconstruction_reads?
|
|
431
|
-
|
|
432
|
-
def fuzzer_claims?(name, field)
|
|
433
|
-
key = "#{name}##{field}"
|
|
434
|
-
Hecks::Fuzzing::Properties::FEATURE_COVERAGE.values.flatten.include?(key) ||
|
|
435
|
-
Hecks::Fuzzing::Properties::GUARANTEED_BY_CONSTRUCTION.key?(key)
|
|
436
|
-
end
|
|
437
|
-
private_class_method :fuzzer_claims?
|
|
438
|
-
|
|
439
|
-
def rust_mentions?(field)
|
|
440
|
-
Dir.glob(File.join(Codemod::ROOT, "rust/parser/src/parse/*.rs")).any? { |path| File.read(path).include?(field) }
|
|
441
|
-
end
|
|
442
|
-
private_class_method :rust_mentions?
|
|
443
|
-
|
|
444
|
-
# Renders one field's touchpoint checklist as text.
|
|
445
|
-
#
|
|
446
|
-
# @param preview [Hash] `impact_preview`'s own output
|
|
447
|
-
# @return [String] the human-readable rendering
|
|
448
|
-
def format_impact_preview(preview)
|
|
449
|
-
lines = ["== #{preview[:name]}##{preview[:field]} =="]
|
|
450
|
-
preview[:touchpoints].each do |t|
|
|
451
|
-
mark = if t[:present].nil?
|
|
452
|
-
"n/a"
|
|
453
|
-
else
|
|
454
|
-
(t[:present] ? "yes" : "NOT YET")
|
|
455
|
-
end
|
|
456
|
-
lines << " [#{mark.rjust(7)}] #{t[:touchpoint]}"
|
|
457
|
-
end
|
|
458
|
-
done = preview[:touchpoints].count { |t| t[:present] == true }
|
|
459
|
-
total = preview[:touchpoints].count { |t| !t[:present].nil? }
|
|
460
|
-
lines << ""
|
|
461
|
-
lines << "#{done}/#{total} applicable touchpoint(s) show signs of this field — advisory, not a gate; " \
|
|
462
|
-
"a NOT YET can be a legitimate exemption (Deviations, GUARANTEED_BY_CONSTRUCTION, or a spec-only " \
|
|
463
|
-
"META_DOMAIN_KNOWN_GAPS entry this module deliberately never reads)."
|
|
464
|
-
lines.join("\n")
|
|
465
|
-
end
|
|
466
|
-
|
|
467
|
-
# Renders the duplicate-rule groups as text.
|
|
468
|
-
#
|
|
469
|
-
# @param groups [Array<Hash>] `duplicates`' own output
|
|
470
|
-
# @return [String] the human-readable rendering
|
|
471
|
-
def format_duplicates(groups)
|
|
472
|
-
return "no duplicate given/invariant/ensures rule found" if groups.empty?
|
|
473
|
-
|
|
474
|
-
body = groups.map do |group|
|
|
475
|
-
["== #{group[:kind]}: #{group[:description].inspect} — #{group[:canonical]} ==",
|
|
476
|
-
*group[:locations].map { |loc| " #{loc}" }].join("\n")
|
|
477
|
-
end.join("\n\n")
|
|
478
|
-
|
|
479
|
-
"#{body}\n\n#{groups.size} duplicate group(s), #{groups.sum { |g| g[:locations].size }} declarations total"
|
|
480
|
-
end
|
|
481
|
-
end
|
|
482
|
-
end
|