Module: Hecks::QueryIR
- Defined in:
- lib/hecks/query_ir.rb
Overview
THE SHARED CORE behind bin/query_ir (a text CLI) and
bin/hecks_query_ir_mcp (an MCP server exposing the same two
queries as tools) — one implementation, two front ends, the same
reason Hecks::Codemod exists once rather than per-script.
Every method here returns structured data (Hashes/Arrays/Structs),
never formatted text — formatting is each front end's own job.
Defined Under Namespace
Classes: Rule
Constant Summary collapse
- Codemod =
Hecks::Codemod
- Deviations =
Hecks::Projections::Model::Deviations
- CONSTRUCTS =
THE SAME MAPPING spec/model_shape_conformance_spec.rb's own MODEL_CONSTRUCTS holds — kept here rather than shared from the spec (a spec file is not a library other code should require), matching Deviations' own doc comment: "the generator and the gate read one source" — Deviations is that one source; this table is small and genuinely construct-list bookkeeping, not a rule that can drift silently the way a Deviations entry could.
{ "Bluebook" => Hecks::Bluebook::Chapter, "Aggregate" => Hecks::Bluebook::Aggregate, "Command" => Hecks::Bluebook::Command, "Entity" => Hecks::Bluebook::Entity, "ValueObject" => Hecks::Bluebook::ValueObject, "Policy" => Hecks::Bluebook::Policy, "Query" => Hecks::Bluebook::Query, "ReadModel" => Hecks::Bluebook::ReadModel, "ProcessManager" => Hecks::Bluebook::ProcessManager }.freeze
- RECONSTRUCTION_METHODS =
ONE HAND-TYPED CONSTRUCT-NAME PER RECONSTRUCTION METHOD — the only two
MetaValidator::Reconstructionmethods NOT driven generically throughAssembly::Contracts' own table (its own header explains why:aggregate(row)/entity(row)predate the table and were never migrated).impact_preview's own touchpoint 4 is checked ONLY for these two — every other construct is read generically, so asking "does Command's own reconstruction method mention this field" is a question with no method to check. { "Aggregate" => :aggregate, "Entity" => :entity }.freeze
Class Method Summary collapse
-
.collect_rules(registry, chapter_name = nil) ⇒ Object
Every given/ensures/invariant DECLARATION reachable from a booted registry, walked recursively — every owner's own
.preconditions/.invariants(block-declared rules), every value object's own.invariants, and every command's own.givens/.ensures(.givenstoo, not just.ensures— a command's own LOCALgiven("x") { block }not yet hoisted to its owner, round 4's own starting shape, would otherwise be invisible). -
.construct_diff(name) ⇒ Object
The real structural diff between what a Ruby IR class emits (Class.ir_spec.keys) and what the self-hosted meta-domain declares for it — the SAME comparison spec/model_shape_conformance_spec.rb makes, reusing its own Deviations data so this can never silently drift from what that gate actually checks.
- .constructs(names = []) ⇒ Object
-
.duplicates(domains: nil, include_meta: true) ⇒ Object
Grouped by (kind, description, canonical), not canonical text alone — a generic one-liner like
!value.to_s.empty?legitimately recurs dozens of times for unrelated fields; the real signal is the SAME RULE (same description, same predicate), which is also exactly what the given/invariant reference mechanism itself resolves on. -
.format_constructs(diffs) ⇒ Object
SHARED TEXT FORMATTING — both
bin/query_ir(a text CLI) andbin/hecks_query_ir_mcp(an MCP tool result, itself a text block) want the identical human-readable rendering; only the OUTER framing differs (plain stdout vs. a JSON-RPC content array). - .format_duplicates(groups) ⇒ Object
- .format_impact_preview(preview) ⇒ Object
-
.impact_preview(name, field) ⇒ Object
THE SIX TOUCHPOINTS
.claude/skills/bluebook-construct-creator/ SKILL.mdwalks in prose, checked structurally instead of by hand — for a construct/field pair NOT yet fully propagated (typically mid-round, deciding what's left), or as a sanity check before the final gate sweep of a round already believed done. - .meta_declared(name) ⇒ Object
-
.owner_of(location) ⇒ Object
A rule's OWNER — the construct path a "(declared)" location names directly, or (for a command-level
.givens/.ensuresentry) the path with its trailing.CommandNamesegment stripped.
Class Method Details
.collect_rules(registry, chapter_name = nil) ⇒ Object
Every given/ensures/invariant DECLARATION reachable from a booted
registry, walked recursively — every owner's own .preconditions/
.invariants (block-declared rules), every value object's own
.invariants, and every command's own .givens/.ensures
(.givens too, not just .ensures — a command's own LOCAL
given("x") { block } not yet hoisted to its owner, round 4's own
starting shape, would otherwise be invisible).
NOT keyed by object identity (a real, hard-won correction — see
the comment on duplicates' own dedup below for why: a bare
given("x") reference and its owner's own block declaration are
the SAME Ruby object at DSL build time, but MetaValidator.call
(S14 — every bluebook is judged by dispatching its OWN IR into the
self-hosted grammar, then reconstructed via Assembly.call from
flat rows) rebuilds the whole graph fresh from there. By the time
any caller reads chapter.aggregates, EVERY given/invariant is
already a distinct object, whether it was block-declared or
bare-referenced — object identity carries no signal past that
point, for any construct, not just this one).
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/hecks/query_ir.rb', line 100 def collect_rules(registry, chapter_name = nil) rules = [] chapters = chapter_name ? [registry.bluebook(chapter_name)] : registry.bluebooks.values walk_construct = lambda do |construct, path| (construct.respond_to?(:preconditions) ? construct.preconditions : []).each do |rule| rules << Rule.new(kind: "given", description: rule.description, canonical: rule.canonical, location: "#{path} (declared)") end (construct.respond_to?(:invariants) ? construct.invariants : []).each do |rule| rules << Rule.new(kind: "invariant", description: rule.description, canonical: rule.canonical, location: "#{path} (declared)") end construct.commands.each do |command| command.givens.each do |rule| rules << Rule.new(kind: "given", description: rule.description, canonical: rule.canonical, location: "#{path}.#{command.hecks_name}") end command.ensures.each do |rule| rules << Rule.new(kind: "ensures", description: rule.description, canonical: rule.canonical, location: "#{path}.#{command.hecks_name}") end end construct.entities.each { |piece| walk_construct.call(piece, "#{path}.#{piece.hecks_name}") } if construct.respond_to?(:entities) end chapters.each do |chapter| chapter.aggregates.each do |aggregate| walk_construct.call(aggregate, aggregate.hecks_name) aggregate.value_objects.each do |vo| vo.invariants.each do |rule| rules << Rule.new(kind: "invariant", description: rule.description, canonical: rule.canonical, location: "#{aggregate.hecks_name}::#{vo.hecks_name} (declared)") end end end end rules end |
.construct_diff(name) ⇒ Object
The real structural diff between what a Ruby IR class emits (Class.ir_spec.keys) and what the self-hosted meta-domain declares for it — the SAME comparison spec/model_shape_conformance_spec.rb makes, reusing its own Deviations data so this can never silently drift from what that gate actually checks.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/hecks/query_ir.rb', line 51 def construct_diff(name) klass = CONSTRUCTS.fetch(name) { raise ArgumentError, "no such construct #{name.inspect} — known: #{CONSTRUCTS.keys.join(', ')}" } declared = (name) emitted = klass.ir_spec.keys accounted = declared.reject { |field| Deviations::PARENT_REF.call(field) } - Deviations::JUDGE_ONLY - Deviations.folded(name).values.flatten - Deviations.off_the_wire(name) - Deviations.dynamic_tail(name) - Deviations.unpacked(name).keys unaccounted = emitted - declared - Deviations.contained(name) - Deviations.folded(name).keys - Deviations.computed(name) - Deviations.unpacked(name).values.flatten { name: name, declared: declared, emitted: emitted, missing_from_ruby: accounted - emitted, unaccounted_in_ruby: unaccounted } end |
.constructs(names = []) ⇒ Object
76 77 78 79 |
# File 'lib/hecks/query_ir.rb', line 76 def constructs(names = []) targets = names.empty? ? CONSTRUCTS.keys : names targets.map { |name| construct_diff(name) } end |
.duplicates(domains: nil, include_meta: true) ⇒ Object
Grouped by (kind, description, canonical), not canonical text
alone — a generic one-liner like !value.to_s.empty? legitimately
recurs dozens of times for unrelated fields; the real signal is
the SAME RULE (same description, same predicate), which is also
exactly what the given/invariant reference mechanism itself
resolves on.
DEDUPED BY OWNER, not object identity (collect_rules' own
comment has the full story — identity is gone by the time this
reads the registry). Within a group, every command-level rule
whose OWNER already has its own "(declared)" entry in the same
group is just that declaration read again through a reference —
Account.Open/Account.Credit/etc. all naming Account's own
given("customer is active") count as Account's ONE declaration,
not nine. A command-level rule with no matching owner declaration
(a LOCAL, not-yet-hoisted given("x") { block }) counts as its
own standalone declaration — two different commands independently
writing the identical local predicate IS two declarations, a real
hoisting opportunity. A group is reported only when it adds up to
MORE than one real declaration this way.
domains: [] means "the self-hosted meta-domain only" — pass real
domain directories explicitly to include them, or nil (the
default) for meta-domain plus every real example.
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/hecks/query_ir.rb', line 183 def duplicates(domains: nil, include_meta: true) domains ||= Codemod::EXAMPLE_ROOTS all_rules = [] all_rules.concat(collect_rules(Codemod.)) if domains.each do |domain_dir| bluebook_files = Dir.glob(File.join(domain_dir, "bluebook", "*.bluebook")) next if bluebook_files.empty? registry = Codemod.load_bluebook(bluebook_files) all_rules.concat(collect_rules(registry)) end all_rules.group_by { |r| [r.kind, r.description, r.canonical] } .map { |key, rules| [key, rules, declaration_count(rules)] } .select { |_, _, count| count > 1 } .map { |(kind, description, canonical), rules, _| { kind: kind, description: description, canonical: canonical, locations: rules.map(&:location) } } end |
.format_constructs(diffs) ⇒ Object
SHARED TEXT FORMATTING — both bin/query_ir (a text CLI) and
bin/hecks_query_ir_mcp (an MCP tool result, itself a text
block) want the identical human-readable rendering; only the
OUTER framing differs (plain stdout vs. a JSON-RPC content array).
258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
# File 'lib/hecks/query_ir.rb', line 258 def format_constructs(diffs) diffs.map do |diff| lines = ["== #{diff[:name]} =="] lines << " emits: #{diff[:emitted].join(', ')}" lines << " declares: #{diff[:declared].join(', ')}" if diff[:missing_from_ruby].empty? && diff[:unaccounted_in_ruby].empty? lines << " clean — every declared field is emitted (or a named deviation), nothing emitted is undeclared" else lines << " MISSING FROM RUBY (declared, not emitted, not a named deviation): #{diff[:missing_from_ruby].join(', ')}" unless diff[:missing_from_ruby].empty? lines << " UNACCOUNTED IN RUBY (emitted, not declared, not a named deviation): #{diff[:unaccounted_in_ruby].join(', ')}" unless diff[:unaccounted_in_ruby].empty? end lines.join("\n") end.join("\n\n") end |
.format_duplicates(groups) ⇒ Object
381 382 383 384 385 386 387 388 389 390 |
# File 'lib/hecks/query_ir.rb', line 381 def format_duplicates(groups) return "no duplicate given/invariant/ensures rule found" if groups.empty? body = groups.map do |group| ["== #{group[:kind]}: #{group[:description].inspect} — #{group[:canonical]} ==", *group[:locations].map { |loc| " #{loc}" }].join("\n") end.join("\n\n") "#{body}\n\n#{groups.size} duplicate group(s), #{groups.sum { |g| g[:locations].size }} declarations total" end |
.format_impact_preview(preview) ⇒ Object
366 367 368 369 370 371 372 373 374 375 376 377 378 379 |
# File 'lib/hecks/query_ir.rb', line 366 def format_impact_preview(preview) lines = ["== #{preview[:name]}##{preview[:field]} =="] preview[:touchpoints].each do |t| mark = t[:present].nil? ? "n/a" : (t[:present] ? "yes" : "NOT YET") lines << " [#{mark.rjust(7)}] #{t[:touchpoint]}" end done = preview[:touchpoints].count { |t| t[:present] == true } total = preview[:touchpoints].count { |t| !t[:present].nil? } lines << "" lines << "#{done}/#{total} applicable touchpoint(s) show signs of this field — advisory, not a gate; " \ "a NOT YET can be a legitimate exemption (Deviations, GUARANTEED_BY_CONSTRUCTION, or a spec-only " \ "META_DOMAIN_KNOWN_GAPS entry this module deliberately never reads)." lines.join("\n") end |
.impact_preview(name, field) ⇒ Object
THE SIX TOUCHPOINTS .claude/skills/bluebook-construct-creator/ SKILL.md walks in prose, checked structurally instead of by hand
— for a construct/field pair NOT yet fully propagated (typically
mid-round, deciding what's left), or as a sanity check before the
final gate sweep of a round already believed done. Every check
here is BEST-EFFORT and ADVISORY, not a gate: a false does not
always mean "not yet done" (a field can be legitimately exempt —
Deviations' own named categories, GUARANTEED_BY_CONSTRUCTION,
or META_DOMAIN_KNOWN_GAPS, the last of which lives in
spec/fuzzing/meta_domain_coverage_spec.rb, a SPEC file this
module deliberately never requires — see CONSTRUCTS' own
comment on the same principle). Read the touchpoint's own
existing gate (model_shape_conformance_spec.rb,
assembly_spec.rb, meta_domain_coverage_spec.rb) before trusting
a false here as a real gap.
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 |
# File 'lib/hecks/query_ir.rb', line 298 def impact_preview(name, field) CONSTRUCTS.fetch(name) { raise ArgumentError, "no such construct #{name.inspect} — known: #{CONSTRUCTS.keys.join(', ')}" } field = field.to_s { name: name, field: field, touchpoints: [ { touchpoint: "meta-domain grammar declares it", present: (name).map(&:to_s).include?(field) }, { touchpoint: "docs/resolution-rules/ names it", present: resolution_rule_mentions?(field) }, { touchpoint: "Assembly::Contracts consumes it", present: contract_consumes?(name, field) }, { touchpoint: "Reconstruction's hand-typed method reads it", present: reconstruction_reads?(name, field) }, { touchpoint: "fuzzer FEATURE_COVERAGE/GUARANTEED_BY_CONSTRUCTION claims it", present: fuzzer_claims?(name, field) }, { touchpoint: "Rust mirror (rust/parser/src/parse/*.rs) mentions it", present: rust_mentions?(field) } ] } end |
.meta_declared(name) ⇒ Object
41 42 43 44 |
# File 'lib/hecks/query_ir.rb', line 41 def (name) Hecks::Bluebook::MetaValidator.grammar_registry .bluebook("Bluebook").aggregate(name).attributes.map(&:name) end |
.owner_of(location) ⇒ Object
A rule's OWNER — the construct path a "(declared)" location names
directly, or (for a command-level .givens/.ensures entry) the
path with its trailing .CommandName segment stripped. Two rules
sharing an owner are the SAME declaration read twice (an owner's
own precondition, and a command under it referencing that
precondition by name) — not two independent ones.
PUBLIC, not a duplicates-only internal — bin/codemod_hoist_ local_givens reads it directly to group collect_rules' own
output by owner itself, the same reading duplicates' own
declaration_count makes.
153 154 155 156 157 |
# File 'lib/hecks/query_ir.rb', line 153 def owner_of(location) return location.sub(/ \(declared\)\z/, "") if location.end_with?(" (declared)") location.rpartition(".").first end |