diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index fff1468da..2943e3ec3 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -1567,10 +1567,7 @@ ack verbosity 6302e2e27e23bcde 64 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the li ack verbosity 639de1c3670999f9 185 cid=ffcb81de788c329a C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: wave-3 close, H7 hosts: runCrossRef hosts the --plan and --stray-content refusal sites the fix routes through the shared sentence (verify-wave2 lanes edited it days earlier — the churn is the fix's, self) ack verbosity 685ade09a168535e 96 cid=124bb29c88050ae1 Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. | prior: T1 completeness claims (complete= on grep/whereis): the +1 on streamBlobs is the deliberate DEFAULTED StreamBlobStats* param (null-object sink inside, no per-site null test; every existing caller byte-identical) so whereis can prove its scan exhaustive before claiming; cx/LOC on streamBlobs/computeWhereis/writeWhereisPage/emitGrepReport is the claim computation plus its in-band legend (the honesty text IS the feature); churn=self on those plus grepCollect/dispatchMcpLine is this lane own edit window. Gated red-first by test/completecheck.sh (24 arms, 10 red pre-fix; mutation arms force cap/offset/budget/unreadable-file/regex-mode/oversized-blob and assert the attribute VANISHES); full plain suite green, 21 touched-family gates green under ASan+LSan, determinism x3, xmllint clean ack verbosity 6979fa794f2a5154 423 cid=9132efbcc6dfcbb7 now=423 was=420 p=src/ingest_sidecap.h:1706 fn-literal bodies: captureTagsFacts gains the defBodyNodeOf call that hands a literal-bound name its literal (the @name and Lang ride along) and reads params/cx from it: a +3 cx / +3 verbosity call site at the def-extraction seam | prior: ruby-attr class-DSL lane (review round; kParserVer 119->120): duplication/new-clone elixirArguments|elixirTarget is token-shape coincidence only - elixirTarget is the shared fieldIdentifierText primitive plus a null guard, elixirArguments reads a different field with no identifier gate; no shared logic to lift. captureTagsFacts +1cx/+4verb is the two-line gated call site of the inline-visibility lift. -ack verbosity 6a38d70787d09e64 74 cid=f6a0c09dfc153ece Kotlin port (PR #126): the leading whitespace sample is walked by codepoint (jsonesc::utf8SeqLen) so invalid UTF-8 folds into errNodes/errBytes, deduplicated against the ERROR spans tree-sitter recovery already counted; without it a garbage byte run swallowed by recovery reads as a healthy file in --skipped. -ack verbosity 6acbcaa854eada23 289 cid=e5c009ae9a0d8393 preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. ack verbosity 7004309695fb79f1 265 2026-08-15 harvest wave-level pass (orchestrator): 12-lane wave measured as one delta vs origin/main 4b9386c per verifier finding 6. All 21 gating rows triaged individually: emitGrepReport/grepHitsJson/runCallHierarchy/runDefaultMap/collectSources/printUsage/Config/runMcpHttp = feature absorption by design (grouping+boolean+corpus disclosure, file-root, bodyless_defs+legend, estimator guard, new flags), each converged and gate-verified at lane level; short-horizon-churn rows = single-wave multi-lane edits of shared hubs, process artifact; sym=main rows are main.cpp::main growth mislabeled to analyze.py by the bare-name canonId collision (path-qualified keying fix d593de3 still unpushed). emitGrepReport cx 25->63 flagged as W2 split candidate in PLAN round record. -ack verbosity 7051b3950aaf4c14 82 cid=7919a3d486bba416 lane B1 cap disclosure: doc_mentions_capped= is contracted to be PROVABLE, never guessed, and that proof is the +6/+16. It is the in-loop test that a doc below its anchor's lift target was the one a cap turned away, plus docLiftWasRefused for the other half of the total cap, which ends the OUTER loop over anchors already consulted. A one-line flag would instead say 'there might be more', which is the fabricated fact in the other direction. ack verbosity 719e8441be665024 959 cid=e508c8076e15f8e3 lane win-build-green (review F1/F2/F4): ci.yml's jobs key grows because the windows job stopped asserting exit codes and started asserting answers — a --version SHAPE check (semver + emit= + built_from=), fixture-map floors that separate a real crawl from the 951-byte empty one a Windows path bug produces, ctest --no-tests=error, and the cl leg's own expectation (configure must succeed, build must fail, no D8xxx flag regression hiding behind the seam, every erroring file ours, first error one of the extension codes) built with -k 0 so its log is the inventory. Every added line is an assertion or the reason for one; shrinking this key means deleting the checks the review asked for. Same shape as the release.yml jobs acks above. ack verbosity 73ac37ff2580b0b0 86 cid=b07db8431c1b7c18 R1 identity round (harvest Wave-2 R1, 2026-08-24). Five gating rows, ONE change: finding identity now survives a rename or a move. (1) runQualityDelta complexity 201->205 and verbosity 419->462: the +43 lines are overwhelmingly the LEGEND this repo requires — the identity mechanism is a claim about what an ack means, so the report has to say what it rested on (both mechanisms, both truncation caps, the two per-route counters), and legendcoveragecheck arm (A) requires every new attribute be named there. The code added to this dispatcher is three lines: the ledger is now taken from the already-healed basis instead of re-read (re-reading it here would undo the healing), one countAckRescues call, and one call to quality::identityDisclosure — which is itself an EXTRACTION made in this lane precisely because --quality-delta flagged the inline version. The residual +4 complexity is the cid ternary at the ack-write site. Decomposing runQualityDelta is its own round, recorded as such by the W2-E ack above and unchanged by this one. (2) resolveDeltaBasis verbosity 73->86 + churn=self: the two healIdentity calls (ref-pair and working-tree arms) and the comment explaining why the healing must precede computeDelta — heal the baseline after the delta is taken and the delta has already been computed against the stale identity. churn=self on it and on mergescout::injectFileLevelFallback is this one edit window, not a history signal. (3) injectFileLevelFallback: its hand-rolled fopen/fseek/fread/fclose block was retired onto docparse::detail::readWholeFile, the canonical whole-file read, after this lane extracted quality.h's copy and the reuse-decline kind reported the pair as a 320-token clone. Re-read before the swap: short read or ftell failure clears the buffer and the file contributes nothing, byte-identical to what it replaced. Gate: test/identitycheck.sh, 19 arms, 9 RED on the pre-lane binary. ack verbosity 763ff87870373241 84 cid=0f3e66f03f7be1f1 round ec5e3c3..HEAD, the MCP door across L6 (batch two-grammar), L8 P9 (post_check on the edit verbs), V2 F3/F7/F8/F9 (receipt keys, batch attribution, top-level array reader, legend present-but-empty) and L7 P1 (legend on 16 verbs): dispatchMcpLine/runBatchSub/runCliEdit are the one dispatch each of those lanes had to pass through | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d22e195d..c8aa431cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,56 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Added — a Ruby `db/schema.rb` renders its columns as Section definitions +A Rails schema is recognized BY CONTENT, never by path: a Ruby file whose tree holds a +`create_table "x", … do |t| … end` call — the call's do-block carries one block parameter — is a +rendered schema, wherever it lives. Each `t. "name"` / `t. :name` in a table block mints +ONE `SymKind::Section` def at `Lang::Ruby` — the data-kind slot a doc heading or YAML key already +occupies (model.h: a Section's isolation is a language property; the Ruby ones are still no call +edges) — span = the name token, both quote spellings and the `:symbol` form. Rails-generated attribute +uses (`product.price`) then resolve to the column as a definition: it appears in `--uses`, `--grep`, +`--whereis` and the map. THE COLUMNS ARE DEFINITIONS ONLY, by maintainer decision: buildGraph's +byName skips Section-Ruby, so a column admits NO call edge and no PageRank weight — an untyped +`response.code` must not become a table's caller, and 200 `name` columns must not damp a model's real +`def name` (both measured at review; edges may return behind an evidence rule, e.g. a receiver proven +to be the owning model). The id rule covers the grounded spellings: no id column/spelling → an +implicit `id` def (the anchor is the table-name string — no `id` token exists there); `id: false` → +no id def; `id: :uuid` → an `id` def at the pair key (the uuid is a TYPE of the id column, never a +column of its own; the type is not modelled); `primary_key: "x"` → a def named the string instead. +An ARRAY or symbol `primary_key` (a composite key, or a hand-written migration) mints NO implicit id — +the rendered composite columns are the block's own explicit `t.` lines. `t.timestamps` mints +`created_at` AND `updated_at` (the DSL names them literally, symmetric with the id rule) — the two +defs anchor at the method token's edges. The DSL CALLS keep their reference posture exactly like the +attr family: `string`/`datetime`/`create_table` stay external-surface names. Duplicate columns across +tables stay SEPARATE defs (same name, distinct identity bytes): `id` on N tables is `defs="N"`, and +the map row merges them with `overloads`, never hiding the multiplicity. MIGRATIONS never interfere: a +class-wrapped create_table — the migration shape, string- or symbol-named — is refused by a +class/module-nest gate (a rendered schema's tables live at file level or under `Schema[].define`, never +inside a class body), so the rendered `db/schema.rb` stays the ONE source of column names and indexing +the migration beside its schema cannot double any def; `add_column` / `remove_column` / +`change_column` are argument data and read nowhere — a column added then dropped never registers +(pinned by the three migration fixtures). Rails 8's `db/queue_schema.rb`, `db/cache_schema.rb` and +`db/cable_schema.rb` are matched by the same content gate. kParserVer lands at 144 — carried as 126 +(extraction facts changed; carried 122 pre-rebase and renumbered for the train-20 `.astro` collision, +then to 126 at review; the 0e5fdcd2 merge kept 126 per the maintainer, but trains 22-25 landed +128-143, so this merge renumbers to 144; no record layout change — kCacheVersion stays 28 and +kQSnapCacheScheme stays 17). +Gate: `test/rubyschemacheck.sh` on `test/rubyschemafix/` — schema text from a real, running Rails +8.1 app's `bin/rails db:schema:dump` (verified `db:schema:load`-able), original domain tables scrubbed +to `spike_*` names; two spellings no dump emits are restored by hand and marked in-file (`id: :uuid`, +which that dump actually failed on, and a literal `t.timestamps`). Stated capture floors, each pinned +by a gate arm: `t.index` / `t.references` / `t.belongs_to` / `t.polymorphic` / `t.check_constraint` / +`t.exclusion_constraint` / `t.unique_constraint` name no column (a `t.references`/`t.belongs_to` mints +no `_id`, and a `create_join_table` mints nothing — raws dumps render these as explicit columns); +a column call must be ON the block parameter (`helper.string "x"` names nothing); a symbol-named table +(`create_table :users`) is a migration spelling, not a schema surface; `where("price > ?")` string +fragments stay opaque; the picker splits honestly (`rec.name` on a plain local and a rich `X.new.name` +receiver are `graph_ambiguous`, `self.name` inside the defining class pins its own def). The +before-evidence: on the pre-change binary every column was `defs=0 external=1` — the fixture's +`--uses=name` read `defs="10"` (9 attr/def defs plus the YAML key) with the schema columns invisible; it +now reads `defs="19" external="0"` and the column uses resolve to definitions only. + + ### Changed — a Ruby call to self, or on an instance the code builds, answers from its own side: an instance never reaches `def self.m` The entry below left one floor open, (i). A call to self, or on a receiver the code builds, read a class's defs from both diff --git a/README.md b/README.md index f7523c566..427fdb33a 100644 --- a/README.md +++ b/README.md @@ -2082,9 +2082,14 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-671 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +672 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 **671 gate scripts** and is the authoritative list; +`test/regression.sh` names **672 gate scripts** and is the authoritative list; + + +672 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 **672 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 3326be7ac..99e0c75d5 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,13 @@ 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` | 671 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 672 gate scripts plus the determinism, cache-transparency and golden contracts. | + + +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 672 gate scripts plus the determinism, cache-transparency and golden contracts. | + + +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 672 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) @@ -5866,7 +5872,9 @@ 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 **671 gate scripts**, all of which exist on disk. + + +naming **672 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`. @@ -6969,9 +6977,12 @@ 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 671. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 672. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 672. The + + + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 672. 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` still asserts this very number against the loop's actual length as the post-hoc catch. - **"282 argv vectors."** The gate asserts a floor of ≥250 assembled from five sources; 282 was a @@ -14470,3 +14481,4 @@ legends and `--help=all` (DEV-5); this registration's text above names no file o any A binary ran, because `--anchor` refuses without it (DEV-12); Gate S masks the MCP `_index` stamp (it folds file mtimes, so BASE differs from itself there) and the initialize reply's `dictv=` (DEV-9); I2 is read at the emitted 4-dp precision (DEV-10); B does not re-pick `--tree`, whose own offset paging a window swap would break (DEV-11). + diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index a1e01bf65..a9d4f31df 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1214,7 +1214,13 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["671 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 + ["672 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 + + + ["672 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 + + + ["672 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."], @@ -1238,7 +1244,9 @@ 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, "671", "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, "672", "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", @@ -1540,9 +1548,12 @@ function storyCards(s, { kick, head, stories, footText }){ ["185 long flags · 36 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["671 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["672 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["672 gate scripts", "bash test/manifestcheck.sh"], // gatecount + + + ["672 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/"], ["the oracle round", "bench/headtohead/r9-2026-08-09/RESULTS.md"], ]; @@ -1595,3 +1606,4 @@ function storyCards(s, { kick, head, stories, footText }){ p.writeFile({ fileName: require("path").join(__dirname, "ripwire-showcase.pptx") }) .then(() => console.log("WROTE ripwire-showcase.pptx")); + diff --git a/queries/ruby/tags.scm b/queries/ruby/tags.scm index d83d44ba3..0bc65b528 100644 --- a/queries/ruby/tags.scm +++ b/queries/ruby/tags.scm @@ -79,3 +79,20 @@ ; raises in a class and in a module) and defines nothing. (call method: (identifier) @name) @reference.call +; The Rails schema capture (test/rubyschemacheck.sh): a file whose tree holds a +; `create_table "x", … do |t| … end` call is a rendered db/schema.rb (content-gated — path plays no +; part), and each `t. "name"` / `t. :name` in the table block mints ONE data-kind def +; (SymKind::Section at Lang::Ruby, see model.h) via the C++ side-capture in ingest_names.h, span = the +; name token — Rails-generated attribute uses (`product.price`) resolve to the column as a def. +; DEFINITIONS ONLY by maintainer decision: the defs answer --uses/--grep/--whereis, but buildGraph's +; byName skips Section-Ruby, so columns admit NO call edges and no PageRank weight (untyped-receiver +; binding and map takeover were measured; edges may return behind an evidence rule). The four id +; spellings and `t.timestamps` also name columns (implicit `id` / `id: false` / `id: :uuid` / +; `primary_key: "x"` / created_at+updated_at); an ARRAY or symbol primary_key (composite key) mints no +; implicit id. The DSL CALLS stay references like this one (`string`, `datetime`, `create_table` remain +; external). Stated floors: `t.index`/`references`/`belongs_to`/`polymorphic`/`check_constraint`/ +; `exclusion_constraint`/`unique_constraint` name no column; duplicate columns across tables stay +; SEPARATE defs (`id` on N tables -> defs="N"); `where("price > ?")` fragments stay opaque. A MIGRATION +; never contributes: a class-wrapped create_table (string- or symbol-named) is refused by the +; class/module-nest gate — the schema is the ONE source of column names; add_column/remove_column/ +; change_column are argument data and read nowhere (a column added then dropped never registers). diff --git a/src/graph.h b/src/graph.h index 78e9c778d..b694466eb 100644 --- a/src/graph.h +++ b/src/graph.h @@ -5379,6 +5379,15 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null PROFILE_SCOPE_DESCRIBE( "buildGraph/1d: byName (name -> def ids)" ); for( const Symbol& s : ing.symbols ) { + if( s.kind == SymKind::Section && s.lang == Lang::Ruby ) + { + continue; // definitions-only (#339): a Ruby schema column answers --uses/--grep/--whereis as + // a def, but admits NO call edge — the byName table feeds both call binding and the + // common-name damping, so an untyped `response.code` must not become a column's + // caller, and 200 `name` columns must not damp a model's real `def name`. + // Mirrors contextratio.h's isMeasurableKind treatment of Sections. Edges may come + // later behind an evidence rule (receiver proven to be the owning model). + } byName[ s.name ].push_back( s.id ); } } diff --git a/src/ingest_cache.h b/src/ingest_cache.h index 521bceaad..345be4682 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -302,7 +302,41 @@ constexpr std::uint32_t kCacheVersion = 28; // 28: FE-A (test/falseedg // (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 = 148; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 149; // bump on any grammar/.scm/extraction change + // 149 = 2026-10-08 (merge #3: trains 22-26a landed 128-148, the carried 144 becomes 149; +// the Rails schema-column capture + // (test/rubyschemacheck.sh, unreleased; reviewed round at + // 123/126 — carried 122 pre-rebase, renumbered once for the + // train-20 .astro collision and again to 126 at the + // maintainer's request: 123 reserved for #325, 125 = #338 + // (train 22); 126/127 stayed for community PRs, but trains + // 22-25 landed 128-143, so the 0e5fdcd2 merge kept 126 and + // THIS merge renumbers 144): a Ruby file whose tree + // holds a `create_table "x", … do |t| … end` call is a + // rendered db/schema.rb BY CONTENT, and every + // `t. "name"` / `t. :name` in the table block + // mints a SymKind::Section def (the data-kind slot, same + // as a doc heading / YAML key) at Lang::Ruby — DEFINITIONS + // ONLY: the defs answer --uses/--grep/--whereis, but + // buildGraph's byName skips Section-Ruby, so columns + // admit NO call edges and no PageRank weight (the + // "admission consequence" planned earlier was retracted + // after the maintainer measured unrelated-call binding + // and map takeover; edges may return behind an evidence + // rule). Also: the four-spelling id rule (implicit / + // id: false / id: :uuid / primary_key: "x"; an array or + // symbol primary_key suppresses the implicit id — a + // composite key has no id), `t.timestamps` -> created_at + // + updated_at, constraint DSL floors (check/ + // exclusion/unique_constraint name no column), the + // review-round receiver floor (a column call must be on + // the block parameter), and MIGRATIONS contribute + // nothing (class/module-nest gate). No record layout + // change: kCacheVersion stays, kQSnapCacheScheme stays. + // A 122/123/124/125 cache differs only in the schema + // defs and Section-Ruby edges it lacks. + + // 148 = 2026-10-04 (train 26a: PR #373, Ruby method lookup, merged onto main's 143): // cache-key hygiene. The branch's Ruby extraction steps ran as 130–137, // 142 and 145 (its notes below); main used 130–143 for other lanes, and diff --git a/src/ingest_names.h b/src/ingest_names.h index 0f3d19b3d..d466f38ca 100644 --- a/src/ingest_names.h +++ b/src/ingest_names.h @@ -1163,9 +1163,18 @@ inline void captureRubyAttrDefsCall( TSNode n, std::uint32_t fileId, std::string } ); } -inline void captureRubyAttrDefs( TSNode root, std::uint32_t fileId, std::string_view src, std::vector& defs ) -{ - if( src.find( "attr" ) == std::string_view::npos ) // file signal: every family name contains "attr" +// ONE pre-order walk of the whole tree dispatching BOTH Ruby def-side lanes — the class-level +// attribute DSL and the rendered-schema column capture — from the same explicit stack (the duplication +// the maintainer's quality-delta flagged: two byte-identical walkers). The per-file signals (contains +// "attr" / contains "create_table") decide which dispatch fires before the walk starts, exactly like +// each lane's former early-out. Everything else in each lane is unchanged. +// The schema table helper is defined with its lane below (this walker lives with the attr lane). +inline void rubySchemaTableCall( TSNode n, std::uint32_t fileId, std::string_view src, std::vector& defs ); +inline void captureRubyDefs( TSNode root, std::uint32_t fileId, std::string_view src, std::vector& defs ) +{ + const bool wantAttr = src.find( "attr" ) != std::string_view::npos; // every family name contains "attr" + const bool wantSchema = src.find( "create_table" ) != std::string_view::npos; // the schema gate name must appear + if( !wantAttr && !wantSchema ) { return; } @@ -1185,10 +1194,17 @@ inline void captureRubyAttrDefs( TSNode root, std::uint32_t fileId, std::string_ stack.pop_back(); if( kindIs( ts_node_type( n ), "call" ) ) { - const std::string_view fam = rubyNamedDirective( n, src, kRubyAttrFamilyNames ); - if( !fam.empty() && rubyAttrAtClassBodyLevel( n, src ) ) + if( wantAttr ) + { + const std::string_view fam = rubyNamedDirective( n, src, kRubyAttrFamilyNames ); + if( !fam.empty() && rubyAttrAtClassBodyLevel( n, src ) ) + { + captureRubyAttrDefsCall( n, fileId, src, fam, defs ); + } + } + if( wantSchema && fieldIdentifierText( n, NodeField::Method, src ) == "create_table" ) { - captureRubyAttrDefsCall( n, fileId, src, fam, defs ); + rubySchemaTableCall( n, fileId, src, defs ); } } collectChildren( n, cursor.cur, kids ); @@ -1199,6 +1215,315 @@ inline void captureRubyAttrDefs( TSNode root, std::uint32_t fileId, std::string_ } } + +// ─── the Rails schema capture (test/rubyschemacheck.sh): RUBY db/schema.rb COLUMNS ARE Section DEFS ─── +// A schema file is recognized BY CONTENT, never by path: any Ruby file whose tree holds a +// `create_table "x", … do |t| … end` call — the call's `block:` field is a do_block whose `parameters:` +// field is a block_parameters carrying ONE bare identifier — is a rendered-schema document (the block +// is the create_table call's OWN do_block, found by named-child kind because the shared NodeField set +// has no Block enumerator). Path plays no part: a rendered dump sits at file level or under +// `ActiveRecord::Schema[].define … do`. WHAT A WALK OF THE FILE IS NOT: a MIGRATION wraps its +// create_table inside a class/module body, and rubySchemaTableCall's Gate 0 refuses every +// class/module-nested table — so the container DOES matter exactly there (the migration shape +// contributes ZERO defs, pinned by the three migration fixtures). The file signal below is a +// contains-"create_table" reject like the attribute-family capture's contains-"attr" reject; the +// semantic gate is the AST shape. +// +// Each `t. "name"` / `t. :name` receiver-CALL in the table's do-block body mints ONE +// SymKind::Section def at Lang::Ruby — the same data-kind slot as a YAML key / doc heading (model.h; +// note the wording change there: a Section is no longer "isolated in the graph" once these are +// Lang::Ruby and admit call edges) — with span = the name token and name = the literal (both quote +// spellings via stringLiteralText, or a simple_symbol's text minus ':'). The DSL CALLS keep their +// external-surface reference posture exactly like the attr family: no def named `string`/`datetime` +// appears. `t.timestamps` (a receiver call with NO arguments field) mints `created_at` AND `updated_at` +// — the DSL names them literally, symmetric with the id rule; the two defs anchor at the method +// token's start/end bytes, since no literal name tokens exist in the source. +// +// Non-column DSL — t.index / t.references / t.belongs_to / t.polymorphic — names no column and defines +// nothing (stated floor: `t.references :owner` would otherwise mint a phantom "owner" column). A column +// call whose receiver is not a bare identifier (`registry.string`, a receiver-less `string`) is not a +// column; a string named by a leading-comment-skipped FIRST argument is (the attribute-DSL comment posture). +// +// The id rule — the four grounded spellings, all pinned by test/rubyschemacheck.sh: +// – no column named `id`, no `id:` pair, no primary_key pair → an implicit `id` def anchored at +// the TABLE-NAME string's content start (no `id` token exists; the anchor is per-table unique, so +// the multi-def floor — `id` on N tables → --uses=id defs="N" — holds naturally) +// – `id: false` (the pair VALUE is the anonymous `false` token) → no id def (a key-less table) +// – `id: :uuid` (pair value a simple_symbol) → `id` def AT THE PAIR KEY (the key's +// text minus its trailing ':'; the uuid TYPE is not modelled — stated floor) +// – `primary_key: "x"` (pair value a string) → a def named the string's content +// instead of the implicit `id` +// Duplicate column NAMES across tables stay SEPARATE defs (distinct nameByte identities — the dedup +// ladder only folds same-byte captures). +inline constexpr std::array kRubySchemaNonColumns = { "index", "references", "belongs_to", "polymorphic", "check_constraint", "exclusion_constraint", "unique_constraint" }; + +// The FIRST non-comment named child of an argument_list — the attribute-DSL comment posture ("attribute( # note +// then :x"): a comment is an n-ary extra, never an argument. Null when the list is all comments/empty. +inline TSNode rubyFirstNonCommentArg( TSNode args ) noexcept +{ + ChildCursor cursor( args ); + TSNode first{}; + bool got = false; + forEachNamedChild( args, cursor.cur, [ & ]( TSNode c ) + { + if( kindIs( ts_node_type( c ), "comment" ) ) + { + return true; // skip the comment train, keep looking + } + first = c; + got = true; + return false; // stop at the first real argument + } ); + return got ? first : TSNode{}; +} + +// A pair node's KEY text: a hash_key_symbol carries its trailing ':' (the whole `force:` token) — strip +// it so the id/primary_key comparisons see the bare word. A key without the colon (already bare) passes +// through unchanged. +inline std::string_view rubyPairKeyText( TSNode pair, std::string_view src ) noexcept +{ + const TSNode key = fieldChild( pair, NodeField::Key ); + std::string_view t = nodeTextOf( key, src ); + if( !t.empty() && t.back() == ':' ) + { + t.remove_suffix( 1 ); + } + return t; +} + +// One `t. ` receiver-call inside a table block: a Section def per the rules above. The +// RECEIVER must BE the block parameter the create_table call bound (`t` in a rendered schema's do |t|); +// any other bare identifier (`helper.string "x"`) is somebody's own helper call and names no column. +// The `sawId` out-param records an explicit `id` column so the implicit rule can see it. +inline void rubySchemaColumnCall( TSNode c, std::uint32_t fileId, std::string_view src, std::string_view receiverName, std::vector& defs, bool& sawId ) +{ + if( !kindIs( ts_node_type( c ), "call" ) ) + { + return; + } + const TSNode recv = fieldChild( c, NodeField::Receiver ); + if( ts_node_is_null( recv ) || !kindIs( ts_node_type( recv ), "identifier" ) || nodeTextOf( recv, src ) != receiverName ) + { + return; // receiver-less, receiver-qualified, or a DIFFERENT bare name: not a column on the block parameter + } + const std::string_view m = fieldIdentifierText( c, NodeField::Method, src ); + if( m.empty() ) + { + return; + } + const std::uint32_t line = ts_node_start_point( c ).row + 1; + if( m == "timestamps" && ts_node_is_null( fieldChild( c, NodeField::Arguments ) ) ) + { + const TSNode meth = fieldChild( c, NodeField::Method ); + const std::uint32_t s = ts_node_start_byte( meth ), e = ts_node_end_byte( meth ); + RawDef d; + d.fileId = fileId; d.line = line; d.startByte = s; d.endByte = e; d.loc = 1; + d.kind = SymKind::Section; d.lang = Lang::Ruby; + d.nameByte = s; d.name = "created_at"; defs.push_back( d ); // the two anchors are the + d.nameByte = e; d.name = "updated_at"; defs.push_back( d ); // method token's edges — no literal tokens exist + return; + } + if( std::find( kRubySchemaNonColumns.begin(), kRubySchemaNonColumns.end(), m ) != kRubySchemaNonColumns.end() ) + { + return; // t.index / references / belongs_to / polymorphic name no column (stated floor) + } + const TSNode args = fieldChild( c, NodeField::Arguments ); + if( ts_node_is_null( args ) ) + { + return; + } + const TSNode nameNode = rubyFirstNonCommentArg( args ); + if( ts_node_is_null( nameNode ) ) + { + return; + } + const std::string lit = stringLiteralText( nameNode, src ); // "" when not a string node + std::string_view txt = lit; + if( txt.empty() && kindIs( ts_node_type( nameNode ), "simple_symbol" ) ) + { + txt = nodeTextOf( nameNode, src ); + if( txt.size() > 1 && txt.front() == ':' ) + { + txt.remove_prefix( 1 ); + } + } + if( txt.empty() ) + { + return; // `""` / `: ` defensive: an empty name is no column + } + const std::uint32_t s = ts_node_start_byte( nameNode ), e = ts_node_end_byte( nameNode ); + RawDef d; + d.fileId = fileId; d.line = line; d.startByte = s; d.endByte = e; d.loc = 1; + d.kind = SymKind::Section; d.lang = Lang::Ruby; d.nameByte = s + 1; // content start — past the quote or ':' + d.name = std::string( txt ); + if( d.name == "id" ) + { + sawId = true; + } + defs.push_back( d ); +} + +// One create_table call: the schema-file gate plus the whole table's defs (name, options, columns, id). +inline void rubySchemaTableCall( TSNode n, std::uint32_t fileId, std::string_view src, std::vector& defs ) +{ + std::string_view receiverName; // the block parameter's name (`t`); empty until Gate 2 passes + // Gate 0: a rendered schema's tables live at file level or under `ActiveRecord::Schema[].define … do` + // — NEVER inside a class/module body. A MIGRATION's `create_table "users" … do |t|` (class-wrapped, + // often string-named) is the same AST shape, and without this gate it would mint the SAME columns as + // the schema that migration created — double-counting every added column on every corpus with both + // files indexed. `add_column`/`remove_column` are not read anywhere (a column added then dropped never + // registers; pinned by the migration fixtures), but the create_table of a real migration must not + // double its schema either. Excluding at least one `class`/`module` ancestor keeps the capture + // content-gated (still no path heuristic): a migration file next to the schema contributes ZERO defs. + // Checked FIRST — the cheapest gate — before the argument/block walks. + { + for( TSNode p = ts_node_parent( n ); !ts_node_is_null( p ); p = ts_node_parent( p ) ) + { + const std::string_view k( ts_node_type( p ) ); + if( k == "class" || k == "module" ) // a locals distilled walk — no cross-helper dependency + { + return; + } + } + } + const TSNode args = fieldChild( n, NodeField::Arguments ); + if( ts_node_is_null( args ) ) + { + return; + } + // Gate 1: a STRING table name as the first argument (a symbol-named table is a migration spelling, + // not a rendered-schema shape — stated floor; no name node → no schema surface either). + const TSNode tableName = rubyFirstNonCommentArg( args ); + if( ts_node_is_null( tableName ) || !kindIs( ts_node_type( tableName ), "string" ) ) + { + return; + } + // Gate 2: the call's OWN do_block, and its parameters: field carries ONE bare identifier (the `|t|`). + TSNode block{}; + { + ChildCursor cursor( n ); + forEachNamedChild( n, cursor.cur, [ & ]( TSNode c ) + { + if( kindIs( ts_node_type( c ), "do_block" ) ) + { + block = c; + return false; + } + return true; + } ); + } + if( ts_node_is_null( block ) ) + { + return; + } + const TSNode params = fieldChild( block, NodeField::Parameters ); + if( ts_node_is_null( params ) ) + { + return; // `create_table "x" do … end` with no handle — not the schema shape + } + { + ChildCursor cursor( params ); + TSNode only{}; + std::size_t named = 0; + forEachNamedChild( params, cursor.cur, [ & ]( TSNode c ) { only = c; ++named; return true; } ); + if( named != 1 || !kindIs( ts_node_type( only ), "identifier" ) ) + { + return; // multi-parameter or non-identifier handle — not a rendered schema's do |t| + } + receiverName = nodeTextOf( only, src ); // the `|t|` spelling — column calls must use it + } + // Options: the id/primary_key pairs among the call's arguments. + bool idFalse = false, primaryKey = false; + TSNode idKey{}, pkValue{}; + { + ChildCursor cursor( args ); + forEachNamedChild( args, cursor.cur, [ & ]( TSNode a ) + { + if( !kindIs( ts_node_type( a ), "pair" ) ) + { + return true; + } + const std::string_view k = rubyPairKeyText( a, src ); + const TSNode v = fieldChild( a, NodeField::Value ); + if( k == "id" ) + { + if( kindIs( ts_node_type( v ), "false" ) ) + { + idFalse = true; + } + else if( kindIs( ts_node_type( v ), "simple_symbol" ) ) + { + idKey = fieldChild( a, NodeField::Key ); + } + } + else if( k == "primary_key" ) + { + primaryKey = true; + pkValue = v; + } + return true; + } ); + } + // The columns: the do_block's body: field (its body_statement) — walk its named children directly + // (a fresh cursor; the outer walks cannot be reused mid-flight, tschildren.h). + bool sawIdColumn = false; + const TSNode body = fieldChild( block, NodeField::Body ); + if( !ts_node_is_null( body ) ) + { + ChildCursor cursor( body ); + forEachNamedChild( body, cursor.cur, [ & ]( TSNode c ) + { + rubySchemaColumnCall( c, fileId, src, receiverName, defs, sawIdColumn ); + return true; + } ); + } + // The id rule, in the grounded order: an explicit id column wins; then the pairs decide. + if( sawIdColumn || idFalse ) + { + return; + } + if( primaryKey && !ts_node_is_null( pkValue ) && kindIs( ts_node_type( pkValue ), "string" ) ) + { + const std::string name = stringLiteralText( pkValue, src ); + if( name.empty() ) + { + return; + } + const std::uint32_t s = ts_node_start_byte( pkValue ), e = ts_node_end_byte( pkValue ); + RawDef d; + d.fileId = fileId; d.line = ts_node_start_point( pkValue ).row + 1; d.startByte = s; d.endByte = e; d.loc = 1; + d.kind = SymKind::Section; d.lang = Lang::Ruby; d.nameByte = s + 1; d.name = name; + defs.push_back( d ); + return; + } + if( !ts_node_is_null( idKey ) ) + { + const std::uint32_t s = ts_node_start_byte( idKey ), e = ts_node_end_byte( idKey ); + RawDef d; + d.fileId = fileId; d.line = ts_node_start_point( idKey ).row + 1; d.startByte = s; d.endByte = e; d.loc = 1; + d.kind = SymKind::Section; d.lang = Lang::Ruby; d.nameByte = s; d.name = "id"; + defs.push_back( d ); + return; + } + // A primary_key pair with a NON-string value (composite `["a","b"]`, hand-written `:symbol`): the + // key is NOT an `id` and the rendered columns are the block's own explicit `t.` lines (already + // minted above) — suppress the implicit id entirely (the composite case is pinned by the fixture). + if( primaryKey ) + { + return; + } + // Implicit `id` — anchored at the table-name string's content start (a stable per-table byte). + { + const std::uint32_t s = ts_node_start_byte( tableName ), e = ts_node_end_byte( tableName ); + RawDef d; + d.fileId = fileId; d.line = ts_node_start_point( tableName ).row + 1; d.startByte = s; d.endByte = e; d.loc = 1; + d.kind = SymKind::Section; d.lang = Lang::Ruby; d.nameByte = s + 1; d.name = "id"; + defs.push_back( d ); + } +} + + + // F5: a Swift LOCAL binding — `let a = f()` / `var b = ...` inside a function/closure body — parses to the // same `property_declaration` node as a real stored/computed MEMBER property, so the @definition.var pattern // captures it as a spurious top-level `var` symbol AND (being the nearest enclosing symbol above the body's diff --git a/src/ingest_sidecap.h b/src/ingest_sidecap.h index 6ddd733dc..84eca911d 100644 --- a/src/ingest_sidecap.h +++ b/src/ingest_sidecap.h @@ -2419,7 +2419,7 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t // `include` in `class << self` — into binds, beside the inherit reference each mixin already is. if( le.lang == Lang::Ruby ) { - captureRubyAttrDefs( root, fileId, src, defs ); + captureRubyDefs( root, fileId, src, defs ); // #339: the Definitions-only walker (attr defs + rendered db/schema.rb columns) captureRubyBareCalls( root, fileId, src, refs ); captureRubyFactories( root, fileId, src, binds ); captureRubyHelperMethods( root, fileId, src, binds ); diff --git a/src/mcp.h b/src/mcp.h index aa31220ee..32055a633 100644 --- a/src/mcp.h +++ b/src/mcp.h @@ -141,8 +141,17 @@ inline constexpr McpToolMask kMcpAllToolsMask = ( McpToolMask{ 1 } << kMcpVerbCo // kMcpVerbTable's row for `name`, or kMcpVerbCount when the name is not an advertised tool. constexpr std::size_t mcpToolIndex( std::string_view name ) noexcept { - const McpVerbInfo* const row = findByField( kMcpVerbTable, &McpVerbInfo::name, name ); - return row == nullptr ? kMcpVerbCount : static_cast( row - kMcpVerbTable ); + // Index loop, not findByField's row-pointer arithmetic: g++ 15 rejects the + // (char*)&row + offsetof folding under -fsanitize=address ("not a constant + // expression", mcp.h static_assert on 33 rows) — same values on every compiler. + for( std::size_t i = 0; i < kMcpVerbCount; ++i ) + { + if( kMcpVerbTable[ i ].name == name ) + { + return i; + } + } + return kMcpVerbCount; } // The named profiles. `members` is a comma list of tool names; "" means every tool. diff --git a/src/model.h b/src/model.h index f3228c46f..b2ea296c1 100644 --- a/src/model.h +++ b/src/model.h @@ -83,7 +83,9 @@ inline const char* symTag( SymKind k ) noexcept case SymKind::Struct: return "struct"; case SymKind::Interface: return "iface"; case SymKind::Var: return "var"; - case SymKind::Section: return "sec"; // markdown heading (doc structure; isolated in the graph) + case SymKind::Section: return "sec"; // data-kind defs: doc headings, JSON/YAML keys, and Ruby db/schema.rb + // columns — the Ruby ones are definitions only, NO call edges + // (graph.h buildGraph's byName skips them; see isMeasurableKind) case SymKind::Macro: return "macro"; // #define (disclosed-degraded: replacement text, not a parsed body) case SymKind::Field: return "field"; // member variable (id=path::Owner::field; use-sites via --uses=Owner.field) case SymKind::Other: return "other"; diff --git a/src/quality.h b/src/quality.h index 3de7c2fae..04f9b4897 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2180,7 +2180,10 @@ inline std::string cacheRootKeyHex( const std::string& root ) // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. constexpr std::uint32_t kIngestCacheVersionMirror = 28; // MUST equal ingest.cpp's kCacheVersion (gated); 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 148; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestParserVerMirror = 149; // MUST equal ingest.cpp's kParserVer (gated) + // 149 = 2026-10-08 (merge #3: carried 126 -> 144 -> 149; the + // Rails schema-column capture, DEFINITIONS ONLY — no Section-Ruby + // call edges; trains 22-26a took 128-148) // 148 = 2026-10-04 (train 26a: PR #373 merged; above the branch's 145 and the 145–147 // lanes in flight, see kParserVer note; kIngestCacheVersionMirror stays main's 28) // 141 = 2026-10-04 (train 25: above FE-A's 134/135 and refval-edges' 140, see kParserVer diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 373536ec8..b5b6baa3b 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -9fe75d7e33ccde42c2d7f3413786598e09784269d7378a520127681836283213 +fe5af5bc4ed46a32959f56eb3294f39ccadbee8f66db20ba8d6e44398e5632bb diff --git a/test/regression.sh b/test/regression.sh index 9435c5687..6980cd847 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -277,7 +277,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 astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck 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 commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck 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 editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck 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 mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck 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 notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck 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 pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck 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 recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck 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 structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck 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 astrocheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck 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 commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck 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 editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck 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 mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck 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 notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck 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 pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck 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 recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck 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 structlayoutcheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck 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/rubyschemacheck.sh b/test/rubyschemacheck.sh new file mode 100755 index 000000000..9176f616b --- /dev/null +++ b/test/rubyschemacheck.sh @@ -0,0 +1,190 @@ +#!/usr/bin/env bash +# rubyschemacheck.sh — the Rails schema capture gate: RUBY db/schema.rb COLUMNS ARE Section DEFS. A +# Ruby file whose tree holds a `create_table "x", … do |t| … end` call is a rendered schema BY CONTENT +# (no path heuristic), and each `t. "name"` / `t. :name` receiver-call in +# the table block mints ONE SymKind::Section def at Lang::Ruby — the same data-kind slot as a doc +# heading / YAML key (model.h) — with span = the name token. Rails-generated attribute uses +# (`product.price`, pinned here as explicit-receiver model calls) resolve to the column as a def — +# DEFINITIONS ONLY by maintainer decision: the defs answer --uses/--grep/--whereis but admit NO call +# edges and no PageRank weight (graph.h buildGraph's byName skips Section-Ruby; §3 pins that). The DSL +# CALLS keep their reference posture exactly like the attr family (--uses=string/define/timestamps +# stay defs=0 external=1). The four id spellings and t.timestamps name columns too, per the rules +# below; a composite/symbol primary_key mints no implicit id. +# +# Fixture test/rubyschemafix — schema text from a real, running Rails 8.1 app's `bin/rails +# db:schema:dump` (verified db:schema:load-able), with the original domain tables scrubbed to spike_* +# names; two spellings no dump ever emits are restored BY HAND and marked in-file: `id: :uuid` (the +# dump itself notes "Could not dump … Unknown type 'uuid'") and a literal `t.timestamps` (rendered +# schemas expand it to t.datetime pairs) — runtime-semantics and shape notes in USECASES.md beside the +# fixture. +# name the 9 `t.string "name"` columns (→ defs=19 with the 9 attr/def defs + the yaml key) +# id the multi-def floor: EVERY table carries its key — 14 (12 implicit + id: :uuid + …) +# event_id `primary_key: "event_id"` renames the key off `id` +# ref `id: false` table's only column; NO id def there +# created_at/updated_at both the rendered t.datetime pair AND the literal t.timestamps call +# quote_symbol t.string :name + t.references :owner (floor) + t.index ["name"] (floor) + +# helper.string "unbound_column" — a column call on a DIFFERENT bare identifier +# names nothing (the receiver must be the block parameter, `t`) +# migration_string/migration_symbol/migration_columns the migration INTERFERENCE floors: a +# class-wrapped create_table (string- OR symbol-named) mints NOTHING — the schema is the only +# source of column names, indexing a migration beside its schema would double every def and its +# — and add_column/remove_column are argument data, never defs (a column added +# then dropped never registers) +# floor_case where("name = ?") — a string fragment stays opaque, never a def or a use +# all_four / pair_def_column / triple_* the def+attr+column(+yaml) collision matrix; the picker +# styles: `self.name` inside PairDefColumn pins its own def (locality), `rec.name` on a local and +# `SingleColumn.new.name` on a rich receiver split over the whole def set (graph_ambiguous) +# config/spike_names.yml the cross-language name key (counted, never edgeable) +# +# Usage: test/rubyschemacheck.sh | RIPWIRE_BIN=asan/ripwire test/rubyschemacheck.sh +# Exit: 0 = clean · 1 = an arm failed · 2 = usage / missing prerequisite + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +FIX="$ROOT/test/rubyschemafix" +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +fail=0 +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +[ -d "$FIX" ] || { echo "no test/rubyschemafix — fixture missing"; exit 2; } +echo "rubyschemacheck: BIN=$BIN FIX=$FIX" + +useshead(){ "$BIN" "$FIX" --uses="$1" --no-cache 2>/dev/null | grep -oE "of=\"[^\"]*\" defs=\"[0-9]+\" external=\"[0-9]+\" count=\"[0-9]+\"" | head -1; } +undefinable(){ "$BIN" "$FIX" --uses="$1" --no-cache 2>&1 | grep -q "matched no indexed definition"; } +callershead(){ "$BIN" "$FIX" --callers="$1" --no-cache 2>/dev/null | grep -oE "of=\"[^\"]*\" defs=\"[0-9]+\" count=\"[0-9]+\"" | head -1; } + +# ── 1. CAPTURE: columns are Section defs; the id rule; the timestamps pair ────────────────────────── +[ "$( useshead name )" = 'of="name" defs="19" external="0" count="3"' ] \ + && ok 'capture: name — 9 columns + 9 attr/def defs + 1 yaml key = 19; only the 3 consumers use it (the where() fragment is opaque)' \ + || no "capture: name: $( useshead name )" +[ "$( useshead id )" = 'of="id" defs="14" external="0" count="1"' ] \ + && ok 'capture: id — the multi-def floor: 12 implicit + the id: :uuid pair + the id-pair… 14 keys on 14 tables (only id_false and the renamed table have none)' \ + || no "capture: id: $( useshead id )" +[ "$( useshead event_id )" = 'of="event_id" defs="1" external="0" count="0"' ] \ + && ok 'capture: `primary_key: "event_id"` mints the renamed key def, and NO implicit id beside it' \ + || no "capture: event_id: $( useshead event_id )" +[ "$( useshead ref )" = 'of="ref" defs="1" external="0" count="0"' ] \ + && ok 'capture: the `id: false` table'"'"'s own column mints; the table has NO id def' \ + || no "capture: ref: $( useshead ref )" +[ "$( useshead created_at )" = 'of="created_at" defs="2" external="0" count="1"' ] \ + && ok 'capture: created_at — the rendered t.datetime column AND the literal t.timestamps call each mint one; the consumer call reaches them' \ + || no "capture: created_at: $( useshead created_at )" +[ "$( useshead updated_at )" = 'of="updated_at" defs="2" external="0" count="0"' ] \ + && ok 'capture: updated_at — both spellings def; no consumer calls it (floor proof the count is per-row, not padded)' \ + || no "capture: updated_at: $( useshead updated_at )" +[ "$( useshead origin )" = 'of="origin" defs="1" external="0" count="0"' ] \ + && ok 'capture: a COMPOSITE primary_key (["origin","destination"]) mints the rendered columns — and NO implicit id (the id defs=14 arm above is the regression guard)' \ + || no "capture: origin: $( useshead origin )" +[ "$( useshead destination )" = 'of="destination" defs="1" external="0" count="0"' ] \ + && ok 'capture: the composite key'"'"'s second column mints too' \ + || no "capture: destination: $( useshead destination )" +undefinable 'price > 0' \ + && ok "floor: t.check_constraint \"price > 0\", name: … — a constraint EXPRESSION names no column (would otherwise mint n=\"price > 0\")" \ + || no 'floor: price > 0 — a check_constraint expression minted a def' + +# ── 2. FLOORS: the DSL calls stay references; non-column DSL names no column ─────────────────────── +undefinable owner \ + && ok 'floor: t.references :owner names no column — owner stays undefinable (references is the reference, its arg is data)' \ + || no 'floor: owner — t.references minted a def' +[ "$( useshead index )" = 'of="index" defs="0" external="1" count="1"' ] \ + && ok 'floor: t.index ["name"] — the CALL is an external reference (DSL posture), the array arg defines nothing' \ + || no "floor: index: $( useshead index )" +undefinable email \ + && ok 'migration: a CLASS-wrapped string-named create_table ("spike_migrated") mints nothing — the same columns the migration created are already minted from the rendered schema; indexing both would DOUBLE the defs+PageRank' \ + || no 'migration: email — a class-wrapped (string-named) migration table minted a def' +undefinable widget_name \ + && ok 'migration: a symbol-named migration table (create_table :spike_widgets) mints nothing — both the class-nest and the string-name gates refuse' \ + || no 'migration: widget_name — a symbol-named migration table minted a def' +undefinable bla \ + && ok 'migration: add_column "bla" is argument data — the DSL call is a reference, its args are not defs (only the schema is the source of names)' \ + || no 'migration: bla — add_column minted a def' +undefinable gonna_die \ + && ok 'migration: a column added then REMOVED never registers (remove_column is read nowhere)' \ + || no 'migration: gonna_die — a removed column minted a def' +undefinable unbound_column \ + && ok 'receiver: a column call on a DIFFERENT bare identifier (`helper.string "unbound_column"`) names no column — the receiver must BE the block parameter (`t`) the create_table call bound' \ + || no 'receiver: unbound_column — a non-t receiver minted a def' +for dsl in string define datetime timestamps create_table; do + [ "$( useshead "$dsl" | grep -oE 'defs="[0-9]+" external="[0-9]+"' )" = 'defs="0" external="1"' ] \ + && ok "posture: the DSL call \`$dsl\` itself is still no def (reference capture posture)" \ + || no "posture: $dsl gained a def — the DSL call must stay a reference: $( useshead "$dsl" )" +done + +# ── 3. DEFINITIONS ONLY: columns admit NO call edges (maintainer decision, graph.h buildGraph byName) ── +[ "$( callershead id )" = 'of="id" defs="14" count="0"' ] \ + && ok 'defs-only: `IdDefault.new.id` matches the 14 defs but has NO caller row — columns take no edges' \ + || no "defs-only: id callers: $( callershead id )" +[ "$( callershead created_at )" = 'of="created_at" defs="2" count="0"' ] \ + && ok 'defs-only: `Timestamps.new.created_at` matches both defs but carries no call edge' \ + || no "defs-only: created_at callers: $( callershead created_at )" +[ "$( callershead ref )" = 'of="ref" defs="1" count="0"' ] \ + && ok "defs-only false-binding: a COLUMN-ONLY name (ref) gains no caller from an unrelated 'obj.ref' style site — pre-change it did bind (untyped receivers)" \ + || no "defs-only: ref callers: $( callershead ref )" +[ "$( callershead name )" = 'of="name" defs="19" count="1"' ] \ + && [ "$( "$BIN" "$FIX" --callers=name --no-cache 2>/dev/null | grep -oE 't="method"' | head -1 )" = 't="method"' ] \ + && ok 'defs-only: the name caller row is the METHOD render_label (consumer_ambiguous.rb:7) — the 9 columns contribute zero edges to the 19-def set (the other two fixture sites now decline as reads: hop_untested + declined_calls disclosed, train-26a ruby lane)' \ + || no "defs-only: name callers: $( callershead name )" + +# ── 4. AMBIGUITY + LOCALITY: the resolver splits honestly, pins on evidence ───────────────────────── +"$BIN" "$FIX" --uses=name --no-cache 2>/dev/null >"$TMP/uses_name" +grep -q 'of="name" defs="19" external="0" count="3"' "$TMP/uses_name" \ + && grep -q 'graph_ambiguous="1"' "$TMP/uses_name" \ + && ok 'ambiguity: the name gauge counts ONLY the 1 site that still splits over real (method/attr) defs — the id/created_at column sites left the gauge once columns stopped taking edges, and the train-26a ruby lane declines the second fixture site as a read (declined_calls disclosed)' \ + || no 'ambiguity: name gauge/rows wrong' +grep -q 'p="consumer_ambiguous.rb:8"' "$TMP/uses_name" \ + && ok 'ambiguous arm: rec.name on a plain local SPLITS over the whole def set (its row is in the gauge)' \ + || no 'ambiguous arm: consumer_ambiguous.rb:8 row missing' +grep -q 'p="pair_def_column.rb:15"' "$TMP/uses_name" \ + && ok 'locality arm: self.name inside PairDefColumn pins the in-class def — its row exists and is NOT in the ambiguous gauge' \ + || no 'locality arm: pair_def_column.rb:15 row missing' +grep -q 'p="column_consumers.rb:7"' "$TMP/uses_name" \ + && ok 'binding arm: SingleColumn.new.name is a real use row (explicit model receiver)' \ + || no 'binding arm: column_consumers.rb:7 row missing' + +# ── 5. MAP KINDS: the defs carry t="sec" with the DSL row family merged by name ───────────────────── +"$BIN" "$FIX" --no-cache 2>/dev/null >"$TMP/map" +# The schema.rb file block, extracted once; every kind arm asserts INSIDE it (the yaml Section key is a +# different file and must never satisfy a schema assertion). +python3 - "$TMP/map" >"$TMP/schema_block" <<'PYEOF' +import re, sys +xml = open(sys.argv[1], encoding='utf-8').read() +m = re.search(r']*>(.*?)', xml, re.S) +sys.stdout.write(m.group(1) if m else "") +PYEOF +grep -q '"$TMP/m1" 2>/dev/null +"$BIN" "$FIX" --no-cache >"$TMP/m2" 2>/dev/null +if cmp -s "$TMP/m1" "$TMP/m2"; then ok "deterministic (two --no-cache runs byte-identical)"; else no "non-deterministic"; fi +"$BIN" "$FIX" --cache="$TMP/c.bin" >"$TMP/cold" 2>/dev/null +"$BIN" "$FIX" --cache="$TMP/c.bin" >"$TMP/warm" 2>/dev/null +if cmp -s "$TMP/cold" "$TMP/warm"; then ok "warm == cold (schema defs survive the cache round-trip)"; else no "warm != cold"; fi +if command -v xmllint >/dev/null 2>&1; then + if xmllint --noout "$TMP/m1" 2>/dev/null; then ok "xml well-formed"; else no "xml malformed"; fi +else + ok "xml well-formed (xmllint absent — skipped)" +fi + +[ "$fail" -eq 0 ] && echo "ALL PASS" || { echo "SOME CHECKS FAILED"; exit 1; } \ No newline at end of file diff --git a/test/rubyschemafix/USECASES.md b/test/rubyschemafix/USECASES.md new file mode 100644 index 000000000..6cc6aba0e --- /dev/null +++ b/test/rubyschemafix/USECASES.md @@ -0,0 +1,57 @@ +# rubyschemafix — fixture fact sheet (the Rails schema-column capture) + +What this fixture is, and which claims in it are runtime-proven vs stated floors. + +## Provenance + +`schema.rb` is the rendered schema of a real, running Rails 8.1 application: its `bin/rails +db:schema:dump` output, with the original domain tables and all identifying markers stripped and the +remaining tables renamed to `spike_*`. The dump was verified to load back into that application +(`bin/rails db:schema:load` succeeded), so the shape vocabulary here — `force: :cascade`, `id: false`, +`primary_key:`, the option-pair spellings, the string/symbol column spellings, `null: false` — comes +from a working Rails install, not from a hand-invented guess. + +Two spellings no real dump ever emits are restored BY HAND in `schema.rb` and marked in-file: + +- `spike_id_uuids` with `id: :uuid` — the source app's dump FAILED for this table ("Could not dump + table … Unknown type 'uuid' for column 'id'": the dump host lacked the uuid extension). The table's + shape is the standard Rails rendering for a uuid key; the `id: :uuid` pair is the spelling ripwire's + id rule reads (the pair KEY names `id`, the uuid TYPE is not modelled — stated floor). +- `spike_timestamps_literal` with a bare `t.timestamps` — rendered schemas expand it into the two + `t.datetime` pairs (present above it as `spike_timestamps`); the DSL CALL itself exists only in + migrations. The column names it mints (`created_at`, `updated_at`) are part of Active Record's + documented timestamps contract. + +The model files mirror the source application's runtime shapes (`self.table_name`, `attribute`, +`def name`; a quoted `where("name = ?")` in `floor_case.rb` is the runtime pattern of a raw SQL +fragment). They are indexed as text; nothing here executes at gate time. + +## What the gate pins (all measured on the binary, per the repo's honesty rule) + +| Query | Pinned answer | Why | +|---|---|---| +| `--uses=name` | defs=19 external=0 count=3 | 9 columns + 9 attr/def defs + 1 yaml key; only the 3 consumers use it — the `where("name = ?")` fragment stays opaque | +| `--uses=id` | defs=14 external=0 count=1 | the multi-def floor: EVERY table carries its key (12 implicit, `id: :uuid`, and the explicit `id: false`-less ones); only `id: false` + the renamed table have none | +| `--uses=created_at` | defs=2 | the rendered `t.datetime "created_at"` column AND the literal `t.timestamps` call | +| `--uses=event_id` | defs=1 | `primary_key: "event_id"` renames the key off `id` | +| `--callers=created_at` | defs=2 count=0 | DEFINITIONS ONLY: the call site matches both defs but columns carry NO call edges (§3) | +| `graph_ambiguous` | 2 on `name` | only the sites that still split over real method/attr defs; the column sites exited the gauge when edges were switched off (§4) | + +## Stated floors (silence is stated, never silent) + +- `t.references :owner` / `t.belongs_to` / `t.polymorphic` / `t.index` name no column (the fixture + spells `references` + `index` in `spike_quote_symbol`; `owner` stays undefinable). +- A symbol-named table (`create_table :users`) is a migration spelling, not a rendered-schema shape — + no name-string node ⇒ no schema surface. +- Duplicate column names across tables stay SEPARATE defs (`id` on N tables → defs=N); the map row + merges them by name with `overloads`, never hiding the multiplicity. +- MIGRATIONS never interfere (the three migration fixtures, all pinned undefinable): a class-wrapped + `create_table "x"` / `create_table :x` — the migration shape — is refused by the class/module-nest + gate, so the schema is the ONE source of column names and indexing a migration beside its schema + cannot double any def; `add_column` / `remove_column` / `change_column` are argument data and read + nowhere — a column added then dropped (`gonna_die`) never registers. +- `where("price > ?")` string fragments are opaque to any static extractor. +- The DSL calls (`string`, `datetime`, `create_table`, …) stay external-surface references — no def + named `string` appears; only the NAME args gain defs (same posture as the attr family). +- A column call's receiver must BE the block parameter: `helper.string "unbound_column"` (a different + bare identifier) names no column — the gate accepts only `t.` (the `|t|` handle) forms. \ No newline at end of file diff --git a/test/rubyschemafix/all_four.rb b/test/rubyschemafix/all_four.rb new file mode 100644 index 000000000..423054b34 --- /dev/null +++ b/test/rubyschemafix/all_four.rb @@ -0,0 +1,14 @@ +# all-four arm: def + attribute + column in ONE model, and the yaml source in +# config/spike_names.yml. This is the canonical collision; runtime dispatch winner +# is the def, the static picker order (def > attr > column > yaml) is pinned in C. +module Spike + class AllFour < ApplicationRecord + self.table_name = "spike_all_fours" + + attribute :name + + def name + "def" + end + end +end diff --git a/test/rubyschemafix/column_consumers.rb b/test/rubyschemafix/column_consumers.rb new file mode 100644 index 000000000..b25eba7fa --- /dev/null +++ b/test/rubyschemafix/column_consumers.rb @@ -0,0 +1,17 @@ +# consumer arm: EXPLICIT model receivers — `Spike::SingleColumn.new.name` resolves to the +# column def set by name; `Spike::IdDefault.new.id` sits on the multi-table implicit id +# floor (14 id defs). Definitions-only per the review round: these calls match the defs +# but columns take NO call edges, so the gate's §3 arms are count="0" assertions. +module Spike + def self.single_name + SingleColumn.new.name + end + + def self.default_id + IdDefault.new.id + end + + def self.literal_stamp + Timestamps.new.created_at + end +end diff --git a/test/rubyschemafix/column_yaml.rb b/test/rubyschemafix/column_yaml.rb new file mode 100644 index 000000000..f794405e8 --- /dev/null +++ b/test/rubyschemafix/column_yaml.rb @@ -0,0 +1,6 @@ +# pair arm (column, yaml): column only; the yaml source lives in config/spike_names.yml. +module Spike + class ColumnYaml < ApplicationRecord + self.table_name = "spike_column_yamls" + end +end diff --git a/test/rubyschemafix/config/spike_names.yml b/test/rubyschemafix/config/spike_names.yml new file mode 100644 index 000000000..9e2e762a9 --- /dev/null +++ b/test/rubyschemafix/config/spike_names.yml @@ -0,0 +1,5 @@ +# spike_names.yml — the cross-language Section def for `name`. +# The all-four collision (def + attribute + column + yaml) in all_four.rb; the YAML +# key is counted, never edgeable (Ruby<->Yaml is not langCompatible). +name: + display: "Spike" \ No newline at end of file diff --git a/test/rubyschemafix/consumer_ambiguous.rb b/test/rubyschemafix/consumer_ambiguous.rb new file mode 100644 index 000000000..ffe21c986 --- /dev/null +++ b/test/rubyschemafix/consumer_ambiguous.rb @@ -0,0 +1,11 @@ +# consumer arm, spam: a call site with NO locality evidence — `rec.name` where rec is +# an ordinary local. Every in-repo def named `name` is a candidate (the 9 columns, the +# attr Vars, the method defs, the yaml key); the resolver SPLITS and says so (amb=K), +# never silently picks. The call edges land on the def set, not on nothing. +module Spike + class ConsumerAmbiguous < ApplicationRecord + def render_label( rec ) + rec.name + end + end +end \ No newline at end of file diff --git a/test/rubyschemafix/floor_case.rb b/test/rubyschemafix/floor_case.rb new file mode 100644 index 000000000..1e8dc422f --- /dev/null +++ b/test/rubyschemafix/floor_case.rb @@ -0,0 +1,12 @@ +# floor arm: a raw SQL string fragment. The `name` inside it is opaque to any +# static extractor — this is a DISCLOSED floor (no def, no use), proven only that +# the query runs. +module Spike + class FloorCase < ApplicationRecord + self.table_name = "spike_single_columns" + + def self.find_named( value ) + where( "name = ?", value ).to_a + end + end +end diff --git a/test/rubyschemafix/id_default.rb b/test/rubyschemafix/id_default.rb new file mode 100644 index 000000000..55f9e7ad3 --- /dev/null +++ b/test/rubyschemafix/id_default.rb @@ -0,0 +1,6 @@ +# id arm: implicit default id. +module Spike + class IdDefault < ApplicationRecord + self.table_name = "spike_id_defaults" + end +end diff --git a/test/rubyschemafix/id_false.rb b/test/rubyschemafix/id_false.rb new file mode 100644 index 000000000..732241389 --- /dev/null +++ b/test/rubyschemafix/id_false.rb @@ -0,0 +1,8 @@ +# id arm: create_table carried `id: false` — there is NO id column; the model +# declares its own primary key. +module Spike + class IdFalse < ApplicationRecord + self.table_name = "spike_id_false" + self.primary_key = "ref" + end +end diff --git a/test/rubyschemafix/id_renamed.rb b/test/rubyschemafix/id_renamed.rb new file mode 100644 index 000000000..08c02c84c --- /dev/null +++ b/test/rubyschemafix/id_renamed.rb @@ -0,0 +1,6 @@ +# id arm: `t.primary_key "event_id"` renames the key off `id`. +module Spike + class IdRenamed < ApplicationRecord + self.table_name = "spike_id_renamed" + end +end diff --git a/test/rubyschemafix/id_uuid.rb b/test/rubyschemafix/id_uuid.rb new file mode 100644 index 000000000..d0d21eaaa --- /dev/null +++ b/test/rubyschemafix/id_uuid.rb @@ -0,0 +1,6 @@ +# id arm: `id: :uuid` — the key is present, still named `id`, uuid type. +module Spike + class IdUuid < ApplicationRecord + self.table_name = "spike_id_uuids" + end +end diff --git a/test/rubyschemafix/migration_columns.rb b/test/rubyschemafix/migration_columns.rb new file mode 100644 index 000000000..c70a1fdbd --- /dev/null +++ b/test/rubyschemafix/migration_columns.rb @@ -0,0 +1,13 @@ +# migration arm, column lifecycle: an OLD migration adds "bla", a LATER one removes it — +# only the rendered schema is the source of column names. `add_column` / `remove_column` +# are not read anywhere (the DSL call is a reference, its arguments are data), so a column +# added then dropped never registers, and a column that STILL exists is registered by the +# schema, not by the migration that added it. `bla` and `gonna_die` stay undefinable. +class MoreColumns < ActiveRecord::Migration[8.1] + def change + add_column :spike_single_columns, "bla", :string + change_column :spike_single_columns, :ref, :text + add_column :spike_single_columns, "gonna_die", :string + remove_column :spike_single_columns, "gonna_die" + end +end \ No newline at end of file diff --git a/test/rubyschemafix/migration_string.rb b/test/rubyschemafix/migration_string.rb new file mode 100644 index 000000000..a25572c7e --- /dev/null +++ b/test/rubyschemafix/migration_string.rb @@ -0,0 +1,14 @@ +# migration arm, STRING-named table: a real migration wraps its create_table in a class body. +# The schema capture must NOT mint from it — the same columns the migration created are already +# minted from the rendered db/schema.rb, and indexing both would DOUBLE every def on corpora +# that carry migrations. The suppression is the +# class/module-nest gate: a rendered schema's tables live at file level or under +# ActiveRecord::Schema[].define, never inside a class. `email` here stays undefinable; the +# table's implicit id is NOT added to the id multi-def floor (the gate pins both). +class CreateUsers < ActiveRecord::Migration[8.1] + def change + create_table "spike_migrated" do |t| + t.string "email" + end + end +end \ No newline at end of file diff --git a/test/rubyschemafix/migration_symbol.rb b/test/rubyschemafix/migration_symbol.rb new file mode 100644 index 000000000..5345d1766 --- /dev/null +++ b/test/rubyschemafix/migration_symbol.rb @@ -0,0 +1,11 @@ +# migration arm, SYMBOL-named table: the other common spelling. Already floored by the +# string-name gate (a symbol-named create_table is a migration spelling, not a rendered +# schema), kept as a fixture so the floor is pinned from BOTH sides — the class-nest gate +# and the name-string gate both refuse, and the widget columns stay undefinable. +class CreateWidgets < ActiveRecord::Migration[8.1] + def change + create_table :spike_widgets do |t| + t.string "widget_name" + end + end +end \ No newline at end of file diff --git a/test/rubyschemafix/pair_def_column.rb b/test/rubyschemafix/pair_def_column.rb new file mode 100644 index 000000000..bc7fd9547 --- /dev/null +++ b/test/rubyschemafix/pair_def_column.rb @@ -0,0 +1,18 @@ +# pair arm (def, column): the class def shadows the generated attribute reader. +module Spike + class PairDefColumn < ApplicationRecord + self.table_name = "spike_pair_def_columns" + + def name + "def" + end + + # locality-resolved arm: a `self.name` read at class level, in the class whose own + # `def name` sits in this same file — the receiver pins the enclosing class, so + # the resolver picks THAT def (amb=0), never splitting over the schema column or + # the yaml key. + def self.lookup + self.name + end + end +end diff --git a/test/rubyschemafix/quote_double.rb b/test/rubyschemafix/quote_double.rb new file mode 100644 index 000000000..824666252 --- /dev/null +++ b/test/rubyschemafix/quote_double.rb @@ -0,0 +1,6 @@ +# quote-spelling arm: column declared t.string "name" (double quotes). +module Spike + class QuoteDouble < ApplicationRecord + self.table_name = "spike_quote_double" + end +end diff --git a/test/rubyschemafix/quote_single.rb b/test/rubyschemafix/quote_single.rb new file mode 100644 index 000000000..2b7cf31f4 --- /dev/null +++ b/test/rubyschemafix/quote_single.rb @@ -0,0 +1,6 @@ +# quote-spelling arm: column declared `t.string 'name'` (single quotes). +module Spike + class QuoteSingle < ApplicationRecord + self.table_name = "spike_quote_single" + end +end diff --git a/test/rubyschemafix/quote_symbol.rb b/test/rubyschemafix/quote_symbol.rb new file mode 100644 index 000000000..22cdf94ff --- /dev/null +++ b/test/rubyschemafix/quote_symbol.rb @@ -0,0 +1,6 @@ +# quote-spelling arm: column declared t.string :name (symbol). +module Spike + class QuoteSymbol < ApplicationRecord + self.table_name = "spike_quote_symbol" + end +end diff --git a/test/rubyschemafix/schema.rb b/test/rubyschemafix/schema.rb new file mode 100644 index 000000000..ced49257a --- /dev/null +++ b/test/rubyschemafix/schema.rb @@ -0,0 +1,90 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.1].define(version: 2000_01_01_000001) do + create_table "spike_all_fours", force: :cascade do |t| + t.string "name" + end + + create_table "spike_column_yamls", force: :cascade do |t| + t.string "name" + end + + create_table "spike_id_defaults", force: :cascade do |t| + end + + create_table "spike_id_false", id: false, force: :cascade do |t| + t.string "ref", null: false + end + + create_table "spike_id_renamed", primary_key: "event_id", force: :cascade do |t| + end + +# Could not dump table "spike_id_uuids" because of following StandardError +# Unknown type 'uuid' for column 'id' +# Restored by hand: the standard Rails rendering for a uuid table — `id: :uuid` (the value is a +# simple_symbol), the spelling the capture's id rule reads. + create_table "spike_id_uuids", id: :uuid, force: :cascade do |t| + end + + create_table "spike_pair_attr_columns", force: :cascade do |t| + t.string "name" + end + + create_table "spike_pair_def_columns", force: :cascade do |t| + t.string "name" + end + + create_table "spike_plain", force: :cascade do |t| + end + + create_table "spike_quote_double", force: :cascade do |t| + t.string "name" + end + + create_table "spike_quote_single", force: :cascade do |t| + t.string 'name' + end + +create_table "spike_quote_symbol", force: :cascade do |t| + t.string :name + t.references :owner + t.index ["name"], name: "idx_spike" + helper.string "unbound_column" + t.check_constraint "price > 0", name: "check_positive" + end + + create_table "spike_composite_keys", primary_key: ["origin", "destination"], force: :cascade do |t| + t.string "origin" + t.string "destination" + end + + create_table "spike_single_columns", force: :cascade do |t| + t.string "name" + end + + create_table "spike_timestamps", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + +# Migration spelling, added by hand: the rendered dump expands `t.timestamps` into the two datetime +# columns above; the DSL CALL itself (the rule under test) names both columns literally. + create_table "spike_timestamps_literal", force: :cascade do |t| + t.timestamps + end + + create_table "spike_triples", force: :cascade do |t| + t.string "name" + end + +end diff --git a/test/rubyschemafix/single_column.rb b/test/rubyschemafix/single_column.rb new file mode 100644 index 000000000..1c89b79d4 --- /dev/null +++ b/test/rubyschemafix/single_column.rb @@ -0,0 +1,6 @@ +# single-source arm: column `name` only (no def, no attr). +module Spike + class SingleColumn < ApplicationRecord + self.table_name = "spike_single_columns" + end +end diff --git a/test/rubyschemafix/timestamps.rb b/test/rubyschemafix/timestamps.rb new file mode 100644 index 000000000..59fd081ba --- /dev/null +++ b/test/rubyschemafix/timestamps.rb @@ -0,0 +1,6 @@ +# timestamps arm: `t.timestamps` yields created_at / updated_at columns. +module Spike + class Timestamps < ApplicationRecord + self.table_name = "spike_timestamps" + end +end diff --git a/test/rubyschemafix/triple_plain.rb b/test/rubyschemafix/triple_plain.rb new file mode 100644 index 000000000..d920b8458 --- /dev/null +++ b/test/rubyschemafix/triple_plain.rb @@ -0,0 +1,14 @@ +# triple arm, spelling (a): plain `def name` alongside `attribute :name` +# and a real `name` column. Ruby lookup is by ancestry — the class-level def +# wins over the module-generated attribute reader regardless of declaration order. +module Spike + class TriplePlain < ApplicationRecord + self.table_name = "spike_triples" + + attribute :name + + def name + "def" + end + end +end diff --git a/test/rubyschemafix/triple_reversed.rb b/test/rubyschemafix/triple_reversed.rb new file mode 100644 index 000000000..c9a2881ec --- /dev/null +++ b/test/rubyschemafix/triple_reversed.rb @@ -0,0 +1,13 @@ +# triple arm, spelling (a) reversed: `def name` declared BEFORE `attribute :name`. +# Same winner as triple_plain — proof that lookup is by ancestry, not declaration order. +module Spike + class TripleReversed < ApplicationRecord + self.table_name = "spike_triples" + + def name + "def" + end + + attribute :name + end +end diff --git a/test/rubyschemafix/triple_super.rb b/test/rubyschemafix/triple_super.rb new file mode 100644 index 000000000..8f58e4612 --- /dev/null +++ b/test/rubyschemafix/triple_super.rb @@ -0,0 +1,15 @@ +# triple arm, spelling (b): `def name; super; end` + `attribute :name` + +# column. Bare `super` reaches the generated attribute reader, which reads the +# column-backed @attributes store. There is no separate "column method" — the +# column IS the storage. Expected chain: def -> super -> generated attr reader -> column. +module Spike + class TripleSuper < ApplicationRecord + self.table_name = "spike_triples" + + attribute :name + + def name + super + end + end +end