Skip to content

fix(resolve): a member field typed std::string narrowed to an in-repo class named string - #257

Merged
joyful-ii-V-I merged 10 commits into
mainfrom
lane/std-field-compose
Sep 17, 2026
Merged

joyful-ii-V-I merged 10 commits into
mainfrom
lane/std-field-compose

Conversation

@joyful-ii-V-I

@joyful-ii-V-I joyful-ii-V-I commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #248 and #254; review only the last commit (50129f8). It lands in the coordinator's integration train after #248 and #254, which is where main gets merged in, kParserVer gets its final number and the qschemetrip hash is re-derived.

The defect

struct Record { std::string name_; int nameLength() { return name_.size(); } }; pinned name_.size() to an in-repo string::size: census mech=receiver-rule, one precise edge, no amb=. The field capture keeps a qualified type's final segment. All three readers of that record took string for any in-repo class with that name:

  • Rule 2b's field-type table.
  • The HAS-A edges (Record → string).
  • The member index (--uses=string.len pinned name_.len and this->name_.len).

The previous commit on this stack refused a std:: written type for locals and parameters; fields are the third place a written type is read.

The change

  • Capture: a field's compose record carries the namespace its type was written in as its qualifier, the same (name, immediate qualifier) pair a call carries. kParserVer 98 → 99 as committed, plus the quality.h mirror (the final number is assigned in the train); the cache format is unchanged.
  • Readers: resolve.h fieldTypeWrittenInStd is the one predicate. buildFieldNarrowTables records "" for such a field, and the HAS-A loop draws no edge. The member index reads the same table.
  • Other qualifiers: they narrow as before. store::Text body_; body_.size() still reaches Text::size.
  • Why "" and not a skip: rocksdb's two StringSource classes share the key StringSource#contents_ (std::string in testutil.h, Slice& in log_test.cc). Skipping the std record un-tombstones that key, and four testutil.h contents_.size() calls then pin Slice::size.

Audit. Every reader of Reference::qualifier excludes compose refs:

  • the resolve loop and census (isResolvableCallReference);
  • the model.h isCompose filters;
  • the inherit-only loops;
  • the Python/Rust/Elixir-only readers.

The other ing.references loops that don't filter isCompose never read qualifier, and the ref sort key excludes it.

Measured

--pin-census --no-cache and the default map, cfa5792 vs this commit:

  • Corpora: rocksdb, a private C++ corpus and src/ (frozen copy) are byte-identical, and so is rocksdb's --metrics.
  • Why: none has an in-repo class that shares a std:: field type's name and defines the called member. Fields written std::X: 1,258 on rocksdb, 1,987 on src/, 143 on the private corpus (counted with --match).
  • Probe: rocksdb plus one in-repo shim::string defining size, empty, data, c_str, clear, append and len.
    • Pins to shim::string: 384 before → 7 after. The 7 are gtest const ::string& parameters (global namespace, not std).
    • 357 sites move: 279 land on the unmodified rocksdb census row, and 78 are c_str calls the probe's own second c_str makes ambiguous.
    • --metrics: 455 class rows lose a composed type.

Gates

  • test/fieldnarrowcheck.sh arm q: 44 PASS.
    • Red on cfa5792: q1, q3, and both q5 rows.
    • Red on a refuse-every-qualifier variant: q2, q4, q6 and all four q7 rows.
    • Red on the skip-at-capture variant: exactly the four q7 rows.
  • Targeted gates, all green: narrowcheck, qextractionkeycheck, qschemetripcheck (re-pinned with a dated note), composelangcheck, fieldusescheck, stdqualcheck, cacheidentitycheck, chacheck, chaconecheck, resolverhonestycheck, localitycheck, clsrecvcheck, shadowcheck, fnptrcheck, externalvetocheck, manifestcheck, gatecountcheck, limitstablecheck, showcasecapturecheck (graph.h is line-neutral), gateexitcheck.
  • ASan+UBSan+LSan: clean on the fixtures and on the rocksdb probe; the sanitizer build's census is identical to the plain build's.
  • Determinism and xmllint: clean on the map, --around, --uses and --for.
  • Full suite, local, one serialised run on 50129f8: gates=632 pass=629 skip=2 fail=1. The two skips are the environmental ones (argvdiffcheck, editchecknotecheck). The one failure was versioncheck, a true positive about the binary's stamp: it read built_from=e20790c92+dirty because I built before amending the commit, and the amend changed only test/qschemetrip.hash and test/qschemetripcheck.sh. After a relink, versioncheck, fieldnarrowcheck and g1freshcheck pass standalone, and the relinked binary's probe census and map are byte-identical to the binary the suite ran.
  • quality-delta, merge-base range: gating=0.

Pins moved

  • test/qschemetrip.hash: the parser version moved, and the new pin equals the gate's printed current= on cfa5792 + this commit. It gets re-derived on the train's merged tree.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved call resolution using declared types for parameters, range-loop variables, and local references.
    • Added support for qualified in-repository types while preserving correct behavior for external standard-library types.
  • Bug Fixes

    • Prevented standard-library types from incorrectly matching same-named project types.
    • Improved handling of unknown or conflicting receiver types, preserving honest ambiguity instead of applying incorrect locality hints.
    • Corrected declaration position tracking for more reliable analysis.
  • Documentation

    • Updated changelog and evaluation notes with the revised resolution behavior.

joyful-ii-V-I and others added 10 commits September 16, 2026 13:39
…he caller's own class

`int Decoy::plainCaller( Target& other ) { return other.pick( 1 ); }` answered
--callees=plainCaller with ONE edge to Decoy::pick: precise, no amb=, wrong. Rule 2 narrowed
a receiver only through a typed local (LocalBindKind::Type); a parameter's written type
(ParamType) was captured but read only by collectFieldUseSites, so the call fell through to
the name ladder and the S6-C locality tie-break gave the caller's own class the scope credit
(pin census: mech=locality, 2 -> 1).

The fix: Rule 2, and CHA-lite through the same lookup (Narrower::recvVarTypeName), reads
ParamType records LEXICALLY. graph.h buildScopedRecvDecls lists, for every name with a
ParamType record, all its declarations in the definition (each VarDecl with its scope span)
and attaches each Type/ParamType record to the declaration whose VarDecl shares its record
position (Binding::startByte, new, in the padding after `kind`: sizeof unchanged, pinned).
The innermost declaration covering the call site decides; an untyped one, a tie, or no
covering declaration answers nothing. Names with no ParamType record keep the flat varType
table byte-identically.

Why not the obvious fold into varType: measured first. It fixed arms 7/8/11/15 and minted
three NEW precise wrong edges on the gate fixture (arms 12-14: a range-for variable's type
reaching a later `auto` loop of the same name, a same-named field read after the loop, a
parameter hidden by an untyped loop variable), and arm 10's tombstone kept the locality pin.

Qualifier guard: a written type is its final segment and class names carry no namespace, so
`const std::map<K, V>& ref; ref.lower_bound( q )` narrowed to an unrelated in-repo `map` —
three such edges on a private 129,759-call-site C++/ObjC++ corpus. A declaration's qualified
written type now rides its Type/ParamType RawBind (importedName, ingest_binds.h
qualifiedNameText) and the lexical lookup refuses it: removes all three, forgoes 11 correct
narrows through qualified in-repo types (those keep main's answer). kParserVer 96 -> 97 (+
quality.h mirror): the record format is unchanged, its content is not, and a warm 96 blob
would narrow a qualified type. An include-visibility guard was measured first and rejected:
path-precise includes miss include-root spellings and it refused ~150 correct narrows there.

Measured, --pin-census --no-cache, main f8e6087 binary vs this change:
  private corpus: 587 of 129,759 sites change target — 373 splits narrow (300 Rule 2,
    73 CHA cone), 147 declined calls gain an edge (bound 80,432 -> 80,583), 66 pins/splits
    move to the parameter's type, 1 edge lost (a friend function scoped inside its class
    self-loops); 956 more keep their target, now decided by Rule 2. Every category sampled
    and read against the source. Wall time within noise (3 cold runs each, 1.66-2.51 s).
  this repo src/: 53 splits -> one Rule-2 pin; nothing else moves target.

Gate: test/narrowcheck.sh arms 7-18 (generated fixture). RED on main: 10 rows (7, 8, 10 x2,
11, 12, 13, 14, 15). RED on the flat-table fold: 10 x2, 12, 13, 14. RED on the lexical lookup
without the qualifier guard: 17. GREEN on this commit: 23 PASS, and under ASan.
Controls that encoded "a parameter has no binding" moved to an untyped `auto` receiver:
narrowcheck (control/param.cpp -> control/untyped.cpp), chacheck, chaconecheck g5,
localitycheck (its call no longer reached the tie-break it tests), resolverhonestycheck F9
(check_signal had gone vacuous on one edge). fieldnarrowcheck (h) 7 -> 6: shadowParam(
Decoy& m_x ) now resolves to the parameter's type; (s1) also asserts Pool::acquire is not
linked (red on main).

Out of scope, stated: an untyped receiver still reaches the locality tie-break; typed locals
keep the flat table and its qualified-type collision. The reported arity split on a typed
local (pick(int) + pick(int,int) for a 1-arg call) is B2.2's documented argCount > params
rule and reproduces identically on the bare-name ladder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CHANGELOG.md: both Unreleased entries kept (#240's Changed, this lane's Fixed).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ument constructor and a 30-complexity builder

quality-delta over the merge-base range reported gating=3 on this lane's own rows: ctorTypeOf's
byte-bounds tail cloned nodeTextOf (duplication + new-clone-of-reused-helper), and Narrower's
constructor went 4 -> 6 parameters against a bar of 5. Non-gating but real: the new
buildScopedRecvDecls at cognitive complexity 30, and bindsVisitNode 88 -> 94 with three new
parameters across pushRawBind/emitBind/emitDeclBinds.

- ctorTypeOf and qualifiedNameText read through nodeTextOf.
- ScopedRecvDecls is one struct holding the table and the bindings its indices point into, so the
  Narrower takes one argument for both (5 parameters).
- buildScopedRecvDecls is three single-loop steps: addParamTypedNames, addRecvDeclScopes,
  attachRecvDeclTypes.
- DeclType { name, qualified } travels as one argument. pushTypedBind is now the ONE record body
  and pushRawBind a one-line wrapper with no qualified text, so pushRawBind and emitBind keep
  their signatures, and declaredTypeOf takes the declaration branch's two ternaries out of
  bindsVisitNode. (A first cut stamped importedName after the push, emitFnBind's shape — and
  quality-delta reported it as a 70-token clone of emitFnBind.)

Zero behaviour change, measured: --pin-census and the default map on the private corpus are
byte-identical (cmp) to the previous commit's binary, as is the census over src/; narrowcheck,
chacheck, fieldnarrowcheck, resolverhonestycheck, fieldusescheck, shadowcheck and
cacheidentitycheck ALL PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
quality-delta --quality-delta=$(git merge-base origin/main HEAD)..HEAD reported gating=2 after the
refactor: api-surface contract-change on Narrower::Narrower and its one call site in buildGraph
(4 -> 5 parameters). That is the fix itself — the Narrower has to be handed the lexical
declaration table — so it is acked through the binary (--quality-ack --ack-only=api-surface),
which wrote exactly two +ack rows, one per symbol, and nothing else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d, a moved parser-version pin, two one-line verdicts

Full suite at 17a9f5f: gates=632 pass=627 skip=2 fail=3, all three this lane's own.

showcasecapturecheck (H): the 0.6.1 release capture publishes --at=src/graph.h:3406 as the FIRST
body line of rankGraphTeleport, so it has zero slack — the ~100 lines this lane added to graph.h
above it made 3406 resolve to buildGraph. The capture is the release PR's to regenerate (a lane
recording it publishes local branch names), and the code had a better home anyway: the lexical
table builder (buildScopedRecvDecls and its three steps) moves from graph.h to resolve.h, beside
the ScopedRecvDecls type and Narrower::recvVarTypeName that reads it. graph.h's diff is now five
lines in, five out, at the Narrower construction; rankGraphTeleport is back at 3404 on this tree.

qschemetripcheck: its manifest hashes ingest_cache.h's kParserVer declaration, which moved 96 -> 97
with the quality.h mirror in the same diff (qextractionkeycheck green), so this is the gate's own
re-pin case: UPDATE_GOLDEN=1, and the new pin equals the current= hash the suite printed.

gateexitcheck (G2): narrowcheck's expectRows helper and arm 18 reported through a one-line
'A && ok || no'; both are if/else now.

No behaviour change: --pin-census and the default map over the private corpus are byte-identical
(cmp) to the fix commit's binary. qschemetripcheck, gateexitcheck, showcasecapturecheck,
narrowcheck, chacheck, chaconecheck, localitycheck, fieldnarrowcheck, resolverhonestycheck,
fieldusescheck, shadowcheck, fnptrcheck, cacheidentitycheck, qextractionkeycheck, manifestcheck
ALL PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…caller's own class

`auto other = make(); return other->pick( 1 );` inside Decoy answered --callees with one
precise edge to Decoy::pick, no amb=. The S6-C locality tie-break excluded field-of-this,
field-of-var and super() receivers but kept every NamedVar receiver on the premise "a typed
var already narrowed above" — and a receiver no rule typed never narrowed. The scope-segment
credit then handed the same-file tie to the caller's own class.

The fix: a NamedVar receiver whose type no receiver rule (2, 2c, 2b) established keeps the
tie-break's credit only up to the end of the caller's FILE segment (`path::`), so same-file
and same-directory locality still count and the class segment does not. That covers an
untyped local, a member Rule 2b cannot read (`std::unique_ptr<Target> rep_; rep_->pick()`)
and a typed variable whose type defines no such method. `this->` and bare calls keep the
whole tie-break.

Measured first, with --pin-census --no-cache, the lane tip's binary against this logic built
behind a temporary environment switch (removed; the final binary is measured in the next
commit): rocksdb 121 call sites change target (114 locality pins -> splits, 7 splits widen),
a private C++ corpus 71 (51 + 20), django 72, rails 145 (119 + 26), this repo's src/, vue-core
and go/net 0. Samples read against the source: 14 of 14 rocksdb pins wrong, 14 of 16 on the
private corpus — mostly delegation (`rep_->Name()` in Wrapper::Name, `tree_.upper_bound( key )`
in btree_container::upper_bound); one right pin is now a 3-way split that keeps it.
Skipping the tie-break entirely for these receivers moved NO target on any of the seven
corpora, but relabelled every {caller itself, X} tier from locality to unique (262 rocksdb,
53 private, 24 django, 101 rails) and dropped its lpin= disclosure — so the file credit stays.
Stated floor, not closed: tier 1 still admits same-file candidates alone, so a delegation
whose true target lives in another file lands on a same-file namesake before the tie-break.

Gate: test/localitycheck.sh arms 5-9 on a generated fixture. RED on the lane tip: 5
(untyped local), 6 (member of an unreadable type), 7 (typed, no such method) — each a
locality pin to the caller's class. Arm 8 (file credit kept, mech=locality) is RED on the
skip-the-tie-break variant; arm 9 is the Rule-2 control. GREEN on this commit.
Controls moved for the reason the fix exists: localitycheck's HIGH-1 probe was an untyped
local that no longer earns scope credit, so it could not tell a byte-prefix tie-break from a
segment-aware one; it is `this->go()` in a class template with a dependent base now, plus a
census liveness row (pre=2). clsrecvcheck (B)/(C)/(E) asserted the caller-own-class pin
STANDS for `item.validate( v )`; they assert the honest two-way split now (the contrast with
the firing route is unchanged) and (F) reads ambiguous=3 with no locality pin. canoncheck's
comment describing the fixture is updated. chacheck, chaconecheck, resolverhonestycheck and
fieldnarrowcheck pass unchanged: their untyped controls are scope-less free functions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…esake, and an in-repo qualifier refused a parameter

`std::map<int, int> table; table.find( k )` pinned an in-repo map::find: Rule 2's flat
per-function table records a written type's final segment, and class names carry no
namespace. The parameter-receiver lane refused every QUALIFIED parameter type for the same
reason; typed locals, constructor-inferred locals and C++ assignments kept narrowing.

Measured before choosing the rule, --pin-census --no-cache, lane tip vs switches (removed):
refusing ANY qualifier on locals moved rocksdb 424 sites, a private C++ corpus 27, src/ 9.
Joined to each receiver's qualified text: every rocksdb and src/ one was a non-std qualifier
(ROCKSDB_NAMESPACE::Status, test::SleepingBackgroundTask, rw::notes::NoteIndex), every sampled
one read correct, and it minted new wrong ladder pins where the refused narrow fell to a same-dir
namesake (`value_slice.data()` -> MultiGetJNIKeys::data). The only wrong narrows it removed on
the three corpora were six `std::map` locals on the private corpus. The same join on the
lane's parameter guard: refusing only std:: instead moves rocksdb 94 sites (e.g.
`WriteBatch::Handler* handler; handler->MarkCommit( xid )` off WriteBatchInternal::MarkCommit),
private 12, src/ 5, all sampled correct but ONE: an `ankerl::unordered_dense::map<…>&` alias
template that narrows to the in-repo map again.

The fix: `std` is reserved to the implementation, so no in-repo class IS a std:: type.
resolve.h namesStdType is the one predicate: the lexical (parameter) lookup refuses a std::
written type, and the flat table tombstones the variable on one (a tombstone, because the flat
table cannot tell which declaration of the name is in scope). Every other qualifier narrows on
its final segment again, parameters included. The C++ assignment `x = std::map<K, V>()`
recorded no qualified text; it goes through pushTypedBind with declaredTypeOf's constructor
DeclType now, so kParserVer 97 -> 98 (+ quality.h mirror): format unchanged, content changed.
Stated floor, pinned by narrowcheck arm 24: a qualifier that is neither std nor the class's
own namespace still narrows by name; closing it needs the namespace chain in Symbol::scope.

Final binary (this commit, both fixes) vs the lane tip, --pin-census --no-cache: rocksdb 211
sites change target (117 locality, 94 qualified parameters; bound= 200,009 -> 200,036), private
88 (70 locality, 6 std::map locals stop narrowing, 12 qualified parameters), django 72, rails
145, src/ 5, vue-core 0, go/net 0. The assignment capture moved no site on these corpora.
Wall time on rocksdb within noise (3 cold runs each, load avg 42: 1.69-2.19 s vs 1.73-2.80 s).

Gate: test/narrowcheck.sh arms 17-24. RED on the lane tip: 19 (typed local), 20 (constructor
local), 21 (assignment), 23 (in-repo qualified parameter), 24 (the floor: the lane refused it).
RED without the assignment capture: 21. GREEN on this commit. Arm 17 moves from `ext::map` to
`std::map` (the refused shape) and 22 is the in-repo qualified local control.
quality-delta (working tree vs the previous commit): gating=0; a first cut's prefix-strip loop
cloned lintdetail::ltrim and is gone (qualifiedNameText already strips a leading `::`).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…yped-locality

The lane pushed a fix commit for three full-suite reds after this branch was cut; two of them
bind this branch's change as well, and the resolution keeps both sides' intent.

src/graph.h: the lane moved the lexical receiver-table builder to resolve.h so graph.h stays
line-neutral above rankGraphTeleport (the 0.6.1 showcase capture seeds --at=src/graph.h:3406
with zero slack). Took theirs, re-applied this branch's std-guard comment to the moved block,
and made this branch's own graph.h change line-neutral too (+9/-9 against the lane tip,
rankGraphTeleport back at 3404): the tie-break rationale moves into resolve.h
receiverLocalityCap (the cap itself, beside sharedLocality), the flat varType key uses the
existing buildShadowKey (identical bytes), and the flat-table tombstone note joins
namesStdType's comment. No behaviour change: --pin-census over src/, rocksdb and the private
corpus is byte-identical (cmp) to f346283's binary.

test/narrowcheck.sh: took this branch's arms 17-24 with expectNarrow's verdict as if/else
(gateexitcheck G2, the lane's same fix to expectRows and arm 18).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… line this branch moved

qschemetripcheck hashes ingest_cache.h's kParserVer declaration; f346283 moved it 97 -> 98 with
the quality.h mirror in the same diff (qextractionkeycheck green). This is the gate's own re-pin
case: UPDATE_GOLDEN=1, and the new pin (8ca11f67…) equals the current= hash the red run printed.
kQSnapCacheScheme stays 12 and kCacheVersion 22 — record layouts and snapshot semantics are
unchanged. The RE-PIN LOG entry says so, and names the 96 -> 97 move the parameter-receiver lane
re-pinned without one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… class named string

`struct Record { std::string name_; int nameLength() { return name_.size(); } };`
pinned name_.size() to an in-repo `string::size` (census mech=receiver-rule, no
amb=). The field capture (ingest_relations.h captureFields) keeps a qualified
type's final segment, and all three readers of that record took `string` for
any in-repo class of the name: Rule 2b's field-type table, the HAS-A edges
(Record -> string), and the member index (--uses=string.len pinned name_.len and
this->name_.len). The previous commit refused a std:: written type for locals
and parameters; fields were the third place a written type is read.

The fix: a field's compose record carries the namespace its type was written in
as its qualifier (writtenTypeNamespace) — the (name, immediate qualifier) pair a
call ref already carries, so `std::string` is `string` in `std`. resolve.h
fieldTypeWrittenInStd is the one predicate; buildFieldNarrowTables records ""
for such a field and the HAS-A loop draws no edge. The member index reads the
same table, so it needs no change of its own. Every other qualifier narrows as
before (`store::Text body_; body_.size()` still reaches Text::size).
kParserVer 98 -> 99 + the quality.h mirror: format unchanged, content changed.

It records "" rather than skipping the field, because a skip is a measured
wrong answer: rocksdb's test_util/testutil.h `StringSource { std::string
contents_; }` and db/log_test.cc `StringSource { Slice& contents_; }` share the
key StringSource#contents_ (class names carry no namespace), the disagreement
tombstones it, and dropping the std record at capture pinned four testutil.h
contents_.size() calls to Slice::size. An empty type tombstones the entry
exactly as a second real type does.

Audit before touching a compose ref's fields: every reader of
Reference::qualifier excludes compose refs — the resolve loop and pin census
(isResolvableCallReference), model.h macro retag and shadow suppression
(isCompose filters), the inherit-only loops (graph.h isInherit), and the
Python/Rust/Elixir-only readers (quality.h, keepRustQualifiedCandidates,
elixir_resolve.h, expandElixirImplementationReferences). The other loops over
ing.references that do not filter isCompose never read qualifier (eval.h,
main.cpp byte stats, impurity and LCOM4 name scans, scip/filter/workspace
copies); the ref sort key is (fileId, startByte, name, role, isInherit). The
qualifier already rides the cache record, so no layout change.

Measured, --pin-census --no-cache and the default map, cfa5792's binary vs
this commit: rocksdb, a private C++ corpus and src/ (a frozen copy, one tree
for both binaries) are byte-identical, and rocksdb's --metrics is too. Counted
with --match, the corpora have 1,258 / 143 / 1,987 fields written `std::X`;
only rocksdb's 1,116 std::string/wstring fields share a name with an in-repo
type (gtest's `typedef ::std::string string`, no members, no HAS-A edge). So a
probe measured the reach: rocksdb plus one `namespace shim { struct string {
size empty data c_str clear append len } }`. The previous binary pins 384 call
sites to shim::string by receiver-rule; this commit leaves 7, all gtest
parameters written `const ::string&` (global namespace, not std, not fields).
357 sites move: 279 land on exactly the unmodified rocksdb census row, 78 are
c_str calls the probe's own second c_str makes ambiguous. --metrics: 455 class
rows lose a composed type from cbo= (446 by one, 9 by two).

Gate: test/fieldnarrowcheck.sh arm q (new fixtures, so (h)'s gauge is
untouched). RED on cfa5792 and on f346283: q1 (Rule 2b), q3 (HAS-A), q5 x2
(--uses, bare and this->). RED on a refuse-every-qualifier capture variant:
controls q2, q4, q6 and the four q7 tombstone rows. RED on the skip-at-capture
variant: exactly the four q7 rows — StringSource/StringSink collisions in both
record orders. (Variants measured on the f346283-based tree; the rebase onto
cfa5792 touched neither captureFields nor the two readers.) GREEN here, 44
PASS. qschemetripcheck re-pinned for the parser version with a dated note, as
its message directs (new pin == the current= it printed).

graph.h stays line-neutral above rankGraphTeleport (3404), so the 0.6.1
capture's published seed still resolves (showcasecapturecheck green). A first
cut hoisted the std-qualified call guard's lambda into a shared function;
--quality-delta read it as a 47-token clone of docparse.h's extension
lookups (gating=3), so the call guard keeps its lambda and the field readers
use a Reference-level predicate. quality-delta (working tree vs HEAD):
gating=0, six minor rows (churn; captureFields LOC 120 -> 121).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@joyful-ii-V-I joyful-ii-V-I added this to the 0.6.2 milestone Sep 17, 2026
@coderabbitai

coderabbitai Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: a0c9d2eb-6b0a-43c3-8c70-5bfca7524853

📥 Commits

Reviewing files that changed from the base of the PR and between 105666c and 50129f8.

⛔ Files ignored due to path filters (1)
  • test/qschemetrip.hash is excluded by !test/*.hash
📒 Files selected for processing (26)
  • .ripwire_quality_acks
  • CHANGELOG.md
  • docs/EVALS.md
  • src/graph.h
  • src/ingest_binds.h
  • src/ingest_cache.h
  • src/ingest_model.h
  • src/ingest_relations.h
  • src/model.h
  • src/quality.h
  • src/resolve.h
  • test/canoncheck.sh
  • test/chacheck.sh
  • test/chaconecheck.sh
  • test/chaconefix/b.cpp
  • test/chafix/cha.cpp
  • test/clsrecvcheck.sh
  • test/clsrecvfix/boxes.py
  • test/fieldnarrowcheck.sh
  • test/localitycheck.sh
  • test/localityfix/loc.cpp
  • test/narrowcheck.sh
  • test/narrowfix/control/param.cpp
  • test/narrowfix/control/untyped.cpp
  • test/qschemetripcheck.sh
  • test/resolverhonestycheck.sh
💤 Files with no reviewable changes (1)
  • test/narrowfix/control/param.cpp

Included review availability: Your plan provides up to 8 included reviews per hour; 4 remain after this review.


📝 Walkthrough

Walkthrough

The change adds lexical receiver-type narrowing, preserves qualified type provenance, excludes std-written types from narrowing, adjusts locality resolution, bumps parser identities, and expands regression coverage.

Changes

Receiver narrowing and type provenance

Layer / File(s) Summary
Capture declaration type provenance
src/ingest_binds.h, src/ingest_model.h, src/ingest_relations.h, src/model.h
Bindings now retain declaration positions, qualified written types, and field-type namespaces.
Build lexical receiver resolution
src/resolve.h
Scoped declaration tables resolve the innermost receiver type. std-written types are excluded, and locality credit is capped when no receiver rule establishes a type.
Integrate narrowing and cache changes
src/graph.h, src/ingest_cache.h, src/quality.h, test/qschemetripcheck.sh
Graph construction consumes scoped receiver declarations, excludes std-written field relations, and advances parser-version identities from 96 to 99.
Update regression coverage and records
test/*, CHANGELOG.md, docs/EVALS.md, .ripwire_quality_acks
Tests and fixtures cover parameter shadowing, unknown receivers, locality behavior, qualified types, field relations, and cache stability. Documentation records the updated behavior.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix

Suggested reviewers: andriytyurnikov

Merge Risk: ⚪ Minimal · up to 50129

The reviewed receiver and field-resolution changes have no remaining supported merge-blocking risk.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.69% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 64 functions across 22 files. (3 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main fix: preventing a std::string member field from narrowing to a same-named in-repository class.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 54.69% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 64 functions across 22 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch lane/std-field-compose
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lane/std-field-compose

Comment @coderabbitai help to get the list of available commands.

@joyful-ii-V-I

Copy link
Copy Markdown
Collaborator Author

This lands through integration train 2, #265, with parser version 99. Resolutions are on that branch, so nothing more is needed here. This PR will show as merged when #265 merges.

@joyful-ii-V-I
joyful-ii-V-I merged commit 9cc27f7 into main Sep 17, 2026
1 check passed
joyful-ii-V-I added a commit that referenced this pull request Sep 17, 2026
integration: train 2 — param receiver binding (#248), untyped receiver locality + final-segment disclosure (#254), std:: field compose (#257)
joyful-ii-V-I added a commit that referenced this pull request Sep 17, 2026
…ane's base); no conflicts, no pins move

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joyful-ii-V-I added a commit that referenced this pull request Sep 17, 2026
…ype, arms renumbered after #254's 25, identity claims carry no prov="final-segment"

origin/main bcd3b01 brings #248, #254 and #257. Two files conflicted; rerere was off and no recorded resolution was used.

- src/resolve.h: #254 added a namespace-level RecvVarType {name, writtenQualified}, and this lane added a nested
  Narrower::RecvVarType {name, declared}. Git merged the second in beside the first, so it would have shadowed it. Both
  are now ONE namespace-level struct {name, writtenQualified, declared}; recvVarType fills all three, and
  finalSegmentTypeAt is #254's own.
- test/narrowcheck.sh: #254's arm 25 (prov="final-segment") keeps its number. This lane's arms 25-34 become 26-35;
  expectProv/provOf take an optional map file ("${5:-}" under set -u).
- src/graph.h (line-neutral; rankGraphTeleport stays at 3404): an identity CLAIM verified its one class before
  answering, so its edges are not a last-name guess. Two lines change in place: `&& !identityClaim` joins the
  finalSegmentType condition, and its comment is reworded. A class-qualified step-1 narrow keeps the disclosure.
- Arm 36, `hs::DiskHealth& d; d.fine()` claiming the inherited HealthBase::fine through namespace evidence:
  (36a) the edge, (36b) no prov=, (36c) `Skip::Iterator& it; it.key()` keeps prov="final-segment". A build without
  the graph.h condition fails (36b) with prov=[final-segment].
- CHANGELOG entry and ack text: the arm numbers only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
andriytyurnikov pushed a commit to andriytyurnikov/ripwire that referenced this pull request Sep 17, 2026
…no side hashed the merged manifest

Main's pin (37dac79a) hashed parserVer 96 over redhat-et#249's new deserializeSnapshot; redhat-et#248 (db0e8cce, 97), redhat-et#254
(8ca11f67, 98) and redhat-et#257 (01ea4c63, 99) each hashed their own number over the manifest before redhat-et#249. The merged
tree declares 99 over redhat-et#249's function, which none of them hashed. kParserVer is assigned in merge order and
equals what each lane already declared (redhat-et#248 97, redhat-et#254 98, redhat-et#257 99); kCacheVersion stays 22, kQSnapCacheScheme 12.
New pin 95818fe5 == the current= the gate printed before UPDATE_GOLDEN; qschemetripcheck and qextractionkeycheck
ALL PASS on this tree. RE-PIN LOG entry added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aniruddhaadak80 pushed a commit to aniruddhaadak80/ripwire that referenced this pull request Sep 17, 2026
…gration/train-2b on main bcd3b01

Conflicts and resolutions (rerere disabled):
- src/ingest_cache.h / src/quality.h: kParserVer and its mirror -> 100 (the PR declared 97; assigned in merge order
  over train 2's 99). redhat-et#244's note renumbered and placed above train 2's 99/98/97 notes.
- test/qschemetripcheck.sh: redhat-et#244's RE-PIN LOG entry kept, renumbered to 100, above train 2's entry; its
  "MERGE origin/main into PR redhat-et#244 ... stays 97" entry dropped (superseded by the train's single re-derivation).
- test/qschemetrip.hash: main's value kept as a placeholder; re-derived ONCE on the final train tree.
- test/fieldnarrowcheck.sh: train 2's section (q) (std-typed member fields, redhat-et#257) kept whole; redhat-et#244's TS/JS literal
  receiver section header and corpus variables replace the old "KNOWN GAP" header both sides started from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aniruddhaadak80 pushed a commit to aniruddhaadak80/ripwire that referenced this pull request Sep 17, 2026
…ely resolved edge

redhat-et#254 marks an edge prov="final-segment" when a parameter's or local's QUALIFIED written type chose it by
its last name alone. Rule 2b makes the same guess for a member field: `store::Text body_; body_.size()`
narrowed on `Text`, never checked `store`, and its edge read as uniquely resolved.

The fix: redhat-et#257 gave a field's compose record the namespace its type was written in, so Rule 2b's
"Class#field" table now holds resolve.h FlatRecvType ({type, writtenQualified}), the shape redhat-et#254's flat
table already uses. Both tables fold through one core, recordFlatRecvTypeFact (the binding fold,
recordFlatRecvType, now calls it). Narrower::fieldEntryAt is the one "Class#field" lookup, shared by
rule2bFieldRecvType and the new fieldFinalSegmentAt, so the narrow and its disclosure cannot read
different entries. buildGraph records that Rule 2b decided the site and ORs fieldFinalSegmentAt into
finalSegmentType. Decisions are unchanged: the mark discloses, never demotes. An unqualified field narrow
stays unmarked, a std:: field never narrows, and extraction is unchanged, so kParserVer stays 99. The legend
text ("a qualified type matched by last name only") already covers fields, so no legend pin moves. graph.h
stays line-neutral above rankGraphTeleport (3404).

Measured, the integration/train-2 binary (92b4c91) against this change, --no-cache:
--pin-census is byte-identical on rocksdb, a private C++ corpus and src/ (frozen copy). The default map
is byte-identical on all three (rocksdb 31,711 B). Across the full map (--top-k=1000000), final-segment
marks grow on rocksdb 355 -> 382 (+27, +567 B), private 98 -> 143 and src/ 80 -> 81. Every sampled new
mark is a qualified field type: InternalStats::CompactionStatsFull compaction_stats_ -> SetMicros,
toku::locktree_manager ltm_ -> set_max_lock_memory, strkern::Byteset256 heads -> contains.

Gate: test/fieldnarrowcheck.sh arm r (49 PASS). RED on the train binary: (r1), the qualified field's mark,
and (r5), the compact legend term. RED on a variant that marks every Rule 2b narrow: (r3) alone (unqualified
field stays unmarked). Controls: (r2), the std field has no edge, and (r4), the census still names Rule 2b's
single edge. narrowcheck, chacheck, chaconecheck, localitycheck, resolverhonestycheck, fieldusescheck and
clsrecvcheck all pass.

quality-delta (working tree vs HEAD): gating=0 after one ack through the binary (--ack-only=gating). It
raises buildGraph's existing short-horizon-churn ack from 21 to 69: the resolve loop that decides a
Rule 2b narrow lives in buildGraph, and moving the decision into Narrower would still edit buildGraph and
grow the constructor redhat-et#254 was reviewed down from. buildGraph complexity 780 -> 782 is a minor row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant