Repository navigation
Meta: Hill climb Writebook internals - #22
Closed
thomasklemm wants to merge 70 commits into
Closed
thomasklemm wants to merge 70 commits into
thomasklemm wants to merge 70 commits into
Conversation
Filter intermediate polymorphic associations by their owner type in both lazy and batch SQL. Add generic emitted CRuby and native regressions, including scope and ordering controls. Co-Authored-By: Codex <noreply@openai.com>
On the ruby and JRuby targets, only an `ActiveJob::Base` subclass loads `runtime/active_job`. This causes two problems. An app without `app/jobs/application_job.rb` never loads the file. The test helper calls `ActiveJob.enqueue_without_running` at load time, so the first test of that app fails with a `NoMethodError`. An app with jobs loads the file with its models, after `thread_state`. `thread_state` replaces the job queue methods with locked, per-thread versions, and the later load puts three of the unlocked versions back: `enqueue`, `drain` and `performed`. The Spinel `boot.rb` already loads the file before `thread_state`. This commit adds the same line to the ruby overlay `boot.rb`.
AOT pins a local from its first write. Rails helpers that take a record or its fixture label (`user = users(user) unless user.is_a? User`), an id or a record (`user = User.find(user)`), or that coerce in place (`id = id.to_i`) reuse the binder, so Spinel refuses the later write (`sp_sym` then `User`). campfire's spliced `sign_in` took 27 compiled test files with it. Rewrite the later writes onto `__rh_<name>` and follow subsequent reads. A one-statement `if`/`unless` around the assign becomes a join so the fresh local is written once. Non-parameters, `||=`, and multi-statement branches stay as they are. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a108c5-8f79-731a-87ba-dce013d4be5f
A record in a non-id query slot (`before: @message`) is projected to `.id.to_s` at the call site, but the helper's RBS still said Integer. Spinel refused the String against that seed, so campfire's messages_controller_test never linked. Type those keys as String; id / *_id stay Integer because those sites pass `record.id`. `f(**h)` into `def f(**rest)` survived ingest as a positional Hash. Ruby 3 will not auto-convert it (`given 1, expected 0`) — campfire's embeds_from(**details) against attachments_for(**details). Restore the KeywordSplat after the legacy projection that strips it for explicit-keyword expansion. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a108c5-8f79-731a-87ba-dce013d4be5f
Campfire's config.ru is `use Rack::Deflater`. The CRuby overlay dropped
that line, so every signed-in page went out uncompressed (~420 KB room
page vs ~20 KB behind Thruster) and campfire-http-shape recorded 65
Content-Encoding misses.
Wrap only Main.run_rack: the /cable hijack tuple is [-1, {}, []] and
Deflater has no skip for it. Spinel has no Rack; tep gzips inline
bodies when Accept-Encoding includes gzip, using packages/zlib (the
CRC-32 port has no codec). Measured on this box: CRuby 423 KB → 20 KB;
Spinel 423 KB → 24 KB. Gzip itself costs Spinel room-page throughput
(1984 → 694 req/s); identity remains available without Accept-Encoding.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a108ac-5b18-764e-8d41-d4cddd8b07b4
scripts/bench-campfire --dhh-http --gzip times the same five workloads the Basecamp ports publish (room, messages?before=, sidebar, search, POST into a second room) with wrk, Accept-Encoding: gzip, and a restaged seed after writes. Non-2xx responses are not throughput. The seed fills message_search_index (insert_all skipped the FTS callback) and adds index_messages_on_room_id_and_created_at so a room page does not sort the whole history. rust/go/elixir lanes boot native once-campfire-* binaries on the same sqlite file. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a108ac-5b18-764e-8d41-d4cddd8b07b4
Thermos: rust/go/elixir sqlite is overwritten only when that lane runs, CSRF/login/parity curls force Accept-Encoding: identity so gzip bytes cannot empty a token, and a POST cell without a token is skipped instead of timed as 422s. Overlay/help comments now say HTML-only gzip. FTS backfill indexes plain text, not HTML. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a108ac-5b18-764e-8d41-d4cddd8b07b4
ActiveSupport's compact_blank on a Hash rejects blank VALUES, and
campfire's default_url_options is `{ host:, protocol: }.compact_blank`.
The pass only rewrote a typed Array, so a Hash of nilable Strings filed
residue and kept a send Spinel cannot AOT (`unsupported call:
compact_blank`). Rewrite it as `reject { |_k, v| v.blank? }` through
the same emptiness rule a String blank? already takes.
An Array whose element is still an inference var — a helper's
`[ author.name, author.bio ].compact_blank` — used to keep the send
too. The reject body now calls ActiveSupport.blank?, the same runtime
predicate an untyped blank? already takes. A class with its own
predicate still files residue.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a108c5-8f79-731a-87ba-dce013d4be5f
`turbo_stream.replace target, partial: "…", collection: xs, as: :x` is Rails' once-per-element render. Declining it left the builder in source shape, where turbo_stream resolves to nothing — campfire's accounts/users/index.turbo_stream.erb. Treating collection: as a single render would keep only the first element. Walk the collection into the same capture accumulator the block form already uses, render the named partial once per element through named_partial_call, and hand the concatenated String to Broadcasts.turbo_stream_fragment. An unknown option key still stays source-shaped. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a108c5-8f79-731a-87ba-dce013d4be5f
Campfire pages a room with ordered.last(PAGE_SIZE). last_n used to materialize every row and slice in memory. Unloaded, with no existing LIMIT/OFFSET, reverse each ORDER BY, LIMIT n, load, and reverse the rows back — Rails' SQL tail. A loaded relation or one that already has a window still takes the in-memory tail of that page. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a108ac-5b18-764e-8d41-d4cddd8b07b4
Tep.maybe_gzip! now leaves 1xx/204/304 and HEAD uncompressed, matching Rack::Deflater. The POST wrk seed strips the 0x prefix before parsing the table address so LuaJIT does not collapse every thread onto seed 0. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a108ac-5b18-764e-8d41-d4cddd8b07b4
--http-suite replaces --dhh-http. Comments name the workloads, not a tweet. The corpus OpenGraph fixtures are unchanged. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a108ac-5b18-764e-8d41-d4cddd8b07b4
A signed-in room page is the same identity bytes for every wrk GET. Zlib.gzip on each request was the measured cliff (Spinel 1984 → 694 req/s). Cache the compressed copy, keyed by the identity body, on both CRuby (GzipCache around run_rack) and Spinel tep. /cable, HEAD, 1xx/204/304, and Static stay uncompressed. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a108ac-5b18-764e-8d41-d4cddd8b07b4
CI unit shards failed the measured untyped ceilings after last_n (+16 full-context, +64 in the historical probe). reverse_order_term now flips each comma-separated fragment, so order(a: :asc, b: :desc) is a DESC LIMIT tail of both columns. Tep and GzipCache skip gzip when Accept-Encoding names gzip;q=0. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a108ac-5b18-764e-8d41-d4cddd8b07b4
…h-http-bench Gzip Campfire HTML, time the five HTTP routes, cache identical bodies
…e-spinel-compile-walls
CodeRabbit caught several rewrites that would change Ruby semantics: a union element with its own blank? must not go through ActiveSupport, *items beside **opts is not an erased keyword splat, and a parameter write that does not dominate later reads cannot become a branch-local. Invalid as:/locals: stay source-shaped, collection locals bind once, and a nil collection iterates as empty. The writebook inventory was failing because the turbo_stream residue message changed identity. Restore the previous wording. Ignore the unused parenthesized field in Array.wrap grounding. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a108c5-8f79-731a-87ba-dce013d4be5f
Ingest flattens a consuming `def f(**details)` to `details = {}`.
Restoring `**h` against that is unexpected keywords in Ruby 3. Only a
kept keyword-rest (`last.keyword && last.rest`) needs the splat back.
A modifier-if join nested in a loop or outer `if` does not dominate
later reads, so that parameter stays on the dynamic path. Tests now
pin both halves: flattened vs forwarding **rest, nested join-if, and
the exact `__rh_id_2` / ActiveSupport.blank? names.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Amp-Thread-ID: https://ampcode.com/threads/T-01a108c5-8f79-731a-87ba-dce013d4be5f
…inel-compile-walls Meta PR: Split rebound parameters, keep query keys and **rest on the wire
CRuby GzipCache and Spinel tep both held Mutex across Zlib.gzip, so every miss (and, on Spinel, every 420 KB Hash lookup) serialized onto one core. Gzip now runs outside the lock. CRuby still keys by the identity body — SHA-256 of the same bytes was slower on MRI (~1725 → ~1140 req/s on /rooms/1). Spinel keys by SHA-256 so the lock only covers a 64-char lookup. CRuby Rails.cache.read_str no longer dups the stored fragment: a <% cache %> hit only appends it, and DupCoder's write-side copy already isolates the store. read still dups. write_str freezes its stored copy. On this orb, CRuby /rooms/1 went 1725 → 1886 req/s, messages 3303 → 3601, search 2851 → 3271. Spinel /rooms/1 869 → 1210 with the digest key. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a108ac-5b18-764e-8d41-d4cddd8b07b4
write and increment_str put Strings in the same @DaTa hash as write_str. After read_str stopped duping, a caller that mutated a typed read of a write-path entry would change later hits. Freeze those stored copies too; read still dups. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a108ac-5b18-764e-8d41-d4cddd8b07b4
…digest Gzip Campfire HTML outside the lock; skip fragment dups on CRuby
…`'s writer (rubys#289) * `super` in a model's own password writer reaches `has_secure_password`'s writer In Rails, `has_secure_password` defines `password=` in a module the model includes, so a model can wrap it: def password=(value) @password_supplied = true super end Here the model's writer wins outright and the macro's is never synthesized, so that `super` went to `ActiveRecord::Base`, which has no such writer. `check` was clean, and the emitted program raised `super: no superclass method 'password='`. A post-analyze pass, `apply_secure_password_super`, now adds the macro's writer to such a model as `_secure_password_writer` and points each `super` in the override at `self._secure_password_writer(...)` (a bare `super` passes the writer's own parameter), as `lower::as_json_super` does for `as_json`. The override is marked as mutating self. The pass runs before `create_block`, which inlines blocks, so it sees the writer as written, and after the analyzer, so it types the call and the helper it adds: the transpile's diagnostics gain nothing. (`roundhouse check` does not run this pass.) It leaves a writer alone, rather than risk skipping another writer or passing the wrong value, when a module is mixed into the model or an ancestor model (in a class body or by an initializer), when the app already has a method of the helper's name (in a model, a column of any table, or a library class), when the writer's parameters are not one plain positional, and when a `super` sits inside a block (there the writer's parameter name can mean the block's own variable). Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * `super` in each secure-password writer reaches its macro writer A model can declare has_secure_password more than once. The super pass handled only the first declaration, so recovery_password= still reached ActiveRecord::Base and raised NoMethodError. Consider each attribute independently, retaining the existing eligibility checks for its helper. The regression covers custom writers with bare and explicit super in both declaration orders. The emitted Ruby test also checks that the two writers update their own plaintext fields. Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Bunsy <268838774+bunnykong@users.noreply.github.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The invite link stays private; contributors ask on an issue or PR. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…bys#427) Since 7816924 (rubys#279), unresolved source constants only fall back to exactly modeled classes. These standard-library names were missing from the registry, so Struct.new constant reads and JSON rescue classes became unsupported. Register the existing Ruby-family values at that fallback's registry seam. Keep capability errors for targets that do not provide them, without inventing member or synchronization return types. The execution regression passes at fc49a94, fails at 7816924 and current main, and passes with this fix. Library and focused suites pass, as do Ruby, JRuby and native Spinel execution. All 98 strict fixture/target comparisons match main; rejected cases also produce identical output with --allow-unsupported. Co-authored-by: Bunsy <268838774+bunnykong@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Preserve relation receivers for find_by and find_by! Keep terminal finder receivers on the runtime Relation path when the whole call cannot be folded. Cover inline, assigned, and helper-returned relations with emitted execution and nullable association/missing rows. Co-Authored-By: Codex <noreply@openai.com> * Document relation finder regression fixtures Explain the receiver and optional-association paths exercised by the fixture, and the distinct missing-record contracts checked by the assertion helper. Co-Authored-By: Codex <noreply@openai.com> --------- Co-authored-by: Codex <noreply@openai.com>
`rails new --api` writes `class ApplicationController <
ActionController::API`, and the runtime defined only
`ActionController::Base`. `check` was clean, but the ruby tree raised
NameError loading the controller, and the spinel binary built and then
answered every request with
500 GET /widgets/1 -- NoMethodError: undefined method 'params=' for an instance of WidgetsController
`ActionController::API` is now Base under the API name, in
`runtime/ruby/action_controller/api.rb`, which the aggregator requires
beside the other ruby-family controller files. Rails' API is Base
without the browser modules; here those stay within reach. Like the
other files the aggregator requires, it stays off the strict-target
tables.
Fixes rubys#163
Co-authored-by: Bunsy <268838774+bunnykong@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…d` (rubys#385) * `owner.<has_many>.new(attrs)` builds with the foreign key, like `build` CollectionProxy aliases `new` to `build`. Only `build`, `create` and `create!` were seeded with the owner's foreign key, so `assoc.new` reached the reader's folded Array and raised `undefined method 'new' for an instance of Array` — in a model method and in a view's blank nested form alike. `new` now takes the `build` path everywhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Normalize `owner.<has_many>.new` to `build` once, at ingest `new` on an association is CollectionProxy's alias for `build`. The previous commit taught `scope_chain` the alias, which covered a model method or view's no-arg / literal-hash form, but the motivating shape, `@article.comments.new(comment_params)` in a nested create, goes through the controller lowering (and seeds, and per-target test emitters), which know `build` only: the emitted action still called `Array#new`. Rewrite the spelling once, at the end of ingest, so every consumer reads `build`: a new `ingest::association_new` pass renames `new` when its receiver is a bare read of a name some model declares `has_many` (the by-name rule `AssocRegistry::is_has_many_name` uses), across every hook body, views, and test modules. `Model.new` (a Const receiver) and a local variable (not a Send) never match. The `scope_chain` additions become redundant and are reverted. tests/association_new_params.rs overlays real-blog's comments_controller with `@article.comments.new(comment_params)` and runs the emitted controller test, asserting the POSTed comment saved under the article. It fails on main and on the previous commit with `undefined method 'new' for an instance of Array`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Normalize association new only for its owning model Rails treats CollectionProxy#new as build, but an unrelated comments reader can return a class whose new must remain new. Move the alias normalization from ingest to the early post-analysis lower pass and require a typed model owner or the declaring model context. Preserve model method overrides and prove the emitted behavior with real-blog overlays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Require typed collection reads for association new A model class method or constant can share a has_many reader name while returning an unrelated class. Check the analyzed read type and declaring model, and keep bare reads scoped to model instance methods. Cover the real-model class receiver cases with emitted Ruby regressions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ing the callback (rubys#393) * A model callback's `if:`/`unless:` guards its body instead of dropping it `parse_callback` returned None for any callback carrying `if:` or `unless:`, so the declaration fell through to Unknown and was dropped entirely: a `before_validation :m, on: :create, if: -> { color.blank? }` ran in no circumstance at all, leaving the column blank and the record failing validation. Read the value into `Callback.condition`: a zero-arity lambda/proc contributes its body (Rails instance-execs it with self the record), a Symbol is the receiverless predicate call. `unless:` is negated, and both together become `if && !unless`, matching Rails' "run when if holds and unless does not". `push_symbol_callback` wraps the callback body in that guard, composing with the existing `on: :create` new_record? guard. An unmodeled value (a lambda with parameters, an array of conditions) still declines as before. Pinned by tests/callback_conditions.rs and two emit_and_run tests that run the emitted program; tests/model_lowerer.rs's old drop expectation is updated to the predicate guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Splice only a zero-parameter, single-expression lambda/proc condition A callback condition's lambda/proc body is spliced inline into the hook, with `self` the record and no frame of its own. A parameter (`->(r) { r.title.present? }`, `proc { |p| … }`, numbered params, `it`) would read unbound; a `return`/`next`/`break` would exit the whole callback chain; a local write would leak into the hook. Those now decline back to the unsupported-DSL warning, as before conditions were modelled, instead of emitting a broken guard with no diagnostic. The `unless:` test now asserts the negated guard itself (`if !(loud?)`) rather than names that also appear as method definitions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * A local bound by a target node also declines the condition A lambda/proc `if:`/`unless:` body is spliced inline as the callback's guard, so a local write in it would leak into the hook's scope and has to decline. The Escapes visitor caught local-variable write, operator-write, or-write and and-write nodes, but a local bound by a target node — a multiple assignment `(a, b = title, 1)`, a match-write `title in String => t`, a `rescue => e` or a hash-pattern binding — is bound through `LocalVariableTargetNode` and slipped past, splicing the body with the local leaked. Visiting that node too declines them, as they did before conditions were modelled: the callback falls back to the unsupported-DSL warning rather than running with a broken guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Any multi-write also declines a spliced condition The `Escapes` visitor already caught a local bound through a target node, which covers a multi-write whose targets are locals. A multi-write with non-local targets — `(@A, @b = name, 1) && @a` — binds through instance-variable targets instead and slipped past, splicing a guard that does not parse (`if @A, @b = [...] && @a`). Visiting the multi-write node declines it, and any multi-write, since the guard is a single inline expression and a multi-write binds regardless of target kind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Name a bundled gem in the Gemfile when the app requires it
The roundhouse-rb/once-campfire preview branch (upstream main plus the open PRs, on Rails main) failed 31 of its tests on the Ruby lane. These close the ones that are runtime or test-harness gaps: - Channel subscriptions answer `stream_names` (Rails 8.2 made `streams` private in channel tests), on both cable runtimes. - assert_response takes Rails' second argument, a failure message (`assert_response :success, platform`), instead of a response. - `ActiveSupport::JSON.encode` for a flat Hash, and `ActionCable.server.broadcast(..., coder: nil)` sending already-encoded text as given (basecamp/once-campfire#292 encodes the unread notice once). The CRuby overlay reads the text back into the Hash it carries. - Under the test job adapter, `perform_later` holds its work, the same zero-argument Proc a drain takes, so a blockless `perform_enqueued_jobs only:` runs it later, and a blockless `assert_enqueued_with` counts from the test's start (rubys#296's tests post first and perform after). Each test starts with nothing held. - assert_select's string matcher reads `:not([attr])`, the `*=`, `^=` and `$=` operators, and attribute values written with `>` where the tag helper escaped it (rubys#301's `img[src*='install-edge']`, rubys#303's `input[type=checkbox]:not([checked])` and its `data-action`). The preview branch's Ruby lane goes from 399 to 409 of 430; the pinned upstream stays at 406 of 406. tests/emit_and_run.rs pins the selector and response shapes, and the held job with its pre-encoded broadcast. The untyped-site ceiling rises by five, measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`where`/`having` with named placeholders (`:size`, `IN (:user_ids)`) only bound positional `?`, so campfire's direct-room lookup (basecamp/once-campfire#310) sent its placeholders to SQLite unbound. They read as NULL, every lookup missed, and every Ping created a new direct room. On the preview fork that surfaced as "Creator must exist" in three Rooms::Direct tests and a second room in the directs controller's. - Relation#substitute_binds hands a single Hash argument to substitute_named_binds: each `:name` the Hash answers is replaced by its escaped value, an Array by a comma-separated list; a `::` cast and an unknown name are left alone. - Relation#to_set (campfire compares direct-room members as sets). - Spinel's record equality gains `eql?` and `hash`, matching `==`, as the CRuby overlay's already does, so a Set of records loaded twice holds. Pinned on both lanes in tests/emit_and_run.rs. The active-record probe's ceiling rises by 56 (a character scan, which the probe cannot type); the full-context gate counts no new sites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lambda IR has no slot for parameters after a rest, and ingest dropped
them silently: `->(*, payload) { payload[:sql] }` emitted as
`-> { payload[:sql] }`, a body reading a name nothing bound, and the
expression IR diverged across a round trip. An expression-position
lambda also dropped a NAMED rest (`->(*args) { args }`).
Ruby's rule is that the rest takes everything but the trailing
parameters, so ingest now names the rest (`__rest` when it is
anonymous) and pops them off it, last first, ahead of the body, at all
three lambda and block sites. A named rest keeps exactly what Ruby
gives it. The one difference: too few arguments bind nil where a lambda
would raise ArgumentError.
Found in campfire's query counters (`->(*, payload)`, the shape Rails'
own query assertions document). Optional and keyword parameters on an
expression-position lambda are still dropped; that gap is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rails 7.2 puts `ActiveRecord::Assertions::QueryAssertions` on every `ActiveSupport::TestCase`, and campfire's preview-fork tests count queries, assert none match a pattern, and read query plans through `ActiveSupport::Notifications.subscribed(callback, "sql.active_record")`. None of it existed, so five test files failed to load or raised. - The test helper defines `ActiveSupport::Notifications.subscribed` for "sql.active_record" over `Db.capture_sql`, which records every statement and skips a query-cache replay as Rails' counter skips CACHE events; the callback gets Rails' five arguments and a payload with `:sql` and `:name`, and the block's value is returned. - `QueryAssertions` (`assert_queries_count`, `assert_no_queries`, `assert_queries_match`, `assert_no_queries_match`) on the same capture, leaving schema introspection out unless asked, included in TestBase. - `Connection#select_rows` answers Rails' Arrays of values. Pinned in tests/emit_and_run.rs. The active-record probe's ceiling rises by five, measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Relation#exists? with an offset asks SQLite for one row past it (`SELECT 1 AS one … LIMIT 1 OFFSET n`), as Rails does. A COUNT ignores the offset, so campfire's `paged?` (basecamp/once-campfire#297) answered whether a room had any messages at all and showed the welcome box everywhere. Without an offset the count path, which can answer from preloaded records, is unchanged. - The test helper carries the four knobs campfire's messages caching test turns around one block: `Rails.cache =`, `ActiveSupport::Cache::MemoryStore.new` (the store class Rails.cache already answers with), `ActionView::PartialRenderer.collection_cache`, and a controller's `cache_store` and `perform_caching`. The test's own assertions then exercise the runtime's fragment cache: a cached page runs no rich-text, attachment or boost queries, and an edited message refreshes. Pinned in tests/emit_and_run.rs. The active-record probe's ceiling rises by nine, measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The projection every model query reads (`_columns_sql`) now writes `"messages"."id" AS id`, the table and column always quoted, as Rails' SQLite adapter writes them. Apps' tests pick statements out by that text: campfire's query-plan test (basecamp/once-campfire#312) collects `payload[:sql]` where it `start_with?(%(SELECT "messages"))`, and with unquoted identifiers it found none. The alias keeps sql_ident's quoting only where the name needs it, so `ORDER BY created_at` still resolves against the output column. The preview fork's Ruby lane goes to 424 of 429; the pinned upstream stays at 406 of 406. Pinned by the query-assertions test in tests/emit_and_run.rs; sql_keyword_columns follows the new spelling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`app/views/product-item/_default.html.erb` was emitted as `module Views::Product-item` with a `product-item` parameter, neither of which is Ruby, so the whole view file failed to parse. `camelize` now treats `-` as a word separator (`ProductItem`) and the inferred view argument turns `-` into `_`. Found by parsing the emitted views of a real app (21 of 793 views, all in `product-item/`-style directories). `a_hyphenated_view_directory_renders` renders an ERB partial from such a directory through the blog's controller test; it fails with a SyntaxError without the change. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
`.slim` templates were recorded as "view template not ingested". A new
`src/slim.rs` compiles Slim to the `_buf`-append shape on top of the HAML
compiler's emitter: tags with `.class`/`#id`, attributes (quoted, Ruby,
wrapped, boolean, `==` raw, multi-line), `tag: child` nesting, `=`/`==`/`-`
with blocks closed by indentation, `|`/`'` text, `/` and `/!` comments,
`doctype html`, and the `ruby:`, `javascript:` and `css:` embedded
engines. Other embedded engines, splats and non-HTML5 doctypes are survey
gaps.
Per invariant 6, `a_slim_view_renders` (tests/emit_and_run.rs) swaps the
blog's index for a Slim twin and asserts the rendered markup. It found
that `render_attrs`, which HAML and Slim both emit, was never routed to
`ViewHelpers`, so a dynamic attribute raised NoMethodError in the emitted
Ruby; it is now a `ViewHelperKind::RenderAttrs`.
Checking a real Slim app (745 templates, 0 errors in them) and parsing
the emitted views also exposed three shared bugs, each with a unit test:
`ruby_string_literal` escaped the code inside `#{}`, the Ruby emitter
dropped the parentheses of `(x rescue nil) == true`, and emitted
`j if c ... end` for a command call whose first argument is an `if`.
Tests that used `.slim` as the unsupported-engine witness use `.rabl`.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
`ruby_string_literal` copies a balanced `#{...}` verbatim but ended it at
the first `}`, including one inside a percent literal such as `%q(})`,
after which the rest of the code was escaped as text. The scan now skips
percent literals (paired and same-character delimiters, backslash
escapes), treating `%` after an operand as modulo.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lid Ruby Resolves the conflict with rubys#440 in tests/emit_and_run.rs by keeping both new tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…opts
* Push Relation#last_n into SQL with a reversed LIMIT
Campfire's room page is ordered.last(PAGE_SIZE) over a room that can
hold hundreds of messages. last_n used to hydrate the whole relation
and then Array#last, so with_creator / with_attachment_details /
with_boosts preloaded every row, not the page.
Match Rails: reverse each ORDER BY, LIMIT n, reverse the rows so the
original direction is restored. Unparseable order terms and an OFFSET
keep the materialize fallback.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Skip html_escape and url_encode copies on already-safe strings
gsub always allocates, even when the pattern misses. Campfire's room
page escapes author names, CSS classes, and numeric cache-busters on
every message row; almost none contain special characters.
Return the input after a match? scan, the same shortcut ERB::Util uses
for an already-safe string. Dirty strings still go through the table
gsub. Same fast path on builder_text, builder_attr, and the URI
encoders.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Stop allocating a fresh Hash on every no-opts tag helper
def f(opts = {}) evaluates a new Hash on every call that omits
options. Campfire's message partials call link_to, content_tag, and
button_to dozens of times per row.
Share one frozen EMPTY_HTML_OPTS. Methods that delete keys already
dup first, so the constant is not mutated.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Yield Relation#each from the loaded cache without duping
Collection partials lower to relation.each. to_a.each allocated a
shallow copy of the records array on every pass, including the
messages/_message loop on /rooms/1.
Walk the memoized array in place, the same array a later each reuses.
to_a still dups, so callers that mutate its result cannot corrupt the
cache.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Memoize ActionText::Content#to_plain_text
blank?/empty?/present? all call to_plain_text. On a fragment-cache
miss campfire's message presentation asks more than once per body,
and the HTML scan is the expensive half.
Cache the result on the Content instance. @html is set only in
initialize, so the memo stays valid for the object's lifetime.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Route Relation#find_each through the zero-copy each path
find_each was still to_a.each. After each walks the loaded cache
without duping, find_each should share that path so a second pass
over the same page does not allocate another Array.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Optimize Rails::Cache read_str/write_str for Campfire fragment keys
Skip key.to_s when the key is already a String, and split read_str
expiry so ttl-0 entries return without touching Time.now. Add focused
unit tests for the string store read/write path.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Fast-path link_to and content_tag when html options are empty
Campfire message rows call both helpers many times per render with no
extra attributes. Skip opts.to_h on Ruby Hash, bypass render_attrs when
the options hash is empty, and build the minimal anchor/tag string
directly.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Generalize new runtime-opt comments and pin ActionText blank memo
Reword the last_n / html_escape / to_plain_text / cache comments as
general Rails patterns rather than Campfire-specific call sites, and
add a blank?/to_plain_text reuse assertion for the memo path.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Probe Relation existence with SELECT 1 LIMIT n, not COUNT
Rails' exists?/empty?/any? ask for one matching row; one?/many?
only need to know whether more than one exists. Route those through
exists_sql / probe_existence so cardinality checks never hydrate.
Relation#size uses COUNT when unloaded (Rails contract). last_page?
already short-circuits a short loaded page; keep offset_row_exists?
as the named OFFSET-aware probe.
Generic Item tests pin: no hydrates on exists?/empty?/any?/one?/many?,
exists_sql shape, unloaded size, and short-page last_page?.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Probe Relation existence with SELECT 1 LIMIT instead of COUNT
Wire exists?/empty?/any?/one?/many? through exists_sql so cardinality
questions do not hydrate rows or scan a full COUNT. size answers from
COUNT when unloaded; last_page? short-circuits on a short loaded page.
Add focused hydrate-count and SQL-shape tests.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Short-circuit find_by(id: nil) without a SQL round trip
A sole primary-key => nil lookup can never match (PKs are NOT NULL).
Return nil before WHERE id IS NULL LIMIT 1 — the room-show
find_messages path when message_id is absent.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Address Thermos review: each return, drop empty-tag forks, pin last_n
Relation#each still walks the loaded cache without duping, but returns
self so callers cannot mutate @records. Remove link_to/content_tag empty
fast-paths that duplicated render_attrs' empty short-circuit. Keep
convert_html_to_plain_text private and widen Cache read_str/write_str
key types in RBS. Add last_n tests for loaded / prior LIMIT / OFFSET.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Keep Relation/ActionText typing gates green after SQL opts
Raise the measured Bar B and spinel-blog RBS ceilings for exists_sql /
probe_existence / nil_primary_key_lookup? and find_each's zero-copy
loop. Declare Content#convert_html_to_plain_text, return Relation from
each/find_each, and duplicate find_each's loop so Bar A stays at zero
TyVars.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Ask unloaded Relation#include? via exists? instead of ids
Campfire membership checks (room.users.include?) were projecting every
id (and sorting when the scope was ordered). Cast the record id, answer
from the loaded cache when present, otherwise SELECT 1 LIMIT 1.
Pin Campfire room-show shapes in unit tests: last_page hydrates PAGE_SIZE
rows, find_by(id: nil) skips SQL, any?/exists_sql avoid ORDER BY and
hydration.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Emit String#match?(re) as re.test(s) under TypeScript
The TS emitter assumed Regexp receivers for match?, so
`s.match?(HTML_ESCAPE_PATTERN)` became `s.test(...)` and threw.
Swap when the receiver is String (or the argument is Regexp).
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Keep ActionText::Content#blank? as OwnDispatch in blank lowering
Framework tests load Content outside the app class registry, so the
pass folded content.blank? to false. Empty markup is blank; leave the
real method in place.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Fix Relation size/exists DISTINCT; drop find_by nil special case
size on a limited unloaded relation counts a SELECT-1 subquery so
limit(2).size is at most 2. exists_sql projects DISTINCT primary keys
so distinct.many?/one? see separate rows. Remove the Campfire-only
find_by(id: nil) short-circuit. Revert Cache key is_a?(String) no-op
and cross-target assert_same on escape identity.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Portable escape fast-paths: include? instead of String#match?
String#match?(re) has no portable emit on Rust/Python (match_pred /
match_p on str), which broke compare(rust) and the Python overlay.
Probe with include? character chains instead — same skip-gsub win,
YJIT-friendly, and every target already emits include?. Revert
opts.is_a?(Hash) gates that mapped to HashMap#is_object under Rust.
Bump the RBS untyped ceiling +6 for limited size / DISTINCT exists.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Fix distinct.limit.size and recognize Ty::Str for match? emit
Limited size projected DISTINCT 1, collapsing distinct rows to one;
project the primary key when @distinct, matching exists_sql. TypeScript
String#match? → re.test also treats Ty::Str receivers as strings.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Rewrite rel.count > n to more_than? (SELECT 1 OFFSET n)
Campfire's Message.paged? is count > PAGE_SIZE. COUNT(*) answers the
total; more_than?(n) asks the same FROM/JOIN/WHERE as count_sql then
LIMIT 1 OFFSET n, without mutating the relation (offset would) and
without hydrating. Model.any?/none? take the same exists? probe.
Drop unused offset_row_exists?; share loaded_records between each
and find_each; qualify exists?(id) with the primary key.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Force more_than? parens; pin emit_and_run and typing ceilings
Operator `count > n` is unparenthesized, so the rewrite emitted
`more_than? PAGE_SIZE`. Always parenthesize. Overlay more_than? onto
real-blog so treeshake keeps the method. RBS untyped 1386 -> 1395;
runtime_src gradual 571 -> 560.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Keep Base.any?/none? free of Relation for strict targets
Strict targets transpile Base without ActiveRecord::Relation. Move
the SELECT-1 class emptiness probe to the ruby-family connection.rb
reopen (same pattern as where/all). Also: last_page? short-circuit
only for a non-empty short page, so an empty out-of-range page is
not reported as last.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Fix CI: connection any? RBS, C#/Kotlin empty opts maps
connection.rb's Base.any?/none? reopen lacked RBS (unit fully-typed
gate). EMPTY_HTML_OPTS as a module constant emitted as object? on C#
and untyped-key empty `{}` args became MutableMap<Any?, Any?> on
Kotlin — pin Dictionary/String-keyed maps and align the RBS with
helper opts params.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Trim chatty comments on Relation, helpers, and emit pins
Keep the traps (strict COUNT, DISTINCT collapse, offset mutates,
include? vs match?). Drop duplicate link_to notes and Campfire
asides that restated the code.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
* Correct gradual typing ceiling: 521 was a mis-measure (562)
Base.any?/none? COUNT + connection.rb exists? reopen still pays
Relation.new sites this probe counts. Earlier -39 claim was wrong;
measured residual is 562.
Co-Authored-By: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Thomas Klemm <github@tklemm.eu>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
…#453) * Opt-in draft CI and add a ci:spinel lane Draft PRs stay idle until ci:draft, ci:spinel, or ci:full is applied. ci:draft runs fixture + unit only; ci:spinel runs the Ruby floor plus the full Spinel suite without other-language SDKs. Ready PRs keep path-selected coverage. Gate generate-fixture and unit on planner selection so idle drafts do not burn runners. Co-Authored-By: Cursor <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Widen unit CI to four parallel shards Spread integration targets across four shards (max-parallel 4) to shorten the unit critical path on ready, ci:draft, and ci:spinel runs. Co-Authored-By: Cursor <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Harden always() gates when plan output is missing Cancelled superseding PR runs leave compact-required/ci-summary with an empty CI_PLAN. Treat that as incomplete instead of crashing json.loads. Co-Authored-By: Cursor <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Fix ci:spinel false-failing the compact gate compact-required cannot see Spinel-only jobs. Stop special-casing SPINEL_LANE list identity; compact uses PUBLICATION ∩ jobs (the Ruby floor). Track completeness only for jobs the gate observes. Document stacked-label precedence and cover draft+spinel plus compact needs. Co-Authored-By: Cursor <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Skip compact/summary gates when the workflow was cancelled Bare always() still scheduled those gates after cancel-in-progress, leaving them QUEUED and holding the PR concurrency group so ready/full runs never started. Match other jobs: always() && !cancelled(). Co-Authored-By: Cursor <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
always() && !cancelled() still queued compact-required / ci-summary after plan was cancelled (run 37366559376), leaving them QUEUED and holding the concurrency group. Require plan success and no cancelled need so superseded runs skip immediately instead of blocking the next push/PR run. Co-Authored-By: Cursor <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu>
Use the same path-selected baseline for draft and ready pull requests. Remove idle-draft and ci:draft modes and status-only workflow triggers, while retaining ci:spinel, ci:full, and the four complete unit shards. Give both final gates an explicit !cancelled() status check so expected skips and selected failures reach result evaluation instead of GitHub implicitly suppressing the jobs with success(). Retain cancellation, reuse evidence, and canonical-main publication boundaries. Co-authored-by: OpenAI <noreply@openai.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a10db0-785a-727d-b34c-7956be4620d5
Campfire's undecodable-image test (basecamp/once-campfire#311) builds its upload with rack-test's `Rack::Test::UploadedFile.new(StringIO.new( bytes), type, original_filename:)`. Undefined, it was a constant error on CRuby, and on Spinel a refusal that kept all 18 tests in messages_controller_test from compiling. The test helper now defines the StringIO form as a subclass of the runtime's ActionDispatch::Http::UploadedFile, which is what Rack hands a controller, so a params read takes it unchanged. The path form is not modeled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…th checkpoints Three SQLite policies in runtime/spinel/db_cruby.rb, after the once-campfire Ruby port's measurements: - Read snapshot: the dispatcher and test harness bracket GET/HEAD; the first prepare opens a deferred BEGIN, any exec ends it (a write inside a stale snapshot fails at once with SQLITE_BUSY), the next read opens a fresh one. State lives on the leased connection, not in fiber storage, which Thread.new copies into threads spawned mid-request. - Write permit: writers in one process queue on a Mutex instead of racing SQLite's busy handler; transactions begin IMMEDIATE (Rails 8); cross-process waits use the GVL-releasing busy_handler_timeout. Lease release rolls back an abandoned transaction and frees the permit. - Checkpoints: config.ru asks for checkpoint_in_background!; each serving process sets wal_autocheckpoint=0 on its connections and runs PASSIVE every 250 ms, RESTART (under the permit) past ~32 MB of log. The spinel and JDBC shims define the hooks as no-ops for now. tests/db_sqlite_concurrency.rs pins all three against a file database (three of its checks fail on the previous shim; the snapshot ones need the new API); campfire's suite is 406/406 on the ruby lane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A transaction that starts a writer thread and joins it would wait on the permit forever; before the permit that case waited out SQLite's busy timeout and raised SQLITE_BUSY. The permit is now a Mutex + ConditionVariable with a deadline (5 s): on timeout the writer goes to SQLite unqueued, which is exactly the old behaviour. A transaction begun that way runs its statements without re-queueing, and a permit whose owner fiber is dead counts as free, as the Mutex it replaces did. Lease release closes a leftover snapshot before checking for an abandoned transaction, so it never rolls back its own snapshot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eRuby mise_prepend_latest (a538732) picked the last `sort -V` entry under installs/<tool>, and installs/ruby also holds truffleruby-40.0.0 and jruby-10.1.1.0, which sort after every MRI version. The Rails oracle then booted under TruffleRuby and failed loading `debug` ("unknown event: call"), so the box's nightly campfire run has published nothing since 10-05 (20261005-111323 holds only logs/ and skipped.json). Only plain version directories (4.0.5, 1.26.4, 1.18.4-otp-28) are candidates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ints The CRuby shim's policy (404e32e, e0898fb), ported to runtime/spinel/db.rb: - Snapshot state and the write permit live on DbConn: one lease holder at a time, so no thread or fiber identity is needed. The permit is a Mutex + ConditionVariable with the same 5 s bound and fallback to SQLite's own busy wait. - The snapshot's BEGIN/COMMIT run through the connection's prepared- statement cache, not sqlite3_exec, which re-parses every call (the likely cost behind the CRuby lane's -1..1.6% on GETs). - tep/app.rb brackets GET/HEAD inside the request lease; scaffold/main.rb starts the checkpointer thread at boot (no fork on this lane). - Db.exec and the snapshot calls return a value: spinel compiles a method ending in bare nil as void, and `Db.with_connection { Db.exec(...) }` assigns the block's value. tests/support/db_concurrency_spinel.rb is the CRuby checks in the subset spinel compiles, driven by db_sqlite_concurrency's ignored `spinel_shim_policy`, registered in ci-plan's SPINEL_TESTS and owned by the db inputs. Campfire on the spinel lane: 406/406, 69/69 linked; the server binary builds, serves GETs at c=32 and a first-run POST. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Add bounded Date runtime for Spinel (rubys#303) Program-defined Date for Spinel with ISO SQL seams, JSON serialization, and emit-and-run coverage. Load Date only when the app uses date values so Campfire is not broken by poly Time|Date strftime (matz/spinel#7334). Refs rubys#303 Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Harden Spinel date seams and CI fixture for date_columns_spinel Empty-string format_db_date → nil; escape_value formats Date as YYYY-MM-DD when loaded; framework-tests-spinel downloads real-blog. Refs rubys#303 Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Tighten Spinel AR RBS probe ceiling after Date measure Date `_as_json_only` branch measures 1407 (was provisional 1412). Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Move Spinel date JSON out of shared _as_json_only Keep date-column ISO rewrite in the Spinel-only serialization reopen (omit-when-unused), matching the CRuby overlay posture. Shared connection.rb no longer pays Bar B / AR RBS-probe residuals for Date. Refs rubys#303 Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Package Spinel Date as an omit-gated unit Move parse/format and date JSON rewrite into the Date package, inject boot requires when needed, keep default as_json always-on, and dedupe schema column-list emit. Expand Spinel date column contract tests. Refs rubys#303 Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Lock ci-plan Date ownership expectations Update ci_plan_test.py for date_columns_spinel ownership of the Date package and sqlite_adapter, matching scripts/ci-plan.py. Refs rubys#303 Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Unblock CI plan when Date package edits project.rs The planner's project_change_scope regex backtracked for minutes on the Date boot inject's single escaped multiline string, timing out the 5m plan job. Use concat! for that inject, and skip the project.rs body scan on ci:spinel/full lanes where select short-circuits anyway. Refs rubys#303 Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* Tighten Relation RBS: Base model handle and Array helpers Bar B drops 562 → 458 (−104). initialize takes Base (not Class — that breaks Bar A); sort_in_place!/loaded_tail use Array[untyped]; order_key_of and include? take Base. Leave preloaded bare for Spinel integer seeds. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Skip Relation to_a dups on Enumerable; cheap blank?/attrs map/collect/filter/index_by/group_by/all?/partition/length walk loaded_records instead of to_a's shallow copy. Content#blank? returns true on empty markup without a plain-text scan. render_attrs builds one buffer instead of Array+join. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Memoize sanitize allow-lists; batch HTML text runs Writebook Page#plain_text / sanitize and Campfire ActionText pay O(n) 1-char concatenations and rebuild ~40-element allow-list Arrays per pass. Freeze the rule tables behind memoized ivars, and append ordinary text in one slice until the next markup boundary in to_plain_text, sanitize_engine, strip_tags, and sanitize_text. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Type Relation terminals as Base?/Base (−23 Bar B) first/take/last/first! and find_by/find_by!/first_or_initialize/ find_or_create_by. find(Array) stays untyped. Residual 458→435. Interim before Relation[T] — not per-model element typing. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Count DISTINCT and GROUP BY result sets (rubys#343 slice) count_sql uses a DISTINCT-pk or GROUP BY subquery so scalar count / total_count match the result-set shape, not underlying rows. Pins in base_test; spinel RBS ceiling 1400→1404 for the new SQL branches. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Content#blank? skips scans on whitespace-only markup Empty and whitespace-only HTML return true without to_plain_text. Empty shells and blockquote curly quotes still go through the scan. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Level-3 Base.any? probes with SELECT 1 (_adapter_any?) Synthesize per-model `_adapter_any?` as EXISTS LIMIT 1; Base.any?/none? call it. Hand-written default stays count > 0 (AdapterInterface has no select_rows). Pins in model_lowerer; spinel RBS ceiling 1404→1405. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Drop historical measurement changelogs from typing ratchets Bar B and spinel-blog RBS ceilings keep a short current explanation only. Soft ratchets still fail on rise; ceilings stay at 435 / 1405. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Preserve canonical class IDs in the RBS runtime probe Seed method contexts and dispatch from fully-qualified RBS names, aliasing short names only when unambiguous. Drops the short-name mismatch that counted Connection#quote_string's String param as unresolved (1412 → 57 on this head). Fixes rubys#435 Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Model ActiveRecord::Base.sanitize_sql_array (rubys#400) Alias the documented array-form entry point to the existing sanitize_sql bind interleave, with RBS and emit-and-run coverage. Fixes rubys#400 Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Register sanitize_sql(_array) on ActiveRecord::Base App analysis never saw the connection.rb Base reopen methods, so both sanitize_sql and sanitize_sql_array failed send_dispatch even with the runtime alias present. Seed them as String class methods on Base (Fixes rubys#400 analyze half). Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Relation#ids preserves uuid and named primary keys (rubys#310) Select the model's primary_key and cast through _cast_primary_key instead of hard-coding `.id` + `.to_i`. Widen Relation#ids in the shipped sidecar for string-key apps. Fixes rubys#310 Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Lower each.with_index to each_with_index for Spinel AOT Enumerator-chained each.with_index fails Spinel when the block makes a keyword call that closes over the index (Writebook Positionable #move_to_position). Flatten to each_with_index, binding a nonzero offset as index = __with_index_i + n. map.with_index is left alone. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Ingest ActionText::Markdown as a real model (storage only) Seed ActionText::Record in ModelBases and resolve bare Record under module ActionText so Writebook's lib/rails_ext Markdown classifies as a model. Seed the action_text_ table prefix; parent ApplicationRecord for emit (gem abstract base is not ingested). Pins ingest identity and emit_and_run content save/reload. Does not claim has_markdown, touch, or polymorphic record readers. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Drive Spinel AR RBS probe residual 57→0 Seed dependency RBS (Db/Rails/MessageVerifier/ActiveSupport), mirror class_methods, union-merge ivars across Base stems, overlay Relation query and Base lifecycle ivars, and rewrite Relation#with_recursive away from Untyped Array#map. Publicize extract_ivar_assignments for the probe harvest. Soft ceiling now 0. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Harvest RBS @ivar decls for the Spinel AR probe Add parse_app_ivars and seed the RBS probe from declared instance variables instead of a parallel class-name overlay table. Keeps the .rbs sidecars as the single contract for Relation/Base query and lifecycle ivars while preserving cross-stem flow merge (ceiling 0). Co-authored-by: Thomas Klemm <github@tklemm.eu> * Address Thermos maintainability on hill-climb tip Split Exists adapter probes into adapter_emit/exists.rs so adapter_emit stays under 1k. Share append_join_where / append_group_having for count_sql and exists_sql. Move ActionText::Record emit parent into ModelBases.emit_superclass. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Expand mattr/cattr/attr on model ingest for Markdown.renderer ActionText::Markdown as a model dropped mattr_accessor expansion that library ingest had, so to_html's renderer call and Writebook inventory went unresolved. Synthesize the same class/instance readers/writers and refresh the pinned inventory. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Skip monomorphic synth for unresolved polymorphic belongs_to Empty polymorphic_targets (ActionText::Markdown / RichText record) were falling through to Record.find_by and a writer that drops _type. Match only polymorphic: false for the mono arm; pin with a lowering test. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Address CodeRabbit findings on hill-climb tip blank? treats entity-decoded whitespace as blank; sanitize_sql skips quoted `?`; count_sql keeps explicit select projections; Relation#include? accepts Base?; mattr expands instance readers and rejects unmodeled default:; ModelBases resolves bare parents before close_over and prefers scoped bases; each.with_index only lowers literal Int offsets; RBS harvest reads self.@ ivars; probe short-name/union merge tightenings. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Keep plain attr_* Unknown on model body walk for concerns Expanding attr_reader/writer/accessor in ingest_model_body_items broke concern included blocks: is_candidate only matches Unknown Sends, so included_has_accessor went false (private; hard-failed ingest) and virtual accessors never spliced. Only expand mattr_*/cattr_* for Markdown.renderer; leave attr_* for concern_accessors. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Address second-pass CodeRabbit findings on tip SQLite sanitize_sql treats backslash as literal; sanitize_sql_array dispatches named Hash and %s binds; grouped count_sql keeps DISTINCT; from(...).distinct.count projects a bare primary key; each.with_index with MethodRef/rest blocks keeps the dynamic path when an offset cannot be applied. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Keep sanitize_sql_array on positional ? for Bar B Named Hash / %s array forms raised the untyped residual (+4). Leave those Rails shapes for a follow-on; rubys#400 / emit_and_run pin positional `?`. Backslash-literal quote scan and the other second-pass fixes stay. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Spinel: rewrite Relation Base handle/terminals to untyped Shared relation.rbs keeps initialize:(Base) and Base?/Base terminals for Roundhouse Bar A/B. Spinel treats Base as an instance pointer, so Relation.new(User) and User slots receiving first/find_by failed campfire AOT. Rewrite those signatures to untyped on the Spinel emit tree only; pin with a unit test. Local campfire spinel make build reaches build/blog after the rewrite. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Confine Relation Base rewrite to Spinel; qualify from+joins distinct count spinel_relation_model_handle lived in spinel_files, so CRuby/JRuby also got untyped initialize/terminals. Apply it only on Spinel assembly (spinel_base_files + BuildTarget::Spinel) and pin that Ruby keeps Base. When from(...).joins(...).distinct.count projects the primary key, qualify with the active FROM source so SQLite does not reject ambiguous bare id. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Fix hosted CI: portable render_attrs, Content#blank?, optioned mattr render_attrs used String#<< which lowers as .add / write-into-&str on Kotlin, C#, Rust, Python, and Elixir. Concat like sanitize_to_id. Content#blank? called ActiveSupport inside ActionText::Content, which emitted tests resolve as ActionText::Content::ActiveSupport. Scan decoded plain text locally. mattr/cattr with default: / a block on a model returned Unsupported and rewrote Writebook inventory Errors into ingest-gap Infos. Leave those sends unknown instead of expanding without the initializer. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Keep optioned-mattr skip without reformatting model.rs The previous tip rustfmt'd ingest/model.rs. Restore original wrapping and keep only the Writebook default:/block fall-through. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Pin Writebook inventory for optioned mattr/cattr leftovers Optioned `mattr_accessor :renderer, default:` and `cattr_accessor :preview_renderer do` stay unexpanded so ingest does not drop the initializer. Refresh the pin for the resulting unresolved `renderer` / `preview_renderer` diagnostics and emit unsupported-DSL warnings. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> * Fix nested abstract close_over and each_with_index temp shadowing close_over stored a bare parent (`MidBase`) when the qualified base was not yet known, then failed to match `ActionText::MidBase`. Qualify the stored spelling against the child's enclosing modules. Offset inject now picks `__with_index_iN` when the block already uses the default temp name, instead of shadowing an outer local. Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
add basic security policy
Compile-time class-body macros now expand a statically interpolatable class_eval heredoc (and substituted has_one) the same way literal define_method already did. Load-hook includes contribute class-method macros without installing mixin instance methods. HasOne carries scope and autosave; polymorphic builders set the interface type column. Forcing tests are synthetic overlays on the blog fixtures (enum, attribute, define_method, heredoc class_eval), not a named app macro. Dynamic string eval stays unexpanded. Refs #30 Co-Authored-By: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Thomas Klemm <github@tklemm.eu>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
|
Opened against wrong repo; retargeting upstream. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hill-climb stack against
rubys/roundhousemain. Lead slice is compile-time class-body declarations (#30), done as general patterns, not a Markdown-named lowerer.This commit
Expand interpolatable
class_evalheredocs (and substitutedhas_one) at ingest, the same way literaldefine_methodalready expands.on_load(:active_record)includes contribute class-method macros without installing mixin instance methods.HasOnecarriesscope/autosave; polymorphic builders set the interface type column.Forcing tests are synthetic overlays on real-blog / tiny-blog (
tests/class_body_declarations.rs): enum, attribute, literaldefine_method, heredocclass_eval. Dynamic string eval stays unexpanded. Writebook/Lobsters remain extra coverage, not the gate.In scope (still stacking)
has_onepath runs)delegated_typewith emit_and_runeach_with_indexanalyzer re-pass) if still neededOut of scope
has_markdownruntime (autosave/preload/destroy) until emit_and_run proves itRefs #30
Verification (this slice):
cargo test --locked --test class_body_declarations --test on_load_includes --test model_macro_expansion --offline— all passed.