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::Reconstruction methods NOT driven generically through Assembly::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

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 = meta_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.meta_registry)) if include_meta

  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: meta_declared(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 meta_declared(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