diff --git a/CHANGELOG.md b/CHANGELOG.md index c9441f798..f4db6299b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,79 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Added — Ruby has inheritance edges: `class Child < Parent` reaches the lego view and the resolver's base walk + +No Ruby corpus has ever carried an inheritance edge. `captureBases` turns a class's base clause into +inherit refs, which `buildGraph` reads into the CHA-lite name graph the resolver walks after a type's +OWN method set misses — and Ruby reached none of it. The clause kind was never the problem +(`superclass` is already in the table; Java's `extends` clause carries the same node name): the +base-TYPE table held no node kind Ruby uses. Ruby names a base with `(constant)` — `class Child < +Parent` — or `(scope_resolution)` — `class Derived < Space::Base`. Both are now read, under a +language test rather than appended to the shared table, because both kind names are generic enough +to mean something else in another grammar. + +This is floor (a) of the constant-receiver round above, lifted — and it is what held that round's +gem numbers down, since a gem reaches its class methods up an `ActiveRecord::Base` hierarchy. Two +things follow at once: `--lego` answers for Ruby, and `Child.build` resolves to `Parent::build`. + +A base is found by its final segment, like every language's, and then SCOPED by Ruby's own constant +lookup: the superclass as written is resolved innermost-first along the enclosing `Module.nesting`, +then at the top level (`::X` absolute), against every class and module the tree opens — the #57 +constant index, namespace wrappers included — and only the classes that ARE that constant are its +base. So `class Rec < ActiveRecord::Base` is not an implementor of an in-tree `Space::Base`, `class +Inner < Base` inside `module Beta` lands on `Beta::Base` alone, and a base the tree never opens adds +nothing to the CHA name graph, so its class's walk cannot reach an unrelated in-tree `Base`. That +join needs no new extraction: every `class X < Y` already carries its written superclass as the +symbolic directive #57 records. The bases are read by constant rather than through `byName`, whose +C-family decl/def collapse takes a body-less `class Base < StandardError; end` for a forward +declaration and drops it next to any same-named class with a body. + +| corpus | `--lego` implementors | edges | ambiguous | +| --- | --- | --- | --- | +| activerecord 8.1.3 `lib`, `--lego=active_record/base.rb:Base` | 0 → 0 | 9,152 → 9,001 | 1,479 → 1,288 | +| activerecord 8.1.3 `lib`, `--lego=active_record/encryption/errors.rb:Base` | 0 → 6 | — | — | +| activesupport 8.1.3 `lib` | — | 3,912 → 3,915 | 434 → 422 | +| actionpack 8.1.3 `lib` | — | 3,140 → 3,127 | 364 → 355 | +| Rails app A, `--lego=ApplicationRecord` | 0 → 129 | 24,376 → 24,392 | 1,263 → 1,268 | +| Rails app A, `--lego=app/controllers/application_controller.rb:ApplicationController` | 0 → 114 | — | — | +| Rails app A, `--lego=app/controllers/admin/application_controller.rb:ApplicationController` | 0 → 19 | — | — | +| Rails app B | — | 16,112 → 16,121 | 440 → 445 | + +`ambiguous` falls on the gems because a two-way split collapses into one pinned edge, which is also +why `edges` falls where it does — 151 fewer on activerecord is 151 calls that stopped naming two +candidates. No `ActiveRecord::Base` subclass lives in activerecord's own `lib`, so 0 is its answer; +before the scoping, the final-segment key gave it 11, every one a collision (`ActiveJob::Base`, the +encryption errors' own `Errors::Base`, the generators' `Base`). On the apps the scoping is what +moves `ambiguous` UP: a call that reached an in-tree `Base` only through an out-of-tree one is no +longer pinned there. App A's `self.data` in a model (`< ApplicationRecord < ActiveRecord::Base`) was +pinned to a report handler's `data` through `Reports::ResolutionHandlers::Base`; it is an honest +split now. App B's twelve `SomeModel.polymorphic_name` sites were each split three ways over the +app's own `User::Base`, `Organization::Base` and `BankAccount::Base`; ActiveRecord answers them, and +they mint nothing. + +Stated floors, each pinned by an arm of `test/rubyinheritcheck.sh`: a COMPUTED superclass (`class +Dynamic < Struct.new( :a )`) is a call, not a name, and mints nothing — not even an edge to the +call's receiver, which `captureBases`' one-level wrapper descent used to hand over; a MIXIN +(`include Helper`) is NOT an inheritance edge in this round — it is a receiver-less call in the class +BODY, the same shape and the same decision as PHP's in-body `use SomeTrait;`, and Ruby's ancestor +chain really does hold included modules, so it is a stated residue rather than a claim that it is not +inheritance; the base walk's METHOD probe is keyed by the immediate scope (`Base::m`), so two in-tree +bases that share a final name still share one probe — `UsesAlpha.beta_make` pins `Beta::Base`'s +method although `UsesAlpha < Alpha::Base`; and `Built = Class.new( Parent )`, with or without a block, +makes `Built < Parent` at runtime but is a constant assignment whose value is a call, not a `class` +open, so it mints no class and no edge — the same decision as a computed superclass. A class +reopened with its superclass repeated (`class Reop < Parent … end` twice) is two symbols and one +constant, and the lego view lists it once — app A's 129 was 130 before, one model counted twice +because a stub reopens it with the superclass repeated. A fifth floor, of `queries/ruby/tags.scm` rather than of +this round, is pinned beside them: a receiver-less call written with no parentheses and no arguments +parses as `(identifier)`, not `(call)`, and is not a call site at all. + +`--deps` is byte-identical on activerecord, and the default map is byte-identical on four Ruby-free +corpora, with this repository's `--report` totals unchanged. `kParserVer` 97 → 99 in two steps (98: +new records, same layout — a Ruby cache written at 97 holds no inheritance refs; 99: fewer records — +a cache written at 98 holds the computed superclass's stray receiver ref; `kCacheVersion` stays 22), +with the `quality.h` mirror and `test/qschemetrip.hash` moved in each step's commit. + ### Added — a Ruby constant receiver now pins the call, instead of splitting it across every same-named method `Calc.add( 1, 2 )`, `Outer::Engine.run( 3 )`, `::Top.ping` and `Util.format( 5 )` resolved to EVERY diff --git a/README.md b/README.md index 6dbdd5ed4..ed9cbdf1d 100644 --- a/README.md +++ b/README.md @@ -1817,9 +1817,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-622 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +623 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **622 gate scripts** and is the authoritative list; +`test/regression.sh` names **623 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index a9bb5e456..e91511ee7 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 622 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 623 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5834,7 +5834,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **622 gate scripts**, all of which exist on disk. +naming **623 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -6846,7 +6846,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 622. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 623. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index 73f071a28..b4dc96884 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1123,7 +1123,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["622 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["623 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1147,7 +1147,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "622", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "623", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1397,7 +1397,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["180 long flags · 34 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["622 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["623 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/src/graph.h b/src/graph.h index f83a3dbcb..8aad232f5 100644 --- a/src/graph.h +++ b/src/graph.h @@ -2048,6 +2048,10 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // for `class C(A, B)` Python's MRO puts A's chain before B's, so when both A and B define `m`, `super().m()` // in C names A::m. chaUp is sorted for its membership uses and cannot say which base came first. HashMap> chaUpDeclared; + // Ruby bases, SCOPED by Ruby's own constant lookup (resolve.h::RubyBaseScope; test/rubyinheritcheck.sh floor + // (c)). Read here — a base the tree never opens (`< ActiveRecord::Base`) adds no CHA edge, so its class's walk + // cannot reach an unrelated in-tree `Base` — and by the implementor pass below. Empty on a Ruby-free corpus. + const RubyBaseScope rubyBases = buildRubyBaseScope( ing ); { const auto isClassLikeK = []( SymKind k ) noexcept { return k == SymKind::Class || k == SymKind::Struct || k == SymKind::Interface; }; @@ -2058,6 +2062,10 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null { continue; } + if( const std::string* scoped = rubyBases.resolvedBase( std::size_t( &ir - ing.references.data() ) ); scoped && scoped->empty() ) + { + continue; // a Ruby base the tree never opens: nothing in-tree to walk to + } std::string_view derivedName; if( !ir.qualifier.empty() ) { // Rust impl → derived type name in qualifier @@ -3089,14 +3097,36 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null { continue; } + if( ing.symbols[ derived ].lang == Lang::Ruby ) + { + derived = rubyBases.canonicalClass( ing, derived ); // a reopened class is ONE implementor, not one per open + } - const auto it = byName.find( r.calleeName ); - if( it == byName.end() ) + // A Ruby base resolved by Ruby's own lookup: the bases are the classes whose constant it IS, read by constant + // (RubyBaseScope::classesByFqn — byName's decl/def collapse would hide a body-less Ruby class). nullptr ⇒ not + // a scoped Ruby base (every other language, or a Ruby site the join could not place) ⇒ byName stands. + const rw::SmallVec* baseIds = nullptr; + const std::string* rubyScoped = rubyBases.resolvedBase( std::size_t( &r - ing.references.data() ) ); + if( rubyScoped != nullptr ) { - continue; + const auto sit = rubyScoped->empty() ? rubyBases.classesByFqn.end() : rubyBases.classesByFqn.find( *rubyScoped ); + if( sit == rubyBases.classesByFqn.end() ) + { + continue; // the tree never opens the written base — no in-tree row to list it under + } + baseIds = &sit->second; + } + else + { + const auto it = byName.find( r.calleeName ); + if( it == byName.end() ) + { + continue; + } + baseIds = &it->second; } baseCand.clear(); - for( NodeId baseId : it->second ) + for( NodeId baseId : *baseIds ) { if( !isClassLike( ing.symbols[baseId].kind ) ) { diff --git a/src/ingest_cache.h b/src/ingest_cache.h index 405b38319..47626485d 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -232,7 +232,23 @@ constexpr std::uint32_t kCacheVersion = 22; // 22: RawDef gains `inter // (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) — // a target FORMAT change → old caches must be rejected. // 4: Include gained a `bool isAngle` (quote/angle) field -constexpr std::uint32_t kParserVer = 97; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 99; // bump on any grammar/.scm/extraction change + // 99 = 2026-09-23 (a computed Ruby superclass mints nothing, + // test/rubyinheritcheck.sh floor (a)): captureBases stops + // descending into a Ruby `superclass` clause's non-constant + // child, where it landed on the RECEIVER of + // `class Dynamic < Struct.new( :a )` and emitted an inherit + // ref to `Struct`. FEWER records, same layout (kCacheVersion + // stays 22): a Ruby cache written at 98 holds the stray ref. + // quality.h's kIngestParserVerMirror bumped in the SAME commit. + // 98 = 2026-09-17 (Ruby inheritance edges, + // test/rubyinheritcheck.sh): isBaseTypeNode gains a Ruby arm + // ((constant)/(scope_resolution)), so `class Child < Parent` + // emits an inherit RawRef (role Extends) where it emitted + // none. NEW RECORDS from extraction, same record layout + // (kCacheVersion stays 22): a Ruby cache written at 97 holds + // no inheritance refs at all and must be re-parsed. + // quality.h's kIngestParserVerMirror bumped in the SAME commit. // 97 = 2026-09-17 (Ruby constant receivers narrow calls, // test/rubyrecvnarrowcheck.sh): classifyReceiver classifies a // Ruby (constant)/(scope_resolution) receiver as NamedVar with diff --git a/src/ingest_relations.h b/src/ingest_relations.h index 840a04a35..52a61b654 100644 --- a/src/ingest_relations.h +++ b/src/ingest_relations.h @@ -45,6 +45,17 @@ inline bool isBaseTypeNode( const char* nt ) noexcept return false; } +// The base-type test for one base-clause child, by language. Ruby names a base with its OWN node kinds, and only Ruby +// does: `class Child < Parent` hands the `superclass` clause a (constant), `class Derived < Space::Base` a +// (scope_resolution). They are asked under a language test here, rather than added as rows of isBaseTypeNode's shared +// table, because both kind names are generic enough to occur in another grammar with another meaning, and that table is +// consulted for every base clause in every language. `class Dynamic < Struct.new( :a )` hands over a (call) and is +// correctly nothing — a computed superclass is not a name this tool can resolve. test/rubyinheritcheck.sh. +inline bool isBaseTypeNodeIn( const char* nt, Lang lang ) noexcept +{ + return lang == Lang::Ruby ? ( kindIs( nt, "constant" ) || kindIs( nt, "scope_resolution" ) ) : isBaseTypeNode( nt ); +} + // Emit one inherit RawRef (derived → base) for a base-type node. startByte sits inside the class header // (the type node's own start), so the byte-span enclosing attribution binds fromSymbol = the derived class. inline void emitBaseRef( TSNode typeNode, std::uint32_t fileId, Lang lang, std::string_view src, std::vector& refs ) @@ -297,7 +308,11 @@ void captureMacroBodyCalls( TSNode defineNode, std::uint32_t fileId, Lang lang, // Capture base classes for the inheritance/Lego view: walk a class node's base clause and emit an // inherit RawRef per base (derived → base). startByte sits inside the class header, so the enclosing // attribution assigns fromSymbol = the derived class. Explicit-syntax langs: C++/TS/JS/Java/Python/Swift/ -// C#/PHP/Kotlin. Lua is deliberately absent and it is a DISCLOSED non-goal, not an omission: Lua inheritance IS +// C#/PHP/Kotlin/Ruby. Ruby reuses the `superclass` clause name Java's extends clause already has, and +// names its base with (constant)/(scope_resolution) — see isBaseTypeNodeIn. A Ruby MIXIN +// (`include M` / `extend M` / `prepend M`) is NOT captured here, for the same reason the PHP note below +// gives: it is a call in the class BODY, not a clause. Ruby's ancestor chain does hold included modules, +// so that is a stated residue (test/rubyinheritcheck.sh floor (b)), not a claim it is not inheritance. Lua is deliberately absent and it is a DISCLOSED non-goal, not an omission: Lua inheritance IS // `setmetatable( Derived, { __index = Base } )`, an ordinary runtime call over an ordinary table, so there // is no syntax to read and a Lua corpus correctly reports no inheritance edges at all. // @@ -352,11 +367,18 @@ void captureBases( TSNode classNode, std::uint32_t fileId, Lang lang, std::strin forEachChild( clause, clauseCursor.cur, [ & ]( TSNode bn ) { const char* bt = ts_node_type( bn ); - if( isBaseTypeNode( bt ) ) // DIRECT: type node right under the clause + if( isBaseTypeNodeIn( bt, lang ) ) // DIRECT: type node right under the clause { emitBaseRef( bn, fileId, lang, src, refs ); return true; } + // Ruby has no wrapper: its `superclass` clause holds ONE expression, and anything but a constant is + // COMPUTED (`class Dynamic < Struct.new( :a )`). Descending into that (call) lands on its receiver + // `Struct` and minted an edge to it — the receiver is not the base (test/rubyinheritcheck.sh floor (a)). + if( lang == Lang::Ruby ) + { + return true; + } // WRAPPED: descend ONE level into a wrapper (extends_clause / implements_clause / type_list) // and emit each type node it holds. One level is enough for every measured grammar shape. ChildCursor wrapCursor( bn ); diff --git a/src/quality.h b/src/quality.h index a584b6a48..9c6baeb69 100644 --- a/src/quality.h +++ b/src/quality.h @@ -1974,7 +1974,7 @@ inline std::string cacheRootKeyHex( const std::string& root ) // FOLLOW-UP for whoever owns ingest.{h,cpp}: promote the two constants into ingest.h and turn the gate into a // `static_assert` — this lane's file boundary forbade editing those files. constexpr std::uint32_t kIngestCacheVersionMirror = 22; // MUST equal ingest.cpp's kCacheVersion (gated) -constexpr std::uint32_t kIngestParserVerMirror = 97; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestParserVerMirror = 99; // MUST equal ingest.cpp's kParserVer (gated) // 95 = 2026-09-12 (Elixir module/name/arity resolution, PR #81): // RE-BUMPED from the branch's 87 over #139's 93 and #172's 94. // See ingest_cache.h's kParserVer note. diff --git a/src/resolve.h b/src/resolve.h index 29cbcb3d9..8a4f64d61 100644 --- a/src/resolve.h +++ b/src/resolve.h @@ -1572,6 +1572,187 @@ inline std::pair resolveRubyConstant( const RubyCo return result; } +// ─── Ruby base SCOPING (test/rubyinheritcheck.sh floor (c)) ──────────────────────────────────────────────── +// A Ruby inherit reference is found by NAME — its final segment, the byName key every language's bases use — +// and that name alone cannot tell `class Rec < ActiveRecord::Base` from the tree's own `Space::Base`: before +// this, the out-of-tree base landed on every in-tree `Base` as an implementor AND fed the resolver's base walk. +// This table scopes each one with Ruby's own lookup. The superclass AS WRITTEN is already on record — the +// symbolic Include every `class X < Y` emits (ingest_relations.h, parser version 82), at the class's own start +// byte — so no new extraction is needed: the inherit ref is joined to it through the derived class's open +// (the innermost open containing the ref), and the written name is looked up innermost-first along the +// ENCLOSING nesting, then at the top level, `::X` absolute — resolveRubyConstant's order. Against what: every +// open the tree holds, WRAPPERS INCLUDED — `class Outer; class Nested; end; end` defines nothing of Outer and +// is no definer in the #57 index, but it is a class a base can name, so the definer table is the wrong oracle +// here. The bases are then the class symbols whose OWN open carries the resolved constant — read from +// `classesByFqn`, NOT from buildGraph's byName: byName has been through the C-family decl/def collapse, which +// takes a body-less `class Base < StandardError; end` for a forward declaration and drops it whenever a +// same-named class with a body exists (activerecord's Encryption::Errors::Base lost all six of its subclasses +// to rails/generators' Base that way). Ruby has no forward declarations; every open is the class. A written +// base the tree never opens resolves to nothing, and the reference then mints no implementor row and no CHA edge. +// Any reference the join cannot place (no open around it, no superclass directive at that open) stays on the +// byName rule — the pre-scoping answer, disclosed through DEGRADED_PATH_ALERT, never a guess in either +// direction. Built only when the corpus holds a Ruby inherit reference; empty otherwise. +struct RubyBaseScope +{ + RubyConstantIndex ix; + HashMap opened; // the constant of EVERY open, wrapper or not + HashMap baseFqn; // reference index → resolved base constant (EMPTY + // when the tree opens no such constant); find-only + HashMap> classesByFqn; // constant → the Ruby symbols whose own open it is + + // nullptr: not a scoped Ruby base — the byName rule stands. Otherwise the resolved constant (empty = none). + const std::string* resolvedBase( std::size_t refIdx ) const noexcept + { + const auto it = baseFqn.find( std::uint32_t( refIdx ) ); + return it != baseFqn.end() ? &it->second : nullptr; + } + + // The constant of a Ruby class symbol's own open; nullptr when no open of that name starts the symbol. + const std::string* fqnOfSymbol( const Symbol& s ) const noexcept + { + if( s.fileId >= ix.opensByFile.size() ) + { + return nullptr; + } + const std::vector& opens = ix.opensByFile[ s.fileId ]; + const std::uint32_t o = rubyInnermostOpen( opens, s.sigStartByte + 1u ); // strict at the start + if( o == kNoFile ) + { + return nullptr; + } + const std::string_view w = opens[ o ].written; + const std::size_t cut = w.rfind( "::" ); + if( ( cut == std::string_view::npos ? w : w.substr( cut + 2 ) ) != s.name ) + { + return nullptr; + } + return &opens[ o ].fqn; + } + + // The implementor a Ruby class stands as: the lowest-id symbol of its constant with its own kind. A class REOPENED + // with its superclass repeated (`class Reop < Parent … end`, twice) is two symbols and one constant, so both opens + // collapse onto one row instead of listing the class twice. `id` itself when its open is not indexed. + NodeId canonicalClass( const IngestResult& ing, NodeId id ) const noexcept + { + const std::string* fqn = fqnOfSymbol( ing.symbols[ id ] ); + if( fqn == nullptr ) + { + return id; + } + const auto it = classesByFqn.find( *fqn ); + if( it == classesByFqn.end() ) + { + return id; + } + for( NodeId cand : it->second ) // id order → the first match is the lowest id + { + if( ing.symbols[ cand ].kind == ing.symbols[ id ].kind ) + { + return cand; + } + } + return id; + } +}; + +// Every `class X < Y`'s written superclass, keyed by (fileId, the class's own start byte) — the symbolic directive +// #57 records at that byte. Find-only, so its bucket order never reaches output. +inline HashMap rubySuperclassSites( const IngestResult& ing, const RubyConstantIndex& ix ) +{ + HashMap sites; + for( const Include& inc : ing.includes ) + { + if( inc.isSymbolic && inc.fileId < ix.opensByFile.size() && !ix.opensByFile[ inc.fileId ].empty() ) + { + sites.try_emplace( ( std::uint64_t( inc.fileId ) << 32 ) | inc.byte, &inc.target ); // first directive at a byte wins + } + } + return sites; +} + +// Ruby's lookup of a written base from the derived class open `derived`: `::X` absolute, else innermost-first +// along the ENCLOSING nesting (the superclass is evaluated before the class opens), then the top level. Returns +// the constant the tree opens, or empty when it opens none. +// Stated floor — a QUALIFIED name is looked up whole: `class X < Mod::B` tries `::Mod::B` innermost-first, +// then `Mod::B`. Ruby resolves only the FIRST segment lexically and the rest strictly inside it, so where an +// enclosing `Outer::Mod` exists without a `B`, Ruby raises NameError while this falls through to a top-level +// `Mod::B`. That code cannot load, so the difference only shows in a tree that is already broken. +inline std::string rubyResolveBaseConstant( const HashMap& opened, const std::vector& opens, + std::uint32_t derived, std::string_view written ) +{ + const auto isOpened = [ &opened ]( const std::string& c ) { return opened.find( c ) != opened.end(); }; + if( rubyConstIsAbsolute( written ) ) + { + std::string abs( written.substr( 2 ) ); + return isOpened( abs ) ? abs : std::string{}; + } + for( std::uint32_t k = opens[ derived ].parent; k != kNoFile; k = opens[ k ].parent ) + { + std::string cand = opens[ k ].fqn; + cand += "::"; + cand += written; + if( isOpened( cand ) ) + { + return cand; + } + } + std::string top( written ); + return isOpened( top ) ? top : std::string{}; +} + +// Joins each Ruby inherit reference to the superclass directive at its derived class's open and records the +// resolved base constant in `sc.baseFqn`. A reference the join cannot place is left out — the byName rule stands. +inline void rubyScopeBaseReferences( RubyBaseScope& sc, const IngestResult& ing ) +{ + const HashMap sites = rubySuperclassSites( ing, sc.ix ); + for( std::size_t i = 0; i < ing.references.size(); ++i ) + { + const Reference& r = ing.references[ i ]; + if( !r.isInherit || r.lang != Lang::Ruby || r.fileId >= sc.ix.opensByFile.size() ) + { + continue; + } + const std::vector& opens = sc.ix.opensByFile[ r.fileId ]; + const std::uint32_t derived = rubyInnermostOpen( opens, r.startByte ); + const auto site = ( derived == kNoFile ) ? sites.end() : sites.find( ( std::uint64_t( r.fileId ) << 32 ) | opens[ derived ].startByte ); + if( site == sites.end() ) + { + DEGRADED_PATH_ALERT( "Ruby inherit reference with no superclass directive at its class open: base left on the byName rule" ); + continue; + } + sc.baseFqn.emplace( std::uint32_t( i ), rubyResolveBaseConstant( sc.opened, opens, derived, *site->second ) ); + } +} + +inline RubyBaseScope buildRubyBaseScope( const IngestResult& ing ) +{ + RubyBaseScope sc; + const auto isRubyBase = []( const Reference& r ) noexcept { return r.isInherit && r.lang == Lang::Ruby; }; + if( std::none_of( ing.references.begin(), ing.references.end(), isRubyBase ) ) + { + return sc; + } + PROFILE_SCOPE_DESCRIBE( "buildGraph/ruby: base scoping" ); + sc.ix = buildRubyConstantIndex( ing ); + for( const std::vector& opens : sc.ix.opensByFile ) + { + for( const RubyOpenRec& o : opens ) + { + sc.opened.try_emplace( o.fqn, 1 ); + } + } + for( const Symbol& s : ing.symbols ) + { + const std::string* fqn = ( s.lang == Lang::Ruby ) ? sc.fqnOfSymbol( s ) : nullptr; + if( fqn != nullptr ) + { + sc.classesByFqn[ *fqn ].push_back( s.id ); // ids ascending → each list is in id order + } + } + rubyScopeBaseReferences( sc, ing ); + return sc; +} + // Resolve ONE #include / import target to a concrete repo fileId by LEXICAL path semantics, dispatched on // the INCLUDER's language (its file extension). `fileIndex` maps each canonical `ing.files` path → its // fileId. `crateRootDir`/`hasCrateRoot` carry the Rust crate root (empty/false for non-Rust); diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index db11e62e3..19408ca8e 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -a2cb23278b1f94a0e05d947661878b706da18a0ebbf32ab7a9fd6b8264f0147e +95818fe516942f60ccef74b7219c44a357689e879dfc2d591d7ed8fd54b2f1d4 diff --git a/test/regression.sh b/test/regression.sh index 77c03ea15..c9eeb517b 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -268,7 +268,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyconstcheck rubyinheritcheck rubymetricscheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" diff --git a/test/rubyinheritcheck.sh b/test/rubyinheritcheck.sh new file mode 100755 index 000000000..053a66aab --- /dev/null +++ b/test/rubyinheritcheck.sh @@ -0,0 +1,453 @@ +#!/usr/bin/env bash +# rubyinheritcheck.sh — gate for Ruby INHERITANCE edges: `class Child < Parent` is an IS-A edge, so it +# reaches the lego (interface/implementor) view and the resolver's base walk. +# +# What was missing. ingest_relations.h::captureBases turns a class's base clause into inherit RawRefs +# (role Extends), which buildGraph reads into the CHA-lite name graph chaUp/chaDown — the table Rule 1's +# `super`/implicit-self walk and Rules 2b/2c's methodOnTypeOrBases probe after a type's OWN method set +# misses. Ruby reached none of it. The clause kind `superclass` was already in captureBases' table (Java +# spells its `extends` clause with the same node name), but isBaseTypeNode's list of base-TYPE node kinds +# had no entry Ruby uses: Ruby names a base with (constant) — `class Child < Parent` — or with +# (scope_resolution) — `class Derived < Space::Base`. So Ruby corpora carried no inheritance edges at +# all: `--lego` listed no implementors, and `Child.build` could not reach a `build` defined on Parent. +# +# That second half is what held the gem numbers down in the constant-receiver round +# (test/rubyrecvnarrowcheck.sh, floor (a)): a Rails gem reaches its class methods up an +# ActiveRecord::Base hierarchy, and without chaUp the receiver rule can only see a type's OWN methods. +# +# The rule: a Ruby base is the (constant) or (scope_resolution) child of the class's `superclass` clause, +# named by its FINAL segment (`Space::Base` → `Base`), which is how chaUp and byName key every other +# language's bases. Everything else about captureBases is unchanged. +# +# Stated floors, pinned below so each stays a decision: +# (a) a COMPUTED superclass (`class Dynamic < Struct.new( :a )`) is a call, not a constant — no edge. Not +# even to the call's RECEIVER: captureBases descends one wrapper level for every language (TS's +# `extends_clause`, C#'s `base_list`), and in Ruby that descent landed on `Struct`, minting an edge that +# only read as absent because the fixture defines no Struct. `class Built < Factory.fabricate( :x )` against +# an in-tree `Factory` pins it. +# (b) a MIXIN (`include Helper` / `extend` / `prepend`) is NOT an inheritance edge in this round. It is +# a receiver-less call in the class BODY, not a clause — the same shape, and the same decision, as +# PHP's in-body `use SomeTrait;` (captureBases' own header). Ruby's ancestor chain really does hold +# included modules, so this is a real residue and a later round's subject, not a claim that it is +# not inheritance. +# (c) the base WALK is keyed by NAME. A base is found by name (its final segment, as every language's is) +# and then SCOPED: the superclass as written is looked up the way Ruby looks it up — Module.nesting +# innermost first, then the top level, `::X` absolute — against every class/module the tree opens (the +# #57 constant index, wrappers included), and only a definition with THAT fully-qualified constant gets +# the implementor row. So `class Rec < ActiveRecord::Base` is no implementor of the fixture's `Space::Base`, +# `class Inner < Base` inside `module Beta` lands on Beta::Base alone, and a base the tree never opens +# (`ActiveRecord::Base`) adds nothing to the CHA name graph — Rec's walk no longer reaches Space::Base's +# methods. What stays name-keyed is the walk's METHOD probe (canonByName is keyed `Scope::method` with the +# IMMEDIATE scope), so two in-tree bases sharing a final name still share one probe: `UsesAlpha.beta_make` +# pins to Beta::Base#beta_make although UsesAlpha < Alpha::Base. Pinned below; lifting it needs a +# qualified scope on the symbol itself. +# (d) `Built = Class.new( Parent )` — with or without a block — makes Built < Parent at runtime, but it is a constant +# ASSIGNMENT whose value is a call, not a `class` open: no class symbol, no superclass clause, no edge. The same +# decision as (a): the tool reads a base off a class header, never off a computed value. +# +# A class REOPENED with its superclass repeated (`class Reop < Parent … end` twice, legal Ruby) is two symbols but one +# constant, so the lego view lists it ONCE: implementors are keyed by the derived class's constant, not by open. +# +# Usage: test/rubyinheritcheck.sh | RIPWIRE_BIN=asan/ripwire test/rubyinheritcheck.sh +# Exits non-zero on any failure. Does NOT edit test/regression.sh. Self-contained via mktemp. +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +fail=0 +ok(){ echo " PASS $1" || { fail=1; echo " FAIL could not write the PASS line for: $1"; }; return 0; } +no(){ echo " FAIL $1"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } + +DIR="$( mktemp -d )"; trap 'rm -rf "$DIR"' EXIT +FIX="$DIR/fix"; mkdir -p "$FIX" + +cat > "$FIX/h.rb" <<'RUBY' +class Parent + def self.build + 1 + end + + def shared_helper( n ) + n + end + + def bare_helper + 2 + end +end + +class Child < Parent + def use_inherited + shared_helper( 1 ) + end + + def use_bare + bare_helper + end +end + +class GrandChild < Child +end + +module Space + class Base + def self.make + 3 + end + end +end + +class Derived < Space::Base +end + +class Rec < ActiveRecord::Base +end + +class Dynamic < Struct.new( :a ) +end + +class Factory + def self.fabricate( x ) + x + end +end + +class Built < Factory.fabricate( :x ) +end + +module Helper + def helped + 4 + end +end + +class Mixed + include Helper +end + +class Unrelated + def self.build + 9 + end + + def self.make + 9 + end + + def shared_helper( n ) + n + end + + def bare_helper + 9 + end +end +RUBY + +cat > "$FIX/caller.rb" <<'RUBY' +class Caller + def call_inherited + Child.build + end + + def call_two_levels + GrandChild.build + end + + def call_scoped_base + Derived.make + end + + def call_mixin + Mixed.new.helped + end + + def call_out_of_tree_base + Rec.make + end +end +RUBY + +# floor (c)'s fixture: two MORE in-tree `Base`s, each in its own file so `--lego=FILE:Base` names one definition. +cat > "$FIX/alpha.rb" <<'RUBY' +module Alpha + class Base + def self.alpha_make + 5 + end + end +end +RUBY + +cat > "$FIX/beta.rb" <<'RUBY' +module Beta + class Base + def self.beta_make + 6 + end + end + + class Inner < Base + end +end +RUBY + +# A BODY-LESS base beside a same-named class WITH a body (Space::Base has `def self.make`). The C-family decl/def +# collapse reads `class Base < StandardError; end` as a forward declaration and drops it from byName whenever a bodied +# `Base` exists — activerecord's Encryption::Errors::Base lost its six subclasses to rails/generators' Base that way. +cat > "$FIX/errs.rb" <<'RUBY' +module Errs + class Base < StandardError; end + class Oops < Base; end +end +RUBY + +cat > "$FIX/scoped.rb" <<'RUBY' +class UsesAlpha < Alpha::Base +end + +class Abs < ::Parent +end + +class Outer + class Nested + end +end + +class FromWrapper < Outer +end + +class ScopedCaller + def call_alpha + UsesAlpha.alpha_make + end + + def call_beta_through_alpha + UsesAlpha.beta_make + end +end +RUBY + +# A class reopened with its superclass repeated — two opens, one constant — and the Class.new form of floor (d). +cat > "$FIX/reopen.rb" <<'RUBY' +class Reop < Parent + def first_half + 1 + end +end + +class Reop < Parent + def second_half + 2 + end +end + +Built2 = Class.new( Parent ) + +Blocky = Class.new( Parent ) do + def blocky + 3 + end +end +RUBY + +MAP="$DIR/map.xml" +"$BIN" "$FIX" --no-cache >"$MAP" 2>"$DIR/map.err" +if [ $? -eq 0 ]; then ok "default map exits 0"; else no "default map exited non-zero: $( cat "$DIR/map.err" )"; fi +[ -s "$DIR/map.err" ] && no "unexpected stderr: $( head -3 "$DIR/map.err" )" || ok "clean stderr" +command -v xmllint >/dev/null 2>&1 && { if xmllint --noout "$MAP"; then ok "xmllint --noout"; else no "xmllint failed"; fi; } + +SPLIT="$DIR/split"; sed 's/>\n"$SPLIT" +rowOf(){ awk -v pat="$1" '$0 ~ pat{f=1;print;next} /^/dev/null | sed 's/>\n"$DIR/ref.out" 2>"$DIR/ref.err" + then + return 1 + fi + grep -q -- "--lego type not found: $1" "$DIR/ref.err" +} +# runq VAR ARGS… — run the binary into VAR in THIS shell and FAIL a non-zero exit on its own line. An absence arm reads +# a crashed run's empty stdout exactly like "no edge", so it runs only when this returns 0. +runq(){ + local var="$1"; shift + if ! "$BIN" "$@" >"$DIR/runq.out" 2>"$DIR/runq.err" + then + no "ripwire ${*#"$DIR/"} exited non-zero: $( head -3 "$DIR/runq.err" )" + printf -v "$var" '%s' '' + return 1 + fi + printf -v "$var" '%s' "$( cat "$DIR/runq.out" )" +} + +echo "=== the fixture parsed the way this gate assumes ===" +for want in 'n="Parent"' 'n="Child"' 'n="GrandChild"' 'n="Base" sc="Space"' 'n="Derived"' 'n="Unrelated"'; do + grep -q "$want" "$SPLIT" && ok "indexed: $want" \ + || no "the fixture did not index $want" +done + +echo "=== a Ruby subclass is an IMPLEMENTOR in the lego view ===" +LP="$( lego Parent )" +echo "$LP" | grep -q '/dev/null )" +echo "$CLP" | grep -q 'n="call_inherited"' && ok "--callers=Parent::build lists call_inherited" \ + || no "--callers=Parent::build does not list call_inherited" +if runq CLU "$FIX" --no-cache --callers=Unrelated::build +then + echo "$CLU" | grep -q 'n="call_inherited"' && no "--callers=Unrelated::build lists call_inherited — the walk took an unrelated same-named def" \ + || ok "--callers=Unrelated::build does not list call_inherited" +fi + +CT="$( rowOf 'n="call_two_levels" ' )" +[ "$( edgesTo "$CT" build )" -eq 1 ] && ok "GrandChild.build → exactly one edge (the walk crosses TWO levels)" \ + || no "GrandChild.build produced $( edgesTo "$CT" build ) build edges: $CT" +CS="$( rowOf 'n="call_scoped_base" ' )" +[ "$( edgesTo "$CS" make )" -eq 1 ] && ok "Derived.make → exactly one edge (base written as Space::Base)" \ + || no "Derived.make produced $( edgesTo "$CS" make ) make edges: $CS" +CLM="$( "$BIN" "$FIX" --no-cache --callers=Base::make 2>/dev/null )" +echo "$CLM" | grep -q 'n="call_scoped_base"' && ok "--callers=Base::make lists call_scoped_base" \ + || no "--callers=Base::make does not list call_scoped_base" + +echo "=== an implicit-self call inside a subclass reaches the superclass method (Rule 1's bare arm) ===" +UI="$( rowOf 'n="use_inherited" ' )" +[ "$( edgesTo "$UI" shared_helper )" -eq 1 ] && ok "a bare shared_helper inside Child → exactly one edge (Parent#shared_helper)" \ + || no "a bare shared_helper inside Child produced $( edgesTo "$UI" shared_helper ) edges: $UI" +CLS="$( "$BIN" "$FIX" --no-cache --callers=Parent::shared_helper 2>/dev/null )" +echo "$CLS" | grep -q 'n="use_inherited"' && ok "--callers=Parent::shared_helper lists use_inherited" \ + || no "--callers=Parent::shared_helper does not list use_inherited" + +UB="$( rowOf 'n="use_bare" ' )" +echo "$UB" | grep -q ' instead: $( head -3 "$DIR/ref.err" )" +if runq LF "$FIX" --no-cache --lego=Factory +then + echo "$LF" | grep -q '"$DIR/impl.out" 2>"$DIR/impl.err" + then + no "--lego=$1 exited non-zero: $( head -3 "$DIR/impl.err" )" + return 2 + fi + sed 's/>\n\n"$DIR/b.xml" 2>/dev/null +cmp -s "$MAP" "$DIR/b.xml" && ok "byte-identical across two --no-cache runs" \ + || no "output differs across runs" +if ! "$BIN" "$FIX" --cache="$DIR/c.bin" >"$DIR/cold.xml" 2>"$DIR/cold.err" +then + no "the cold cache run exited non-zero: $( head -3 "$DIR/cold.err" )" +fi +if ! "$BIN" "$FIX" --cache="$DIR/c.bin" >"$DIR/warm.xml" 2>"$DIR/warm.err" +then + no "the warm cache run exited non-zero: $( head -3 "$DIR/warm.err" )" +fi +cmp -s "$DIR/cold.xml" "$DIR/warm.xml" && ok "warm run == cold run" \ + || no "the warm cache disagrees with the cold run" + +echo "=== mutation: drop the base clause → the inherited pin must vanish ===" +MUT="$DIR/mut"; mkdir -p "$MUT"; cp "$FIX/caller.rb" "$MUT/caller.rb" +sed 's/^class Child < Parent$/class Child/' "$FIX/h.rb" >"$MUT/h.rb" +grep -q '^class Child$' "$MUT/h.rb" || no "mutation did not apply" +if runq MLP "$MUT" --no-cache --lego=Parent +then + echo "$MLP" | grep -q '/dev/null | sed 's/>\n/dev/null )" +echo "$MCU" | grep -q 'n="call_inherited"' && ok "mutation: …and Unrelated::build is back among its callers" \ + || no "mutation: Unrelated::build does not list call_inherited: $( echo "$MCU" | grep -o ']*' )" + +echo +[ "$fail" -eq 0 ] && echo "ALL PASS" || { echo "SOME CHECKS FAILED"; exit 1; } diff --git a/test/rubyrecvnarrowcheck.sh b/test/rubyrecvnarrowcheck.sh index 9279e72c9..4652795b4 100755 --- a/test/rubyrecvnarrowcheck.sh +++ b/test/rubyrecvnarrowcheck.sh @@ -28,8 +28,10 @@ # that does not define the callee, degrades to the unchanged honest ladder. # # Stated floors, pinned below so each stays a decision rather than an accident: -# (a) Ruby feeds NO class-hierarchy edges — ingest_relations.h::captureBases has no Ruby arm — so -# `Child.build` with `build` on the superclass does NOT narrow, and stays an honest split. +# (a) LIFTED at parser version 98 by test/rubyinheritcheck.sh (`class Child < Parent` now mints an +# inherit ref, so chaUp holds Ruby and the base walk runs). The arm below is kept, INVERTED: it +# now asserts `Child.build` pins to Parent::build, and it is the tripwire that fires if Ruby ever +# loses its inheritance edges again. # (b) Matching is by FINAL SEGMENT, so two classes with the same last name in different namespaces # both defining the callee keep BOTH candidates (an honest split), exactly as Rule 2c documents. # (c) A variable receiver (`c.scale`) and a chained one (`Calc.new.scale`) are untouched by this round. @@ -270,10 +272,15 @@ MM="$( rowOf 'n="missing_method_call" ' )" [ "$( edgesTo "$MM" report )" -eq 1 ] && ok "Calc.report → the one report def still resolves through the honest name ladder (the narrow missed, it did not veto)" \ || no "Calc.report lost its edge — a narrow MISS must degrade to the ladder, never delete: $MM" -echo "=== floor (a): Ruby feeds no class-hierarchy edges, so a superclass method does NOT narrow ===" +echo "=== the base walk: a method on the SUPERCLASS narrows too (floor (a), lifted at parser version 98) ===" I="$( rowOf 'n="inherited_call" ' )" -[ "$( edgesTo "$I" build )" -eq 2 ] && ok "Child.build → an honest 2-way split (captureBases has no Ruby arm; floor, stated)" \ - || no "Child.build produced $( edgesTo "$I" build ) build edges — if Ruby gained inheritance edges, say so HERE, in ingest_relations.h::captureBases and in CHANGELOG.md: $I" +[ "$( edgesTo "$I" build )" -eq 1 ] && ok "Child.build → exactly one edge (Parent::build, through the inheritance edge captureBases now mints for Ruby)" \ + || no "Child.build produced $( edgesTo "$I" build ) build edges — if Ruby LOST its inheritance edges, that is the regression: see ingest_relations.h::isBaseTypeNode's Ruby arm and test/rubyinheritcheck.sh: $I" +CB="$( "$BIN" "$FIX" --no-cache --callers=Parent::build 2>/dev/null )" +echo "$CB" | grep -q 'n="inherited_call"' && ok "--callers=Parent::build lists inherited_call" \ + || no "--callers=Parent::build does not list inherited_call" +CBU="$( "$BIN" "$FIX" --no-cache --callers=Unrelated::build 2>/dev/null )" +echo "$CBU" | grep -q 'n="inherited_call"' && no "--callers=Unrelated::build lists inherited_call — the walk took an unrelated same-named def" || ok "--callers=Unrelated::build does not list inherited_call" echo "=== floor (b): two same-final-segment classes both defining the callee keep BOTH (honest split) ===" SS="$( rowOf 'n="same_segment_call" ' )"