Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1816,9 +1816,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published
### In the tests

<details>
<summary><b>618 gate scripts</b>, five contracts no unit test can hold, and the house rule: write the gate before the code it measures</summary> <!-- gatecount -->
<summary><b>619 gate scripts</b>, five contracts no unit test can hold, and the house rule: write the gate before the code it measures</summary> <!-- gatecount -->

`test/regression.sh` names **618 gate scripts** and is the authoritative list; <!-- gatecount -->
`test/regression.sh` names **619 gate scripts** and is the authoritative list; <!-- gatecount -->
`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
Expand Down
591 changes: 299 additions & 292 deletions docs/COMMANDS.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions docs/EVALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | 618 gate scripts plus the determinism, cache-transparency and golden contracts. <!-- gatecount --> |
| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 619 gate scripts plus the determinism, cache-transparency and golden contracts. <!-- gatecount --> |
| **`--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)
Expand Down Expand Up @@ -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 **618 gate scripts**, all of which exist on disk. <!-- gatecount -->
naming **619 gate scripts**, all of which exist on disk. <!-- gatecount -->

`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`.
Expand Down Expand Up @@ -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 618. The <!-- gatecount -->
~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 619. The <!-- gatecount -->
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`
Expand Down
7,151 changes: 7,151 additions & 0 deletions docs/captures/COMMANDS_showcase_2026-09-15.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions present/deck5_ripwire_build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
["618 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
["619 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."],
Expand All @@ -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, "618", "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, "619", "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",
Expand Down Expand Up @@ -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"],
["618 gate scripts", "bash test/manifestcheck.sh"], // gatecount
["619 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/"],
Expand Down
9 changes: 9 additions & 0 deletions queries/java/tags.scm
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@
(method_invocation
name: (identifier) @name) @reference.call

; Candidate Type::method member name (issue #74). This query CANNOT distinguish a simple type
; receiver from a variable receiver: the pinned grammar spells both as `identifier`. Ingest stamps
; the site as JavaTypeCandidate and graph.h admits it only with a type-receiver proof (indexed class
; plus lexical shadowing at the site); every failed proof stops before name fallback. The anchor
; captures only the member, and Type::new has no identifier after `::`, so it stays uncaptured.
(method_reference
"::"
(identifier) @name .) @reference.call

; new Foo( .. ) — object creation resolves to the constructor / class name
(object_creation_expression
type: (type_identifier) @name) @reference.call
Expand Down
155 changes: 155 additions & 0 deletions src/graph.h
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,8 @@ struct FieldNarrowTables
{
HashMap<std::string, std::string> fieldTypeByClass;
HashMap<std::string, char> localNameSet;
HashMap<std::string, rw::SmallVec<VarSpan, 1>> localShadowSpans; // VarDecl spans keyed "<fromSymbol>#<var>"
HashMap<std::string, char> javaFieldShadow; // class-field names copied onto methods
};

inline FieldNarrowTables buildFieldNarrowTables( const IngestResult& ing )
Expand All @@ -1119,6 +1121,7 @@ inline FieldNarrowTables buildFieldNarrowTables( const IngestResult& ing )
}
}
t.localNameSet.reserve( ing.bindings.size() );
t.localShadowSpans.reserve( ing.bindings.size() );
for( const Binding& b : ing.bindings )
{
if( b.fromSymbol == kNoNode || b.var.empty() )
Expand All @@ -1130,10 +1133,143 @@ inline FieldNarrowTables buildFieldNarrowTables( const IngestResult& ing )
key.push_back( '#' );
key.append( b.var );
t.localNameSet.try_emplace( key, 1 );
if( b.kind == LocalBindKind::VarDecl )
{
t.localShadowSpans[ key ].push_back( VarSpan{ b.spanStart, b.spanEnd } );
}
}
// Java fields attribute to the class symbol; method-reference sites attribute to the
// method. Copy class-scope names onto every contained method so a field named like a
// type vetoes Identifier::method the same way a parameter or local does — for the
// whole method, which is Java field lookup. Locals/parameters are NOT copied here;
// JavaTypeCandidate consults localShadowSpans at the call-site byte instead.
for( const Binding& b : ing.bindings )
{
if( b.fromSymbol == kNoNode || b.fromSymbol >= ing.symbols.size() || b.var.empty() )
{
continue;
}
const Symbol& owner = ing.symbols[ b.fromSymbol ];
if( owner.lang != Lang::Java
|| ( owner.kind != SymKind::Class && owner.kind != SymKind::Interface
&& owner.kind != SymKind::Struct ) )
{
continue;
}
key.clear();
Narrower::appendUint( key, owner.id );
key.push_back( '#' );
key.append( b.var );
t.javaFieldShadow.try_emplace( key, 1 );
for( const Symbol& s : ing.symbols )
{
if( s.fileId != owner.fileId
|| ( s.kind != SymKind::Method && s.kind != SymKind::Function )
|| s.sigStartByte < owner.sigStartByte
|| s.endByte > owner.endByte
|| s.id == owner.id )
{
continue;
}
key.clear();
Narrower::appendUint( key, s.id );
key.push_back( '#' );
key.append( b.var );
t.localNameSet.try_emplace( key, 1 );
t.javaFieldShadow.try_emplace( key, 1 );
}
}
return t;
}

inline bool javaClassNamed( const HashMap<std::string, char>& classNames, std::string_view name )
{
return !name.empty() && classNames.find( std::string( name ) ) != classNames.end();
}

inline bool javaLeadingShadowed( const FieldNarrowTables& t, NodeId from, std::string_view name,
std::uint32_t startByte, std::string& key )
{
if( name.empty() || from == kNoNode )
{
return false;
}
key.clear();
Narrower::appendUint( key, from );
key.push_back( '#' );
key.append( name );
if( t.javaFieldShadow.find( key ) != t.javaFieldShadow.end() )
{
return true;
}
const auto it = t.localShadowSpans.find( key );
if( it == t.localShadowSpans.end() )
{
return false;
}
for( const VarSpan& v : it->second )
{
if( startByte >= v.startByte && startByte < v.endByte )
{
return true;
}
}
return false;
}

// Prove a Java method-reference receiver is a type, not a value.
// Widget — last (only) segment is an indexed class, leading name not shadowed
// Outer.Inner — leading is a class: every segment is a class; only leading is shadowed
// com.example.Widget — leading is not a class and not shadowed; last segment is a class
// Expression receivers (this/super/calls) never reach here with a dotted type spelling;
// System.out fails because leading is a class and `out` is not.
inline bool javaTypeReceiverProven( const Reference& r, const HashMap<std::string, char>& classNames,
const FieldNarrowTables& fieldNarrow, std::string& qkey )
{
if( r.recvVar.empty() || r.fromSymbol == kNoNode )
{
return false;
}
const std::string_view recv = r.recvVar;
const std::size_t firstDot = recv.find( '.' );
const std::string_view leading = firstDot == std::string_view::npos ? recv : recv.substr( 0, firstDot );
const std::size_t lastDot = recv.rfind( '.' );
const std::string_view last = lastDot == std::string_view::npos ? recv : recv.substr( lastDot + 1 );
if( leading.empty() || last.empty() || !javaClassNamed( classNames, last ) )
{
return false;
}
if( javaLeadingShadowed( fieldNarrow, r.fromSymbol, leading, r.startByte, qkey ) )
{
return false;
}
if( firstDot == std::string_view::npos )
{
return true;
}
if( !javaClassNamed( classNames, leading ) )
{
return true; // package-qualified type: last is a class, leading is not a value
}
std::size_t begin = 0;
while( begin < recv.size() )
{
const std::size_t end = recv.find( '.', begin );
const std::string_view segment( recv.data() + begin,
( end == std::string_view::npos ? recv.size() : end ) - begin );
if( !javaClassNamed( classNames, segment ) )
{
return false;
}
if( end == std::string_view::npos )
{
break;
}
begin = end + 1;
}
return true;
}

// ── Phase 5 external-name veto tables (docs/EVALS.md "Phase 5", mechanism 1; src/externalnames.h) ─────
// Three small evidence tables the veto in buildGraph's resolve loop consults AFTER every receiver rule has
// missed. Built once per graph, deterministic (pure functions of ing.files / ing.symbols / ing.bindings in
Expand Down Expand Up @@ -2171,6 +2307,21 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null
}
}

// Java issue #74: the grammar labels both `Widget::makeFn` and `widget::makeFn`
// with an identifier receiver. A method-reference capture is therefore admitted only when
// repository evidence proves the receiver is a type at this site. Every other receiver is
// a known callback expression: stop before the bare-name ladder, which would otherwise
// manufacture an ordinary call edge by member spelling.
if( !scipPinned && r.recv == RecvKind::JavaTypeCandidate )
{
if( !javaTypeReceiverProven( r, classNames, fieldNarrow, qkey ) )
{
++g.unresolvedOut[r.fromSymbol];
disposition = CallDisposition::Unresolved;
continue;
}
}

// ---- A4-R5 FFI binding fallback: compute cross-language alias candidates UP FRONT. Applied below ONLY
// if the normal ladder finds no compatible local def (so a same-language local def always wins). Two
// sound gates keep it silent on binding-free corpora: pybind fires only for a foreign-language caller of
Expand Down Expand Up @@ -4763,6 +4914,10 @@ inline FieldUseAnswer collectFieldUseSites( const IngestResult& ing, FieldId fie
{
case RecvKind::ElixirModule:
case RecvKind::ElixirSelfModule:
case RecvKind::JavaTypeCandidate:
{
// Call-only ingest stamp; read/write field collection can never own this site.
}
break; // module receivers name callables, not instance fields
case RecvKind::None:
{
Expand Down
Loading