Skip to content
Merged
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
28 changes: 16 additions & 12 deletions .ripwire_quality_acks

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,46 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli

## [0.6.2] — 2026-09-21

### Added — Ruby's class-level attribute DSL (`attr_*`) defines Var symbols

`attr_reader`/`attr_writer`/`attr_accessor` are Ruby's canonical class DSL, and `attribute`/`attributes` are
their ActiveModel counterparts: each macro generates the accessor methods named by its arguments when the
class is defined — `attr_reader` spells only the reader, `attr_writer` only the writer, and `attr_accessor`/
`attribute` spell both. The family is EXACTLY these five verbs; ActiveSupport's neighbouring accessor macros
(`cattr_accessor`, `mattr_accessor`, `thread_mattr_accessor`, `class_attribute`, `attr_internal`) are
deliberately not in it — the scope is a decision, not an omission.
Those generated names were indexed by none of them — a write against one resolved to nothing. The class DSL
now mints real symbols: one `Var` def per `simple_symbol` argument (named as the argument, minus the leading
`:`), plus the `<x>=` setter for the writer-side macros (`attr_writer`/`attr_accessor`/`attribute`; the plural
`attributes` is READERS-ONLY — its third-party owners, AMS/jsonapi-serializer/dry-struct, define no setters,
measured — so no phantom setter weight) — the exact spelling the setter-call rename already produces, so
`record.x = v` BINDS to a def instead of dropping. The singular `attribute` takes one name; a trailing type
or `default:` argument is metadata, not a def. An `attributes` do-block body is walked but defines nothing.
An INLINE-VISIBILITY wrapper is also class-DSL position: `private attr_reader :x` (Ruby 3, RuboCop's
Style/AccessModifierDeclarations: inline) evaluates its argument first — the macro runs and the method IS
defined — then applies visibility, so the capture unwraps one receiverless `private`/`protected`/`public`/
`module_function` call when the family call is its sole argument. This REVERSES a stated floor:
queries/ruby/tags.scm used to say "attr_accessor/attr_writer/attr_reader define nothing in the source TEXT
… a write against one is an honest nothing". That posture predated measurement; tested to be working in a
real, running Rails application (test/rubyattrsfix/USECASES.md), these macros define methods that every
`record.price` reads — the silence was a coverage hole, not honesty. The reversal is GLOBAL: every indexed
Ruby corpus gains Var defs, attribute names leave the external surface, setter writes bind, and — because
Call refs are language-gated, not kind-gated — attr names receive real call edges and PageRank weight
(accepted churn, disclosed here). The DSL CALL itself stays a reference capture: `attr_accessor` and friends
remain external-surface names, the same posture as the schema DSL rows. One id caveat: a same-class
`def x` + `attr_accessor :x` produces TWO defs sharing one `p::sc::n` id (the dedup ladder folds only
same-byte captures; the pair is two identities) — the id is not unique in that case. kParserVer 119 → 120
(extraction facts changed, the bump past everything main carries; no record layout change — kCacheVersion
stays 24, kQSnapCacheScheme stays 14).
Gate: `test/rubyattrscheck.sh` on `test/rubyattrsfix/` (fixture proven against a running Rails application;
red on the pre-change binary — the before-state `defs=0 external=1` attribution rows are the before-evidence).
Disclosed capture floors, pinned by the gate's `floor_attr.rb` arms: a `begin`- or modifier-`if`-guarded macro
call is not unwrapped to class-DSL position; the do-block BODY of an `included`/`class_methods`
(ActiveSupport::Concern), of `Struct.new`/`Class.new`/`Module.new`, and a non-modifier `if … then … end`
block are not unwrapped either (each defines real methods at runtime); and only `simple_symbol` arguments
define — a quoted (`:"x"`/`:'x'`), string, or splat/`%i[]` argument stays an honest nothing (Ruby defines
those methods; ripwire does not capture them). Every floor is stated and pinned, never silent.

### Added — Microsoft's `cl.exe` builds the tree, so both Windows front ends compile and both gate

The native Windows port (#44) built with clang-cl only; `cl.exe` stopped at the GCC/Clang language extensions
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1867,9 +1867,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published
### In the tests

<details>
<summary><b>647 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>648 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 **647 gate scripts** and is the authoritative list; <!-- gatecount -->
`test/regression.sh` names **648 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 Expand Up @@ -2219,7 +2219,7 @@ same renderer. One computation has one output shape.

| Item | Requirement |
| --- | --- |
| Operating system | macOS (arm64 or x86-64) or Linux (arm64 or x86-64). Native Windows x64 **builds** with both clang-cl and MSVC `cl.exe` — CI builds both on `windows-latest` every full matrix and smoke-tests each binary (`--version`, `ctest`, a real crawl, the two-run byte-identical contract, well-formed XML); the 647-gate suite does not run there, and ASan is compiled but never executed, so treat it as a build, not a validated platform. No prebuilt Windows binary is published; WSL2 remains the supported way to RUN it on a Windows machine. |
| Operating system | macOS (arm64 or x86-64) or Linux (arm64 or x86-64). Native Windows x64 **builds** with both clang-cl and MSVC `cl.exe` — CI builds both on `windows-latest` every full matrix and smoke-tests each binary (`--version`, `ctest`, a real crawl, the two-run byte-identical contract, well-formed XML); the 648-gate suite does not run there, and ASan is compiled but never executed, so treat it as a build, not a validated platform. No prebuilt Windows binary is published; WSL2 remains the supported way to RUN it on a Windows machine. |
| Prebuilt Linux floor | RHEL 8 or later (glibc 2.28) |
| Prebuilt macOS floor | macOS 14 or later, Apple silicon. 0.6.1 is the last release with an Intel macOS binary; on an Intel Mac, pin `RIPWIRE_VERSION=v0.6.1` or build from source. |
| x86-64 floor | x86-64-v3 (Intel Haswell, 2013, or later), for a prebuilt binary and a source build alike |
Expand Down
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` | 647 gate scripts plus the determinism, cache-transparency and golden contracts. <!-- gatecount --> |
| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 648 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 @@ -5837,7 +5837,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 **647 gate scripts**, all of which exist on disk. <!-- gatecount -->
naming **648 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 @@ -6849,7 +6849,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 647. The <!-- gatecount -->
~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 648. 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
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 = [
["647 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
["648 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, "647", "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, "648", "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 }){
["183 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"],
["647 gate scripts", "bash test/manifestcheck.sh"], // gatecount
["648 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
21 changes: 18 additions & 3 deletions queries/ruby/tags.scm
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,23 @@
; Stated floors, all pinned by test/rubysettercheck.sh: an operator_assignment (`obj.count += 1`,
; `obj.count ||= 1`) reads AND writes and one capture carries one name, so it keeps the getter edge
; only; a left_assignment_list (`a.x, b.y = 1, 2`) wraps its targets one level below `left:` and is
; not read either; and attr_accessor/attr_writer/attr_reader define nothing in the source TEXT, so
; their generated methods are not symbols and a write against one is an honest nothing, never a
; wrong edge.
; not read either. This rule still captures only the CALL shape — but note the parser-version-120
; REVERSAL: the class-level attribute DSL (attr_reader/attr_writer/attr_accessor/attribute/attributes
; — EXACTLY these five; ActiveSupport's cattr_accessor/mattr_accessor/thread_mattr_accessor/
; class_attribute/attr_internal are deliberately not in the family) is no longer "an honest nothing".
; A class-level DSL call — receiver-less, so `obj.attr_reader :x` is still somebody's own method —
; mints Var defs (one per simple_symbol argument, plus the `<x>=` setter for the writer-side macros
; attr_writer/attr_accessor/attribute; attr_reader and the plural `attributes` — third-party DSLs
; measured READERS-ONLY, e.g. AMS/jsonapi-serializer/dry-struct — spell no setter) via the C++ side-
; capture in ingest_names.h, so a write against a defined attribute binds to the `<name>=` def instead
; of dropping. An INLINE-VISIBILITY wrapper (`private attr_reader :x`, Ruby 3 / RuboCop inline) is
; class-DSL position too: the macro evaluates first and the method IS defined, then visibility applies.
; The DSL call itself stays a reference like this one; test/rubyattrscheck.sh pins both sides.
; Disclosed floors of the DSL capture, each pinned by an arm: a `begin`- or modifier-`if`-guarded call
; and the do-block BODY of an `included`/`class_methods` (ActiveSupport::Concern), of
; `Struct.new`/`Class.new`/`Module.new`, or a non-modifier `if … then … end` block are NOT unwrapped to
; class position; and only `simple_symbol` arguments define — a quoted (`:"x"`/`:'x'`), string, or
; splat/`%i[]` argument stays an honest nothing. All of these define real methods at runtime; the
; silence is stated, never silent.
(call
method: (identifier) @name) @reference.call
12 changes: 11 additions & 1 deletion src/ingest_cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ constexpr std::uint32_t kCacheVersion = 24; // 24: RawRef gains `viaAr
// (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 = 119; // bump on any grammar/.scm/extraction change
constexpr std::uint32_t kParserVer = 120; // bump on any grammar/.scm/extraction change
// 119 = 2026-09-20 (T13/fix3): queries/java/tags.scm
// and queries/kotlin/tags.scm's import captures were
// @reference.call — an import is a dependency edge,
Expand All @@ -265,6 +265,16 @@ constexpr std::uint32_t kParserVer = 119; // bump on any grammar/.sc
// graph edge (graph.h isResolvableCallReference is
// Call+Macro only). A cache written before this
// double-counts every JVM import as a caller.
// 120 = 2026-09-21 (Ruby class-level attribute DSL,
// test/rubyattrscheck.sh — the attr_* floor reversal): a
// class-body-level receiver-less attr_reader/attr_writer/
// attr_accessor/attribute/attributes call defines one Var
// per simple_symbol argument (plus the `<x>=` setter for
// writer-side macros), so setter CALLS (`record.x = v`)
// now bind; the extracted def SET grows on every Ruby
// corpus. Landed at 115 on the pre-train-6 base; rebased
// in Sept 2026 it bumps past main's 119. No record layout
// change (kCacheVersion stays 24); kQSnapCacheScheme 14.
// 118 = 2026-09-19 (CodeRabbit follow-up, thread
// 4053600599: isJsxIntrinsicTagIdentifier
// (src/ingest_names.h) tested `!isUppercase`, which kept
Expand Down
7 changes: 1 addition & 6 deletions src/ingest_elixir.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,7 @@ std::string_view elixirTarget( TSNode node, std::string_view src ) noexcept
{
return {};
}
const TSNode target = fieldChild( node, NodeField::Target );
if( ts_node_is_null( target ) || std::strcmp( ts_node_type( target ), "identifier" ) != 0 )
{
return {};
}
return nodeTextOf( target, src );
return fieldIdentifierText( node, NodeField::Target, src );
}

/// Return whether target introduces a function, macro, guard, or delegate definition.
Expand Down
Loading
Loading