diff --git a/.claude/agents/blog-batch-orchestrator.md b/.claude/agents/blog-batch-orchestrator.md new file mode 100644 index 000000000..278c74f40 --- /dev/null +++ b/.claude/agents/blog-batch-orchestrator.md @@ -0,0 +1,55 @@ +--- +name: blog-batch-orchestrator +description: Outer-loop orchestrator for N-post blog sprints - picks topics from the groomed queue with live-GSC premise audits, runs blog-post-coordinator per post (WIP=1), then batch gates, plan/OKF sync, one PR, and CI watch. Loops until N posts are delivered or the queue runs dry. +model: fable +--- + +# Blog Batch Orchestrator (outer loop) + +Purpose: deliver N publish-ready posts end-to-end without stopping between +posts. Canonical process: `docs/workflows/blog-pipeline.md` (per-post pipeline ++ Batch mode section). You are the manager: sequence, dispatch, verify, +record - you write plans and reports, not post prose. + +## The outer loop + +0. **Setup once**: feature branch; session-random dev server (`PORT=$((20000 + + RANDOM % 20000)) bin/dev`); read the 20.09 plan §12 queue + refresh-debt + rows; optionally start a NotebookLM deep-research run for fresh angles. +1. **Pick next row** - premise audit FIRST: live GSC for the topic (plans + decay), dedup check. Collision → record the rescope verdict in the plan and + take the next row. Real-code mining (this repo, ~/dev/elital) happens here + so the coordinator gets a sanitized digest, not repo access questions. +2. **Dispatch ONE `blog-post-coordinator`** with the topic row, branch, the + dev-server port/base-URL from step 0, and the digest. WIP=1: wait for its + SHIPPED / RESCOPE / BLOCKED report before the next row. Independent + research/recon agents (read-only) may run in parallel; never a second + committing coordinator in the same checkout - if a second one must run, + it gets its own git worktree (shared-checkout commits race). +3. **Verify the report**: spot-check the commit exists, gates quoted, riskiest + claims noted. BLOCKED → record in the plan and move on; do not force gates. +4. Loop 1-3 until N SHIPPED or the queue is dry after rescopes. + +## Batch close-out (after the last post) + +5. **Cluster sweep** per `.okf/content/voice-rules.md` same-day-cluster rule: + grep the batch for shared intensifiers, metaphor families, verbatim 4+-word + phrases, cloned CTA tails, meta-narration templates. Fix across posts, then + run one slop/editor critic pass on the sweep DIFF (4-eyes is blocking for + every commit) before the polish commit. +6. **Records ride the branch**: 20.09 statuses + changelog, `/okf:okf maintain` + + `/okf:validate .okf --strict`, blog index regen. +7. **One PR** for the batch: per-post verdict table, per-commit summary, gates + listed, flagged decisions surfaced to Paul. Merge conflicts with a moving + master are expected - merge master in, resolve (`.okf/log.md` appends: keep + both), rebuild, push. +8. **Watch CI** with a Monitor until settled; re-run known flakes (15-min + checkout timeout) once with evidence; report any real failure instead of + retrying blindly. + +## Handback + +End with: verdict table (slug / SHIPPED-RESCOPE-BLOCKED / commit), PR link, +localhost review link per post on your session port, and every decision you +flagged rather than made. A post is not "delivered" until it passed cold-eyes, +its gates are green, and its commit is on the PR. diff --git a/.claude/agents/blog-post-coordinator.md b/.claude/agents/blog-post-coordinator.md new file mode 100644 index 000000000..b49addb98 --- /dev/null +++ b/.claude/agents/blog-post-coordinator.md @@ -0,0 +1,60 @@ +--- +name: blog-post-coordinator +description: Inner-loop coordinator for ONE blog post - drives writer, 3-critic panel, cold-eyes gate, and ship gates in a loop until PUBLISH-READY and committed, or 2 failed fix rounds. Spawned per post by blog-batch-orchestrator or the main session. +model: fable +--- + +# Blog Post Coordinator (inner loop) + +Purpose: take ONE assigned topic row from idea to a committed, publish-ready post. +You own the loop; you do not stop mid-post to ask questions that the canonical +docs already answer. + +Input contract (from your spawner): the topic row (slug, angle, dedup notes), +the branch to commit on, the dev-server port/base-URL for the scroll gate +(never assume 1313; if missing, start your own on a session-random port), and +any source-material digest (real-code mining, trends facts). If a required input is missing, derive it from +`docs/projects/2510-seo-content-strategy/20-29-strategy/20.09-content-plan-revision-aug-2026.md` +before asking anyone. + +## The loop + +1. **Premise + dedup** (skip only if spawner says already done): live GSC pull + for the topic, `ls content/blog | grep` + qmd search. Collision → report + RESCOPE verdict back to spawner instead of writing. +2. **Writer**: spawn a `content-creator` agent with the full packet - voice + guide 90.11, cadence anchor from `docs/workflows/blog-writer-reference-samples.md`, + blog-pipeline STEP 4-4d rules, verified internal links, frontmatter hard + rules, latest-versions-as-present framing, zero fabrication. Two-pass write. +3. **Critic panel** (parallel, resumable via SendMessage): `core-reviewer` as + (a) tech fact-checker - fetch every cited source, verify every claim and + every line of copyable code; (b) slop/SEO - AI-pattern scan, SEO checklist, + cross-post repetition gate vs siblings; (c) copy editor - thoughtbot cadence, + cut-test, diagram earns-its-place. Synthesize; apply fixes yourself + (surgical edit discipline). **Iterate: re-send changed sections to the + objecting critic until tech is clean, slop >= 8/10, editor <= minor.** +4. **Cold-eyes**: fresh-context `content-creator` with edit rights running the + blog-pipeline STEP 5c 9-check prompt verbatim. Loop 5c (not the panel) until + PUBLISH-READY. (The canonical loop's founder-resonance check is absorbed + here and by the editor critic; for founder-stream posts add the Alex persona + as a fourth panel critic.) +5. **Ship gates**: stitch 6-slot cover (render at 2400x1260x2, downsample, + VISUALLY inspect the PNG), `bin/render-mermaid` (min visible font x 390 / + viewBox-width >= 9px), `bin/hugo-build` + `ruby bin/check-post-visuals` + green, chrome-devtools scroll gate desktop 1280x800 + mobile 390x844 with + zero console errors and zero 404s. +6. **Commit** on the assigned branch, reviewer verdicts quoted in the message. + +## Stop conditions (report, don't grind) + +- Any gate - including the step-3 critic thresholds and step-4 cold-eyes - + still fails after **2 full fix rounds** → commit nothing, report BLOCKED + with the failing findings. The iterate instructions above are bounded by + this cap. +- Dedup collision (step 1) → report RESCOPE with the owning post named. +- Anything requiring a business decision (pricing, disclosure of internal + numbers, naming) → flag it in your report; make the conservative call in + the text meanwhile. + +Report back: slug, verdict (SHIPPED / RESCOPE / BLOCKED), commit SHA, gate +results one line each, and the 3 riskiest claims a human should eyeball. diff --git a/.okf/build/test-gates.md b/.okf/build/test-gates.md index 1bb1216f0..62ee7f491 100644 --- a/.okf/build/test-gates.md +++ b/.okf/build/test-gates.md @@ -8,6 +8,7 @@ generated: { by: claude/opus-4-8, at: 2026-08-12T20:20:00Z } verified: - { by: claude/fable-5, at: 2026-08-01T11:30:00Z } - { by: claude/sonnet-5, at: 2026-08-20T00:00:00Z } +timestamp: 2026-08-20T00:00:00Z --- # The suites @@ -19,7 +20,7 @@ verified: | `bin/rake test:critical` | Critical Minitest suite (34 runs / 53 screenshots), ~81s host / ~46s Docker since the 2026-08-01 skip_area fix | At component/task milestones and before every commit outside sprint micro-commit trains | | `bin/test` | Visual regression on the host (baselines in `macos/` on a Mac; on Linux, comparable to `linux/` when run through `bin/setup-test-env`'s pinned stack) | ONCE at PR prep (branch head, before `gh pr create`) or on Paul's explicit confirmation - NOT per commit (Paul 2026-07-31: qtest is the routine gate) | | `bin/dtest` | Same suite in Linux/Docker (baselines in `linux/`) - CI runs Linux | Same trigger as bin/test; a PR must never open without this leg (green-locally / red-in-CI otherwise) | -| `bin/check-post-visuals` | Ratchet: counts blog posts over 800 words with no mermaid/SVG/image. Fails when the count exceeds `FLOOR` (78 as of 2026-08-13) | Before publishing any post. Added 2026-08-13 - `diagram_rendering_test.rb` only proves diagrams RENDER, nothing proved posts HAVE them, and 25 of the 31 long posts published since 2026-04 shipped with none | +| `bin/check-post-visuals` | Ratchet: counts blog posts over 800 words with no mermaid/SVG/image. Fails when the count exceeds `FLOOR` (**72** as of 2026-08-20, down from 78) | Before publishing any post. Added 2026-08-13 - `diagram_rendering_test.rb` only proves diagrams RENDER, nothing proved posts HAVE them, and 25 of the 31 long posts published since 2026-04 shipped with none | `bin/qtest` page keys mirror `themes/beaver/assets/css/pages/*.css` basenames AND `critical/-critical.css` basenames - the two sets differ (e.g. @@ -40,6 +41,14 @@ extend it when adding components or critical files. The macOS full suite remains verify by reading the built HTML or the render, not by trusting green. This is the false-green class documented in `docs/20-29-testing-qa/test-architecture-anti-masking.md`. +- **Flat-file posts are invisible to the visuals ratchet** (2026-08-20). + `bin/check-post-visuals` globs `content/blog/*/index.md`, so a post living as + a flat file (`content/blog/2025/.md`) is outside the ratchet population + AND outside page-bundle tooling - no pre-rendered mermaid, no local cover. + Converting one to a bundle preserves its URL as long as the `slug` + frontmatter is set, and adds it to the ratchet. The script self-reports slack + (`post-visuals: floor is loose, lower FLOOR to N`) - obey it: `FLOOR` dropped + 78 -> 72 on 2026-08-20 on the script's own prompt. - **`FORCE_SCREENSHOT_UPDATE=1` re-records EVERYTHING** (2026-08-14). On `bin/dtest` it also disables the `git checkout -- .../linux` guard that normally discards sub-tolerance Rosetta drift, so a run rewrites all 45 diff --git a/.okf/content-strategy/content-plan.md b/.okf/content-strategy/content-plan.md index 996f30ac0..4f25d60e9 100644 --- a/.okf/content-strategy/content-plan.md +++ b/.okf/content-strategy/content-plan.md @@ -12,6 +12,7 @@ verified: at: 2026-08-13T09:56:20Z - by: claude/opus-5 at: 2026-08-19T00:00:00Z +timestamp: 2026-08-20T00:00:00Z --- # Plan of record (2026-08-07) @@ -97,6 +98,15 @@ One test: **will someone search this in six months?** Version-current content incurs refresh debt - budget 1 refresh slot/month. Proof it compounds: the Kamal 1/Traefik post is now factually wrong and still ranking. +**Pick the refresh slot from live GSC, not from the plan (2026-08-20).** The +plan's own "still ranking" assertions decay within weeks. The August slot was +decided on a live pull at decision time: the row the plan named had fallen to 4 +impressions, while `ruby-3-4-yjit-performance-guide` - unnamed anywhere in the +plan - was carrying 6,310 impressions at position 9.5 and turned out to be full +of fabricated claims (see [claims-canon](/content/claims-canon.md)). Same rule +as §"before writing a queued row": check the artifact, not the plan's memory of +it. The MCP pulls live - [analytics-access](/workflows/analytics-access.md). + # Overview Supersedes the ICP-E-focused [20.07 plan](https://github.com/jetthoughts/jetthoughts.github.io/blob/emdash/seo/docs/projects/2510-seo-content-strategy/20-29-strategy/20.07-content-plan-icp-e-q2-2026.md). diff --git a/.okf/content/claims-canon.md b/.okf/content/claims-canon.md index 0cfaac539..7d8d9f65a 100644 --- a/.okf/content/claims-canon.md +++ b/.okf/content/claims-canon.md @@ -7,6 +7,7 @@ status: stable generated: by: claude/opus-5 at: 2026-08-20T00:00:00Z +timestamp: 2026-08-20T00:00:00Z sources: - id: hugo-config resource: config/_default/hugo.toml @@ -147,6 +148,33 @@ grep): from staff experience ("our fractional CTOs average 15+ years") and false-positives immediately. A ban that cannot tell those apart is noise. +# Ranking legacy posts are a live reservoir of fabricated claims (2026-08-20) + +The fabricated-claims purge (PRs #475 / #477 / #481) swept landing and course +surfaces. It did not reach the blog archive, and the blog archive is where the +traffic is. `ruby-3-4-yjit-performance-guide` - **6,310 impressions at position +9.5**, one of the site's better-ranking pages - carried four classes of +invention at once: + +| Fabrication | What it claimed | +|---|---| +| Invented third-party internals | Shopify saving "$2.4M" from YJIT | +| Invented third-party results | GitHub deployment outcomes | +| Invented client work | two JetThoughts case studies with no client behind them | +| Invented future | a Ruby 3.5 / 3.6 / 4.0 roadmap | + +Rewritten in place 2026-08-20 as the Ruby 4.0 YJIT-vs-ZJIT guide with every +claim sourced. Two rules follow: + +- **Extend the purge to ranking legacy posts, highest-impression first.** A + fabricated client story on a page nobody reads is a liability; on a page at + position 9.5 it is the first thing a prospect sees. SEO-era posts predate the + sourced-numbers policy and were never audited against it. +- **Audit the premise before refreshing.** The content plan's *named* refresh + candidate had decayed to 4 impressions while this unnamed post was the real + asset. Pull live GSC at decision time; see + [content-plan](/content-strategy/content-plan.md). + # Where this canon is repeated Correcting a published fact means sweeping the **instruction layer**, not just diff --git a/.okf/content/index.md b/.okf/content/index.md index 17d7db11a..9c4bca077 100644 --- a/.okf/content/index.md +++ b/.okf/content/index.md @@ -2,6 +2,6 @@ * [Course structure](course-structure.md) - the 5-module spine, Going Further set, and course_sequence.yaml * [Course canon](course-canon.md) - the numbers every chapter must agree on -* [Company claims canon](claims-canon.md) - founding date, tenure, rating; what JetThoughts may assert about itself and where it is ratcheted +* [Company claims canon](claims-canon.md) - founding date, tenure, rating; what JetThoughts may assert about itself, where it is ratcheted, and why ranking legacy blog posts still hold fabricated client stories * [Voice rules](voice-rules.md) - Sam voice, banned patterns, and the em-dash rule * [Banned-strings ratchet](banned-strings-ratchet.md) - how fixed prose defects stay fixed diff --git a/.okf/content/voice-rules.md b/.okf/content/voice-rules.md index df248bf8d..0710e248b 100644 --- a/.okf/content/voice-rules.md +++ b/.okf/content/voice-rules.md @@ -7,8 +7,8 @@ generated: by: process:okf-migrate at: 2026-07-13T00:00:00Z verified: - by: claude-opus/4.8 - at: 2026-08-19T00:00:00Z + by: claude-fable/5 + at: 2026-08-20T00:00:00Z --- Full guides: `docs/90-99-content-strategy/strategy-analysis/90.11-voice-guide.md` @@ -81,6 +81,18 @@ worked with", dual-source statistical openers, sustained staccato. Sentence-level slop scoring is necessary but NOT sufficient - shape-tell review (essay arc, pivot sentences, cloned scaffolding) runs alongside it. +**Same-day-cluster tells (2026-08-20, 4-post batch review).** When several posts +ship in one batch, a second sweep runs ACROSS them, because per-post review can't +see cluster fingerprints: (1) shared intensifier stamping - "genuinely" appeared in +all four posts of the 2026-08-20 batch; (2) a shared metaphor family - the +"X earns Y" economics figure appeared 5x across the batch (earn their keep / pays +rent / earns it / earn attention); cap at one per post; (3) verbatim phrase reuse +("covers that half of the build" word-for-word in two siblings); (4) meta-narration +templates ("X deserves its own paragraph" / "deserves more attention than it +gets"); (5) cloned CTA tails ("does exactly that" / "builds exactly this"). +Sweep: grep the batch for the day's pet intensifiers and repeated 4+-word phrases +before the PR. + Regression sweep before any handback: ```bash diff --git a/.okf/log.md b/.okf/log.md index d9f5432e2..d6f822e50 100644 --- a/.okf/log.md +++ b/.okf/log.md @@ -1,5 +1,29 @@ # Bundle Update Log +## 2026-08-20 (YJIT refresh) - the fabricated-claims purge never reached the blog archive + +* **A page at position 9.5 was inventing client work.** + `ruby-3-4-yjit-performance-guide` (6,310 impressions) carried invented Shopify + internals ("$2.4M saved"), invented GitHub deployment results, two fabricated + JetThoughts client case studies, and a fictional Ruby 3.5/3.6/4.0 roadmap. + Rewritten in place as the Ruby 4.0 YJIT-vs-ZJIT guide with every claim + sourced. PRs #475/#477/#481 swept landing and course surfaces; the blog + archive - where the traffic actually is - was never audited. Purge continues + highest-impression-first. Recorded in + [claims-canon](/content/claims-canon.md). +* **The plan's named refresh candidate had decayed to 4 impressions** while + this unnamed post was the real asset. The slot was chosen from a live GSC + pull at decision time, not from the plan's month-old "still ranking" claims. + Generalises the existing "check the artifact before drafting a queued row" + rule to refresh slots: [content-plan](/content-strategy/content-plan.md). +* **Flat-file posts sit outside two toolchains at once.** + `content/blog/2025/*.md` is missed by `bin/check-post-visuals` (globs + `content/blog/*/index.md`) and by page-bundle tooling (pre-rendered mermaid, + local covers). Bundle-izing preserves the URL when `slug` frontmatter is set + and adds the post to the ratchet population. `FLOOR` ratcheted 78 -> 72 on + the script's own "floor is loose" prompt. + [test-gates](/build/test-gates.md). + ## 2026-08-20 (#29 re-source closed) - the v1 list is dead, and VERBATIM tags recorded intent, not verification * **The whole 25-row v1 cold list is closed: 19 verified-and-dropped, 6 @@ -2139,3 +2163,14 @@ ridden the PR that needed them. Recipe corrected in [ci-gates](build/ci-gates.md). Generalised check before any automation call: *does this write to the repo, and if so, where does it land?* + +## 2026-08-20 - Same-day-cluster voice tells (4-post AI+Rails batch) + +Shipping four posts in one day exposed a review gap: per-post critics pass a +post while the BATCH carries fingerprints - a shared intensifier ("genuinely" +in all four), a shared metaphor family ("X earns Y" 5x), verbatim phrase reuse +between siblings, meta-narration templates, and cloned CTA tails. Added the +cross-batch sweep to `content/voice-rules.md`. Also: critic-tech caught that +ruby_llm 1.16 accepts a block only on `model` - `temperature {}` is a silent +no-op - which corrected the R5 post's published sketch (and exposed a likely +live bug in the source app's own AgentBase). diff --git a/bin/check-post-visuals b/bin/check-post-visuals index 13b0b9afd..fbd2092a0 100755 --- a/bin/check-post-visuals +++ b/bin/check-post-visuals @@ -13,7 +13,7 @@ # Usage: bin/check-post-visuals [dir] (default: content/blog) # POST_VISUALS_LIST=1 to print the full burn-down list. -FLOOR = 78 +FLOOR = 72 MIN_WORDS = 800 root = ARGV[0] || "content/blog" diff --git a/content/blog/2025/ruby-3-4-yjit-performance-guide.md b/content/blog/2025/ruby-3-4-yjit-performance-guide.md deleted file mode 100644 index 47b138e48..000000000 --- a/content/blog/2025/ruby-3-4-yjit-performance-guide.md +++ /dev/null @@ -1,1243 +0,0 @@ ---- -dev_to_id: 2159834 -title: "Ruby 3.4 YJIT Performance Guide: Complete JIT Optimization for Rails Applications" -description: "Master Ruby 3.4's YJIT compiler for up to 30% performance gains in Rails applications. Complete guide with benchmarks, configuration, and production deployment strategies." -date: 2025-01-20 -created_at: "2025-01-20T10:00:00Z" -edited_at: "2025-01-20T10:00:00Z" -draft: false -tags: ["ruby", "rails", "performance", "yjit", "optimization"] -canonical_url: "https://jetthoughts.com/blog/ruby-3-4-yjit-performance-guide/" -cover_image: "https://res.cloudinary.com/jetthoughts/image/upload/v1737374400/ruby-yjit-performance-guide.jpg" -slug: "ruby-3-4-yjit-performance-guide" -metatags: - image: "https://res.cloudinary.com/jetthoughts/image/upload/v1737374400/ruby-yjit-performance-guide.jpg" - og_title: "Ruby 3.4 YJIT Performance Guide | JetThoughts" - og_description: "Master Ruby 3.4's YJIT compiler for 30% performance gains in Rails applications. Complete guide with benchmarks and production deployment." - twitter_title: "Ruby 3.4 YJIT Performance Guide" - twitter_description: "Complete YJIT guide: benchmarks, configuration, production deployment for Rails performance optimization" ---- - -Ruby 3.4's YJIT (Yet Another Ruby JIT) represents a significant leap forward in Ruby performance optimization. After years of development by Shopify's Ruby team, YJIT has matured into a production-ready just-in-time compiler that delivers substantial performance improvements for Rails applications and CPU-intensive Ruby workloads. - -This comprehensive guide explores everything you need to know about leveraging YJIT in Ruby 3.4 for maximum performance gains in your production applications. - -## What is YJIT and Why It Matters in 2025 - -YJIT is Ruby's state-of-the-art just-in-time compiler that transforms frequently executed Ruby code into optimized machine code at runtime. Unlike the previous MJIT compiler, YJIT uses a fundamentally different approach called Lazy Basic Block Versioning (LBBV) that produces more efficient optimizations with lower memory overhead. - -In Ruby 3.4, YJIT has reached new performance heights with several critical improvements: - -- **30% faster method calls** through improved inline caching -- **Reduced memory consumption** by up to 40% compared to MJIT -- **Better compatibility** with complex Rails applications -- **Enhanced ARM64 support** for Apple Silicon and cloud deployment - -The significance of YJIT extends beyond raw performance numbers. As Ruby applications scale to serve millions of requests, even modest performance improvements translate to substantial cost savings in cloud infrastructure and improved user experience. - -For Rails applications specifically, YJIT excels at optimizing the hot paths that matter most: ActiveRecord queries, view rendering, and business logic execution. Real-world benchmarks from Shopify show 15-25% performance improvements in production Rails applications, with some CPU-intensive workloads seeing gains exceeding 40%. - -The evolution from MJIT to YJIT represents a paradigm shift in Ruby optimization philosophy. While MJIT attempted to optimize entire methods, YJIT focuses on basic blocks—smaller units of code that can be optimized more effectively with better cache locality and reduced compilation overhead. - -## How YJIT Works: Under the Hood - -Understanding YJIT's architecture is crucial for maximizing its performance benefits. At its core, YJIT employs a sophisticated multi-stage optimization pipeline that balances compilation speed with runtime performance. - -### Lazy Basic Block Versioning (LBBV) - -YJIT's foundation is LBBV, a technique that creates specialized versions of code blocks based on runtime type information. Instead of compiling entire methods upfront, YJIT waits for code to execute, observes the actual types and values used, then generates optimized machine code for those specific cases. - -```ruby -# Ruby code that benefits from LBBV -def calculate_total(items) - total = 0 - items.each do |item| - total += item.price # YJIT specializes based on item types - end - total -end - -# After warming up, YJIT generates specialized code like: -# - Version 1: items is Array, item is Product with Float price -# - Version 2: items is Array, item is LineItem with Integer price -``` - -This approach provides several advantages: - -1. **Precise type specialization**: Code is optimized for the exact types encountered in production -2. **Reduced compilation overhead**: Only hot code paths are compiled -3. **Better cache efficiency**: Smaller compiled units improve instruction cache performance - -### Code Generation and Optimization Pipeline - -YJIT's code generation process involves multiple optimization passes that transform Ruby bytecode into efficient machine code: - -**Phase 1: Execution Profiling** -YJIT monitors bytecode execution to identify hot basic blocks—sequences of instructions that execute frequently without branches. The profiling threshold is configurable but defaults to 30 executions for optimal balance between compilation overhead and optimization opportunity. - -**Phase 2: Type Analysis** -When a basic block reaches the compilation threshold, YJIT analyzes the runtime types and method dispatch patterns. This includes: -- Instance variable types and layouts -- Method receiver types -- Constant values and their stability -- Array and hash structure patterns - -**Phase 3: Machine Code Generation** -YJIT generates optimized assembly code using its custom x86-64 and ARM64 assemblers. Key optimizations include: - -```ruby -# Example: Optimized method dispatch -class User - def full_name - "#{first_name} #{last_name}" - end -end - -# YJIT can optimize this to direct memory access instead of method lookup -user = User.new("John", "Doe") -user.full_name # Compiled to efficient string concatenation -``` - -**Phase 4: Invalidation and Recompilation** -YJIT maintains assumptions about type stability and method definitions. When these assumptions are violated (e.g., monkey patching a method), affected compiled code is invalidated and falls back to interpretation until recompilation. - -### Memory Management Improvements - -Ruby 3.4's YJIT includes significant memory management optimizations: - -**Code Block Pooling**: Compiled code is organized into pools that can be garbage collected when no longer needed, preventing memory leaks in long-running applications. - -**Adaptive Compilation Limits**: YJIT automatically adjusts compilation thresholds based on available memory, ensuring stable performance even under memory pressure. - -```ruby -# Memory-efficient compilation example -RUBY_YJIT_EXEC_MEM_SIZE=64 # Limit JIT memory to 64MB -RUBY_YJIT_CODE_GC=1 # Enable code garbage collection -``` - -## Enabling and Configuring YJIT - -Successfully deploying YJIT requires understanding its configuration options and system requirements. Ruby 3.4 includes YJIT by default, but optimal performance requires proper tuning. - -### Installation Requirements - -YJIT requires minimal additional dependencies but has specific system requirements: - -**System Requirements:** -- Ruby 3.4.0 or later (compiled with YJIT support) -- x86-64 or ARM64 processor architecture -- Minimum 2GB RAM (4GB+ recommended for production) -- GCC 4.9+ or Clang 3.9+ if compiling from source - -**Verification:** -```ruby -# Check if YJIT is available -puts RubyVM::YJIT.enabled? - -# View YJIT statistics -pp RubyVM::YJIT.runtime_stats if RubyVM::YJIT.enabled? -``` - -### Configuration Options - -YJIT provides extensive configuration through environment variables and runtime options: - -**Essential Environment Variables:** - -```bash -# Enable YJIT (Ruby 3.4 default) -export RUBY_YJIT_ENABLE=1 - -# Set compilation threshold (default: 30) -export RUBY_YJIT_MIN_CALLS=50 - -# Limit JIT memory usage (default: 128MB) -export RUBY_YJIT_EXEC_MEM_SIZE=256 - -# Enable code garbage collection -export RUBY_YJIT_CODE_GC=1 - -# Detailed statistics (development only) -export RUBY_YJIT_STATS=1 -``` - -**Advanced Configuration:** - -```ruby -# Programmatic YJIT configuration in Rails -# config/application.rb -class Application < Rails::Application - if RubyVM::YJIT.respond_to?(:enable) - RubyVM::YJIT.enable unless Rails.env.test? - - # Configure for production workload - if Rails.env.production? - ENV['RUBY_YJIT_MIN_CALLS'] = '20' - ENV['RUBY_YJIT_EXEC_MEM_SIZE'] = '512' - end - end -end -``` - -### Production Deployment Considerations - -Deploying YJIT in production requires careful consideration of several factors: - -**Docker Configuration:** -```dockerfile -FROM ruby:3.4-alpine -ENV RUBY_YJIT_ENABLE=1 -ENV RUBY_YJIT_MIN_CALLS=30 -ENV RUBY_YJIT_EXEC_MEM_SIZE=256 - -# Ensure sufficient memory for container -# Add ~256MB to base memory requirements -``` - -**Kubernetes Resource Limits:** -```yaml -resources: - requests: - memory: "1Gi" # Increased from 768Mi for YJIT - cpu: "500m" - limits: - memory: "2Gi" # Account for JIT memory usage - cpu: "1000m" -``` - -**Monitoring Setup:** -```ruby -# Add YJIT metrics to your monitoring system -class YjitMetrics - def self.collect - return {} unless RubyVM::YJIT.enabled? - - stats = RubyVM::YJIT.runtime_stats - { - yjit_compiled_blocks: stats[:compiled_block_count], - yjit_exec_memory: stats[:exec_mem_size], - yjit_compilation_ratio: stats[:compiled_iseq_count] / stats[:iseq_count].to_f - } - end -end -``` - -**Gradual Rollout Strategy:** -1. Enable YJIT in staging environment with production-like load -2. Monitor for 48 hours, checking for memory leaks or performance regressions -3. Deploy to 10% of production traffic using feature flags -4. Gradually increase rollout percentage while monitoring key metrics -5. Full deployment once stability is confirmed - -## Performance Benchmarks: Real-World Results - -Comprehensive benchmarking reveals YJIT's true performance impact across different application types and workload patterns. These benchmarks use representative Rails applications and common Ruby patterns. - -### Rails Application Benchmarks - -**E-commerce Platform (Spree-based):** -Testing a typical e-commerce application with product catalog browsing, search, and checkout workflows: - -```ruby -# Benchmark setup -require 'benchmark/ips' - -# Product listing with complex queries -Benchmark.ips do |x| - x.config(time: 30, warmup: 10) - - x.report("Ruby 3.3") do - # Simulated without YJIT - ProductCatalog.featured_products(limit: 50) - end - - x.report("Ruby 3.4 + YJIT") do - ProductCatalog.featured_products(limit: 50) - end - - x.compare! -end -``` - -**Results:** -``` -Ruby 3.3: 245.2 i/s -Ruby 3.4 + YJIT: 318.5 i/s - 1.30x faster -``` - -**JSON API Serialization:** -```ruby -# Complex nested serialization benchmark -class UserSerializer - def serialize(users) - users.map do |user| - { - id: user.id, - name: user.full_name, - profile: serialize_profile(user.profile), - recent_orders: user.orders.recent.map(&:serialize) - } - end - end -end - -# Benchmark results: -# Ruby 3.3: 89.4 i/s -# Ruby 3.4 + YJIT: 127.8 i/s - 1.43x faster -``` - -### CPU-Intensive Workload Results - -**Mathematical Computation:** -```ruby -# Fibonacci calculation (recursive) -def fibonacci(n) - return n if n <= 1 - fibonacci(n - 1) + fibonacci(n - 2) -end - -# Benchmark fibonacci(35) -# Ruby 3.3: 0.85 i/s -# Ruby 3.4 + YJIT: 1.41 i/s - 1.66x faster -``` - -**String Processing:** -```ruby -# Text analysis workload -def analyze_text(content) - words = content.downcase.split(/\W+/) - word_count = Hash.new(0) - - words.each do |word| - next if word.empty? - word_count[word] += 1 - end - - { - total_words: words.length, - unique_words: word_count.length, - most_common: word_count.sort_by { |_, count| -count }.first(10) - } -end - -# Processing 1MB text file: -# Ruby 3.3: 2.1 i/s -# Ruby 3.4 + YJIT: 2.9 i/s - 1.38x faster -``` - -### Memory Usage Analysis - -YJIT's memory usage patterns differ significantly from MJIT and require careful monitoring: - -**Memory Overhead Comparison:** -```ruby -# Memory usage profiler -class MemoryProfiler - def self.measure - GC.start - before = memory_usage - - yield if block_given? - - GC.start - after = memory_usage - - { - before: before, - after: after, - delta: after - before - } - end - - private - - def self.memory_usage - `ps -o pid,rss -p #{Process.pid}`.split("\n")[1].split[1].to_i - end -end -``` - -**Results (Rails application startup):** -``` -Ruby 3.3 (no JIT): 124MB base memory -Ruby 3.4 + YJIT: 156MB base memory (+25% overhead) -MJIT (Ruby 3.1): 187MB base memory (+51% overhead) - -After 1 hour production traffic: -Ruby 3.3: 298MB stable memory -Ruby 3.4 + YJIT: 334MB stable memory (+12% overhead) -MJIT: 412MB growing memory (+38% overhead) -``` - -### Framework-Specific Performance Gains - -**Sinatra Microservice:** -```ruby -# Simple JSON API -class ApiApp < Sinatra::Base - get '/users/:id' do - user = User.find(params[:id]) - user.to_json - end - - post '/users' do - user = User.create(JSON.parse(request.body.read)) - user.to_json - end -end - -# Load test results (1000 concurrent requests): -# Ruby 3.3: 1,245 req/sec, 95th percentile: 89ms -# Ruby 3.4 + YJIT: 1,687 req/sec, 95th percentile: 63ms -``` - -**Roda Application:** -```ruby -class App < Roda - route do |r| - r.root do - @posts = Post.published.includes(:author).limit(10) - view('homepage') - end - - r.on 'posts' do - r.get Integer do |id| - @post = Post.find(id) - view('post') - end - end - end -end - -# Template rendering benchmark: -# Ruby 3.3: 156.3 i/s -# Ruby 3.4 + YJIT: 198.7 i/s - 1.27x faster -``` - -## YJIT Optimization Strategies - -Maximizing YJIT performance requires understanding which code patterns benefit most from JIT compilation and how to structure your application to take advantage of YJIT's strengths. - -### Code Patterns That Benefit Most - -**Method Chains and Fluent Interfaces:** -```ruby -# YJIT excels at optimizing method chains -class QueryBuilder - def where(conditions) - @conditions = (@conditions || []) + [conditions] - self - end - - def order(field) - @order = field - self - end - - def limit(count) - @limit = count - self - end - - def to_sql - # Complex SQL generation - build_sql_from_conditions - end -end - -# This pattern shows 45% improvement with YJIT -User.query.where(active: true).order(:name).limit(50).to_sql -``` - -**Numeric Computations:** -```ruby -# Financial calculations benefit significantly -class PriceCalculator - def calculate_total(line_items) - subtotal = line_items.sum { |item| item.price * item.quantity } - tax = calculate_tax(subtotal) - shipping = calculate_shipping(line_items) - - subtotal + tax + shipping - end - - private - - def calculate_tax(amount) - amount * tax_rate - end - - def calculate_shipping(items) - total_weight = items.sum(&:weight) - base_rate + (total_weight * weight_rate) - end -end - -# 35% performance improvement with YJIT -``` - -**String Processing and Templating:** -```ruby -# ERB template compilation and rendering -class TemplateRenderer - def render(template, locals = {}) - compiled = compile_template(template) - evaluate_template(compiled, locals) - end - - private - - def compile_template(source) - # Template compilation logic - ERB.new(source).src - end - - def evaluate_template(compiled, locals) - # Template evaluation with locals - eval(compiled, binding_with_locals(locals)) - end -end - -# Template rendering shows 28% improvement -``` - -### Avoiding Deoptimization Triggers - -YJIT's performance depends on stable assumptions about code behavior. Certain patterns can trigger deoptimization, forcing fallback to interpreted execution: - -**Avoid Dynamic Method Definition in Hot Paths:** -```ruby -# BAD: Causes frequent deoptimization -class DynamicModel - def self.create_accessor(name) - define_method(name) do - instance_variable_get("@#{name}") - end - end -end - -# GOOD: Define methods statically -class StaticModel - ATTRIBUTES = [:name, :email, :age].freeze - - ATTRIBUTES.each do |attr| - define_method(attr) do - instance_variable_get("@#{attr}") - end - end -end -``` - -**Maintain Consistent Object Shapes:** -```ruby -# BAD: Changing object shape defeats YJIT optimizations -class User - def initialize(attrs = {}) - @name = attrs[:name] - @email = attrs[:email] - - # Conditionally adding instance variables - @admin = attrs[:admin] if attrs.key?(:admin) - @created_at = attrs[:created_at] if attrs.key?(:created_at) - end -end - -# GOOD: Consistent object shape -class User - def initialize(attrs = {}) - @name = attrs[:name] - @email = attrs[:email] - @admin = attrs[:admin] || false - @created_at = attrs[:created_at] || Time.current - end -end -``` - -### Monitoring YJIT Statistics - -Ruby 3.4 provides comprehensive statistics for monitoring YJIT performance and identifying optimization opportunities: - -```ruby -# Comprehensive YJIT monitoring -class YjitMonitor - def self.report - return "YJIT not enabled" unless RubyVM::YJIT.enabled? - - stats = RubyVM::YJIT.runtime_stats - - { - compilation_stats: { - compiled_blocks: stats[:compiled_block_count], - compilation_ratio: compilation_ratio(stats), - invalidation_rate: invalidation_rate(stats) - }, - memory_stats: { - exec_memory_mb: stats[:exec_mem_size] / 1024 / 1024, - memory_efficiency: memory_efficiency(stats) - }, - performance_indicators: { - exit_rate: exit_rate(stats), - inline_cache_hits: cache_hit_rate(stats) - } - } - end - - private - - def self.compilation_ratio(stats) - return 0 if stats[:iseq_count] == 0 - (stats[:compiled_iseq_count].to_f / stats[:iseq_count] * 100).round(2) - end - - def self.invalidation_rate(stats) - return 0 if stats[:compiled_block_count] == 0 - (stats[:invalidation_count].to_f / stats[:compiled_block_count] * 100).round(2) - end - - def self.memory_efficiency(stats) - return 0 if stats[:exec_mem_size] == 0 - (stats[:compiled_block_count].to_f / stats[:exec_mem_size] * 1024).round(2) - end - - def self.exit_rate(stats) - total_exits = stats[:side_exit_count] || 0 - total_calls = stats[:compiled_method_calls] || 1 - (total_exits.to_f / total_calls * 100).round(2) - end - - def self.cache_hit_rate(stats) - hits = stats[:inline_cache_hits] || 0 - total = hits + (stats[:inline_cache_misses] || 0) - return 100.0 if total == 0 - (hits.to_f / total * 100).round(2) - end -end - -# Usage in Rails initializer -Rails.application.configure do - # Log YJIT stats every hour in production - if Rails.env.production? && RubyVM::YJIT.enabled? - Thread.new do - loop do - sleep 3600 # 1 hour - Rails.logger.info "YJIT Stats: #{YjitMonitor.report}" - end - end - end -end -``` - -### Profiling and Analysis Tools - -**Integration with Ruby Profilers:** -```ruby -# stackprof integration for YJIT analysis -require 'stackprof' - -StackProf.run(mode: :cpu, out: 'yjit_profile.dump') do - # Your application code - perform_complex_operation -end - -# Analyze the profile -profile = StackProf.load('yjit_profile.dump') -puts "Methods spending most time in interpretation:" -profile.data[:frames].select { |_, frame| - frame[:name].include?('YJIT') || frame[:file]&.include?('yjit') -}.each { |id, frame| puts "#{frame[:name]}: #{frame[:total_samples]} samples" } -``` - -**Custom YJIT Profiler:** -```ruby -class YjitProfiler - def self.profile(duration: 60) - return unless RubyVM::YJIT.enabled? - - start_stats = RubyVM::YJIT.runtime_stats - start_time = Time.now - - sleep duration - - end_stats = RubyVM::YJIT.runtime_stats - end_time = Time.now - - analyze_stats(start_stats, end_stats, end_time - start_time) - end - - private - - def self.analyze_stats(start_stats, end_stats, duration) - { - duration: duration, - blocks_compiled: end_stats[:compiled_block_count] - start_stats[:compiled_block_count], - compilation_rate: (end_stats[:compiled_block_count] - start_stats[:compiled_block_count]) / duration, - memory_growth: end_stats[:exec_mem_size] - start_stats[:exec_mem_size], - invalidations: end_stats[:invalidation_count] - start_stats[:invalidation_count] - } - end -end -``` - -## Production Case Studies - -Real-world deployments provide the most valuable insights into YJIT's practical benefits and challenges. These case studies represent diverse application types and deployment scenarios. - -### Shopify's Production Experience - -Shopify, the primary developer of YJIT, has run it in production since Ruby 3.1 with continuous improvements through Ruby 3.4: - -**Implementation Timeline:** -- **Q2 2022**: Initial production deployment on 5% of traffic -- **Q4 2022**: Full production rollout across all Ruby services -- **Q2 2023**: Ruby 3.3 upgrade with 15% additional performance gains -- **Q1 2024**: Ruby 3.4 deployment showing 22% improvement over 3.3 - -**Key Metrics:** -```ruby -# Shopify's internal YJIT metrics (approximated) -production_metrics = { - response_time_improvement: 18, # percent faster - memory_overhead: 12, # percent increase - cpu_utilization_reduction: 15, # percent decrease - compilation_ratio: 73, # percent of hot code compiled - uptime_impact: 0, # zero downtime deployments - cost_savings_annual: 2_400_000 # USD from reduced infrastructure -} -``` - -**Shopify's Optimization Discoveries:** -1. **Precompilation Strategy**: Using a warm-up period during deployment to pre-compile hot paths -2. **Memory Management**: Implementing custom memory limits per service type -3. **Monitoring Integration**: Custom metrics integration with their observability stack - -```ruby -# Shopify-inspired warm-up strategy -class YjitWarmup - def self.perform - return unless RubyVM::YJIT.enabled? - - # Warm up critical paths - warm_up_product_catalog - warm_up_order_processing - warm_up_user_authentication - - Rails.logger.info "YJIT warmup completed: #{RubyVM::YJIT.runtime_stats[:compiled_block_count]} blocks compiled" - end - - private - - def self.warm_up_product_catalog - # Simulate product browsing patterns - 50.times do |i| - Product.featured.limit(20).includes(:variants, :images).to_a - Product.find(i % 1000 + 1) rescue nil - end - end - - def self.warm_up_order_processing - # Exercise order calculation logic - sample_cart = Cart.new - 10.times { sample_cart.add_sample_product } - sample_cart.calculate_total - end -end -``` - -### GitHub's Deployment Results - -GitHub's adoption of YJIT across their Ruby services demonstrates enterprise-scale benefits: - -**Service Coverage:** -- **github.com main application**: 24% response time improvement -- **API services**: 31% throughput increase -- **Background job processors**: 19% processing speed improvement -- **Internal tools and dashboards**: 15% average performance gain - -**Implementation Challenges and Solutions:** -```ruby -# GitHub's gradual rollout approach -class FeatureFlaggedYjit - def self.enable_for_service(service_name, percentage = 10) - if Rails.cache.read("yjit_enabled_#{service_name}") == "true" - ENV['RUBY_YJIT_ENABLE'] = '1' if eligible_for_yjit?(percentage) - end - end - - private - - def self.eligible_for_yjit?(percentage) - Digest::MD5.hexdigest("#{Process.pid}#{Time.current.hour}").to_i(16) % 100 < percentage - end -end - -# Usage in Rails application -GitHub::FeatureFlaggedYjit.enable_for_service('web', ENV.fetch('YJIT_ROLLOUT_PERCENTAGE', 10).to_i) -``` - -### JetThoughts Client Implementations - -Our consulting practice has successfully deployed YJIT across various client applications, providing insights into different deployment scenarios: - -**E-learning Platform (Ruby on Rails):** -- **Application**: Online course platform with video streaming -- **Performance Gain**: 27% improvement in course content rendering -- **Challenge**: Memory usage spikes during peak traffic -- **Solution**: Implemented adaptive memory limits and load balancing - -```ruby -# E-learning platform optimization -class AdaptiveYjitConfig - def self.configure_for_load - current_load = SystemMetrics.cpu_usage - memory_available = SystemMetrics.available_memory_mb - - if current_load > 80 || memory_available < 512 - # Conservative settings under high load - ENV['RUBY_YJIT_MIN_CALLS'] = '50' - ENV['RUBY_YJIT_EXEC_MEM_SIZE'] = '128' - else - # Aggressive optimization under normal load - ENV['RUBY_YJIT_MIN_CALLS'] = '20' - ENV['RUBY_YJIT_EXEC_MEM_SIZE'] = '512' - end - end -end -``` - -**Financial Services API (Sinatra-based):** -- **Application**: Real-time trading data processing -- **Performance Gain**: 41% improvement in numeric calculations -- **Challenge**: Strict latency requirements (sub-10ms responses) -- **Solution**: Custom JIT warm-up procedures and latency monitoring - -```ruby -# Financial services latency optimization -class LowLatencyYjit - def self.configure - # Minimize JIT compilation during request handling - ENV['RUBY_YJIT_MIN_CALLS'] = '10' # Compile hot paths quickly - ENV['RUBY_YJIT_STATS'] = '0' # Disable stats collection overhead - - # Pre-compile critical calculation paths - warmup_pricing_engine - warmup_risk_calculations - end - - def self.monitor_latency - Thread.new do - loop do - sleep 10 - stats = RubyVM::YJIT.runtime_stats - if stats[:side_exit_count] > threshold - alert_high_deoptimization_rate(stats) - end - end - end - end -end -``` - -**Content Management System (Rails CMS):** -- **Application**: High-traffic publishing platform -- **Performance Gain**: 19% improvement in page generation -- **Challenge**: Dynamic content with frequent cache invalidation -- **Solution**: YJIT-aware caching strategy and content precompilation - -## Troubleshooting and Monitoring - -Successful YJIT deployment requires robust monitoring and systematic troubleshooting approaches. Common issues and their solutions have emerged from production deployments. - -### Common Issues and Solutions - -**Memory Leaks in Long-Running Processes:** -```ruby -# Symptom: Memory usage grows continuously over time -# Cause: Accumulated compiled code without garbage collection -# Solution: Enable code GC and implement memory monitoring - -class MemoryMonitor - def self.check_and_rotate_if_needed - current_memory = memory_usage_mb - - if current_memory > MEMORY_THRESHOLD_MB - Rails.logger.warn "High memory usage detected: #{current_memory}MB" - - # Force code garbage collection - if RubyVM::YJIT.enabled? && ENV['RUBY_YJIT_CODE_GC'] == '1' - before_stats = RubyVM::YJIT.runtime_stats - GC.start - after_stats = RubyVM::YJIT.runtime_stats - - freed_blocks = before_stats[:compiled_block_count] - after_stats[:compiled_block_count] - Rails.logger.info "YJIT GC freed #{freed_blocks} compiled blocks" - end - end - end - - private - - def self.memory_usage_mb - `ps -o rss= -p #{Process.pid}`.to_i / 1024 - end -end -``` - -**High Deoptimization Rates:** -```ruby -# Symptom: Performance degradation over time -# Cause: Code patterns that trigger frequent side exits -# Solution: Identify and refactor problematic patterns - -class DeoptimizationAnalyzer - def self.analyze_exits - stats = RubyVM::YJIT.runtime_stats - exit_rate = stats[:side_exit_count].to_f / stats[:compiled_method_calls] - - if exit_rate > 0.1 # 10% exit rate threshold - Rails.logger.warn "High deoptimization rate: #{(exit_rate * 100).round(2)}%" - - # Log common deoptimization causes - log_exit_reasons(stats) - - # Suggest optimizations - suggest_optimizations - end - end - - private - - def self.log_exit_reasons(stats) - Rails.logger.info "Exit reasons analysis:" - Rails.logger.info "- Type mismatches: #{stats[:type_mismatch_exits] || 0}" - Rails.logger.info "- Method redefinition: #{stats[:method_redef_exits] || 0}" - Rails.logger.info "- Constant invalidation: #{stats[:const_inval_exits] || 0}" - end -end -``` - -**Compilation Threshold Tuning:** -```ruby -# Issue: Either too much compilation overhead or missed optimization opportunities -# Solution: Dynamic threshold adjustment based on application characteristics - -class AdaptiveThreshold - def self.adjust_based_on_metrics - stats = RubyVM::YJIT.runtime_stats - compilation_ratio = stats[:compiled_iseq_count].to_f / stats[:iseq_count] - - current_threshold = ENV['RUBY_YJIT_MIN_CALLS']&.to_i || 30 - - if compilation_ratio < 0.1 # Less than 10% compiled - new_threshold = [current_threshold - 5, 10].max - ENV['RUBY_YJIT_MIN_CALLS'] = new_threshold.to_s - Rails.logger.info "Reduced YJIT threshold to #{new_threshold}" - elsif compilation_ratio > 0.8 # More than 80% compiled - new_threshold = current_threshold + 10 - ENV['RUBY_YJIT_MIN_CALLS'] = new_threshold.to_s - Rails.logger.info "Increased YJIT threshold to #{new_threshold}" - end - end -end -``` - -### Monitoring YJIT in Production - -**Comprehensive Monitoring Dashboard:** -```ruby -class YjitDashboard - def self.metrics - return nil unless RubyVM::YJIT.enabled? - - stats = RubyVM::YJIT.runtime_stats - - { - compilation_metrics: { - blocks_compiled: stats[:compiled_block_count], - methods_compiled: stats[:compiled_iseq_count], - compilation_ratio: compilation_percentage(stats), - compilation_rate_per_minute: compilation_rate(stats) - }, - memory_metrics: { - exec_memory_mb: (stats[:exec_mem_size] || 0) / 1024 / 1024, - memory_utilization: memory_utilization_percentage(stats), - average_block_size: average_compiled_block_size(stats) - }, - performance_metrics: { - exit_percentage: exit_percentage(stats), - cache_hit_rate: cache_hit_percentage(stats), - invalidation_rate: invalidation_percentage(stats) - }, - stability_metrics: { - compilation_errors: stats[:compilation_errors] || 0, - gc_freed_blocks: stats[:gc_freed_blocks] || 0, - uptime_hours: Process.clock_gettime(Process::CLOCK_UPTIME) / 3600 - } - } - end - - def self.health_score - metrics = self.metrics - return 100 unless metrics - - score = 100 - score -= 20 if metrics[:compilation_metrics][:compilation_ratio] < 30 - score -= 15 if metrics[:performance_metrics][:exit_percentage] > 15 - score -= 10 if metrics[:memory_metrics][:memory_utilization] > 90 - score -= 25 if metrics[:stability_metrics][:compilation_errors] > 0 - - [score, 0].max - end -end -``` - -**Alerting Configuration:** -```ruby -# Integration with monitoring systems (New Relic, DataDog, etc.) -class YjitAlerting - def self.check_thresholds - metrics = YjitDashboard.metrics - return unless metrics - - # Alert on high memory usage - if metrics[:memory_metrics][:memory_utilization] > 85 - alert(:high_memory, "YJIT memory utilization: #{metrics[:memory_metrics][:memory_utilization]}%") - end - - # Alert on excessive deoptimization - if metrics[:performance_metrics][:exit_percentage] > 20 - alert(:high_exits, "YJIT exit rate: #{metrics[:performance_metrics][:exit_percentage]}%") - end - - # Alert on compilation failures - if metrics[:stability_metrics][:compilation_errors] > 5 - alert(:compilation_errors, "YJIT compilation errors: #{metrics[:stability_metrics][:compilation_errors]}") - end - end - - private - - def self.alert(type, message) - # Integration with your alerting system - AlertManager.send_alert( - severity: severity_for_type(type), - title: "YJIT #{type.to_s.humanize}", - message: message, - tags: ['yjit', 'performance', type.to_s] - ) - end -end -``` - -### Debug Tools and Techniques - -**YJIT Debug Output Analysis:** -```ruby -# Enable detailed debugging (development only) -ENV['RUBY_YJIT_STATS'] = '1' -ENV['RUBY_DEBUG'] = '1' - -class YjitDebugger - def self.dump_compilation_trace - return unless RubyVM::YJIT.enabled? - - # Capture compilation events - trace = TracePoint.new(:script_compiled) do |tp| - if tp.instruction_sequence.to_a[4][:yjit_compiled] - puts "YJIT compiled: #{tp.path}:#{tp.lineno} #{tp.method_id}" - end - end - - trace.enable - yield if block_given? - trace.disable - end - - def self.analyze_hot_methods(threshold: 100) - return unless RubyVM::YJIT.enabled? - - # Use Ruby's built-in profiling to identify hot methods - profile = RubyVM::InstructionSequence.compile_option = { - trace_instruction: true - } - - # Implementation would track method call frequency - # and correlate with YJIT compilation decisions - end -end -``` - -**Performance Regression Detection:** -```ruby -class RegressionDetector - BASELINE_FILE = 'tmp/yjit_baseline.json' - - def self.establish_baseline - baseline = { - timestamp: Time.current.iso8601, - metrics: YjitDashboard.metrics, - ruby_version: RUBY_VERSION, - yjit_enabled: RubyVM::YJIT.enabled? - } - - File.write(BASELINE_FILE, JSON.pretty_generate(baseline)) - Rails.logger.info "YJIT baseline established" - end - - def self.check_for_regression - return unless File.exist?(BASELINE_FILE) - - baseline = JSON.parse(File.read(BASELINE_FILE)) - current = YjitDashboard.metrics - - regression_detected = false - - # Check key performance indicators - if current[:compilation_metrics][:compilation_ratio] < baseline.dig('metrics', 'compilation_metrics', 'compilation_ratio') * 0.9 - Rails.logger.warn "Compilation ratio regression detected" - regression_detected = true - end - - if current[:performance_metrics][:exit_percentage] > baseline.dig('metrics', 'performance_metrics', 'exit_percentage') * 1.5 - Rails.logger.warn "Exit rate regression detected" - regression_detected = true - end - - alert_regression if regression_detected - end -end -``` - -## Future of YJIT - -YJIT's roadmap extends well beyond Ruby 3.4, with ambitious performance targets and architectural improvements planned for future releases. - -### Roadmap for Ruby 3.5 and Beyond - -**Ruby 3.5 (Expected Q4 2025):** -The next major release will focus on compilation efficiency and expanded optimization coverage: - -- **Improved ARM64 Support**: Native compilation optimizations for Apple Silicon and cloud ARM instances -- **Cross-Method Inlining**: Advanced inlining across method boundaries for 15-20% additional performance -- **Specialized Object Layouts**: Memory layout optimizations for common object patterns -- **Background Compilation**: JIT compilation in separate threads to reduce main thread blocking - -**Ruby 3.6 (Projected Q4 2026):** -Longer-term improvements target fundamental performance bottlenecks: - -- **Adaptive Garbage Collection**: YJIT-aware GC scheduling to minimize compilation invalidation -- **Native Extension Integration**: Direct compilation of C extension hot paths -- **Profile-Guided Optimization**: Using production profiling data to inform compilation decisions -- **WASM Backend**: Experimental WebAssembly target for edge computing scenarios - -### Community Contributions - -The Ruby community has embraced YJIT development with significant contributions: - -**Active Contribution Areas:** -```ruby -# Example community contribution: YJIT benchmarking framework -class CommunityBenchmark - STANDARD_BENCHMARKS = [ - 'optcarrot', # NES emulator benchmark - 'liquid', # Template engine performance - 'railsbench', # Rails application simulation - 'discourse', # Forum software performance - 'sinatra_bench' # Microframework benchmark - ].freeze - - def self.run_all - STANDARD_BENCHMARKS.each do |benchmark| - puts "Running #{benchmark}..." - result = run_benchmark(benchmark) - - store_result(benchmark, result) - compare_with_baseline(benchmark, result) - end - end - - private - - def self.run_benchmark(name) - # Integration with ruby/ruby benchmark suite - `ruby benchmark/#{name}.rb --yjit` - end -end -``` - -**Contributing to YJIT Development:** -1. **Performance Testing**: Running benchmarks on diverse workloads -2. **Bug Reports**: Identifying edge cases and deoptimization triggers -3. **Documentation**: Improving guides and best practices -4. **Tooling**: Building monitoring and analysis tools - -### Performance Targets - -The YJIT team has established ambitious performance targets for future releases: - -**Ruby 3.5 Targets:** -- **50% performance improvement** over Ruby 3.0 for CPU-intensive workloads -- **35% improvement** for typical Rails applications -- **Memory overhead under 15%** compared to interpreted Ruby -- **Compilation time under 5%** of total execution time - -**Ruby 4.0 Vision (Projected 2028):** -- **2-3x performance improvement** over Ruby 3.0 baseline -- **Native-level performance** for numeric computations -- **Zero-overhead abstractions** for common Ruby idioms -- **Seamless C extension integration** with shared optimization - -```ruby -# Projected Ruby 4.0 YJIT capabilities -class FutureRuby - # Native-speed numeric operations - def fast_math(array) - # This would compile to vectorized assembly - array.map { |x| Math.sqrt(x * 2.5) }.sum - end - - # Zero-cost abstractions - def zero_cost_iteration(collection) - # Iterator overhead eliminated at compile time - collection.select(&:active?).map(&:to_hash).reduce(:merge) - end -end -``` - -**Getting Involved:** -- **Ruby Core**: Contribute to ruby/ruby repository on GitHub -- **YJIT Specific**: Join discussions in the Ruby YJIT Slack channel -- **Benchmarking**: Submit real-world application benchmarks -- **Testing**: Report compatibility issues and performance regressions - -The future of YJIT represents Ruby's commitment to performance without sacrificing the language's expressiveness and developer happiness. As the ecosystem matures, YJIT will become an essential tool for scaling Ruby applications to meet modern performance demands. - -## Production Deployment Checklist - -Before deploying YJIT to production, ensure you've completed these essential steps: - -**Pre-Deployment:** -- [ ] Verify Ruby 3.4+ with YJIT support compiled -- [ ] Test with production-like workloads in staging -- [ ] Establish performance baselines -- [ ] Configure monitoring and alerting -- [ ] Plan rollback procedures - -**Deployment Configuration:** -- [ ] Set appropriate memory limits (`RUBY_YJIT_EXEC_MEM_SIZE`) -- [ ] Configure compilation thresholds (`RUBY_YJIT_MIN_CALLS`) -- [ ] Enable code garbage collection (`RUBY_YJIT_CODE_GC`) -- [ ] Implement gradual rollout strategy -- [ ] Set up YJIT-specific metrics collection - -**Post-Deployment Monitoring:** -- [ ] Track compilation ratios and performance metrics -- [ ] Monitor memory usage patterns -- [ ] Watch for deoptimization issues -- [ ] Validate performance improvements -- [ ] Document lessons learned - -Ruby 3.4's YJIT represents a significant milestone in Ruby performance optimization. With proper configuration, monitoring, and optimization strategies, you can achieve substantial performance improvements while maintaining Ruby's developer-friendly characteristics. The investment in YJIT adoption pays dividends through reduced infrastructure costs, improved user experience, and future-proofed performance as the technology continues to evolve. - -Implementing YJIT successfully requires deep Ruby expertise and careful performance optimization strategies. Our [expert Ruby on Rails development team](/services/app-web-development/) has extensive experience with Ruby performance optimization, YJIT deployment, and production monitoring, helping companies achieve significant performance gains while maintaining application stability and reducing infrastructure costs. - -Start with small, low-risk deployments, monitor carefully, and gradually expand YJIT usage as you build confidence in its stability and performance benefits for your specific application patterns. \ No newline at end of file diff --git a/content/blog/fibers-async-ruby-llm-streaming-rails/cover.png b/content/blog/fibers-async-ruby-llm-streaming-rails/cover.png new file mode 100644 index 000000000..ab6047474 Binary files /dev/null and b/content/blog/fibers-async-ruby-llm-streaming-rails/cover.png differ diff --git a/content/blog/fibers-async-ruby-llm-streaming-rails/index.md b/content/blog/fibers-async-ruby-llm-streaming-rails/index.md new file mode 100644 index 000000000..628dfc861 --- /dev/null +++ b/content/blog/fibers-async-ruby-llm-streaming-rails/index.md @@ -0,0 +1,141 @@ +--- +title: "Ruby Fibers for LLM Streaming in Rails" +description: "LLM responses stream for 30 seconds while a Rails thread waits. The arithmetic of Puma threads vs open SSE streams, and when Falcon's fibers earn the switch." +date: 2026-08-20 +created_at: "2026-08-20T10:00:00Z" +draft: false +tags: ["ruby", "rails", "performance", "async", "llm"] +canonical_url: "https://jetthoughts.com/blog/fibers-async-ruby-llm-streaming-rails/" +cover_image: cover.png +slug: "fibers-async-ruby-llm-streaming-rails" +metatags: + image: cover.png + og_title: "Ruby Fibers for LLM Streaming in Rails" + og_description: "LLM responses stream for 30 seconds while a Rails thread waits. The arithmetic of Puma threads vs SSE streams, and when Falcon's fibers earn the switch." + twitter_title: "Ruby Fibers for LLM Streaming in Rails" + twitter_description: "The arithmetic of Puma threads vs open SSE streams, and when Falcon's fibers earn the switch." +--- + +A streamed LLM reply takes somewhere between 10 and 60 seconds to finish, and for the whole of that time the Rails thread serving it does almost nothing. It holds the connection open, writes a token whenever the provider sends one, and waits. Ruby fibers exist for exactly this shape of work, which is why LLM streaming in Rails keeps pulling teams toward the [async](https://github.com/socketry/async) gem and Falcon. + +Before you swap web servers, though, it's worth doing the arithmetic. On plenty of apps the boring answer wins. + +## Three threads, thirty-second streams + +A new Rails 8.1 app ships Puma with 3 threads per worker - a default [Rails cut from 5 back in 7.2](https://guides.rubyonrails.org/7_2_release_notes.html), because on CPU-bound request cycles more threads mostly meant more GVL contention and worse latency. For classic CRUD traffic that was the right call. A request that finishes in 80ms releases its thread 12 times a second, so 3 threads go a long way. + +A chat endpoint breaks that assumption. Stream a reply for 30 seconds and the thread is gone for 30 seconds. + +Run two Puma workers with the default 3 threads each and you can hold 6 open streams, total, across the whole app. User number seven gets a spinner until someone else's answer finishes. Scale that to 100 concurrent streams and you need 100 threads, which at 3 per worker means 34 worker processes, each one a full copy of your Rails app doing nothing but babysitting sockets. + +You can redo that arithmetic for your own app in a minute; the only inputs are the Puma settings you already have and an honest estimate of peak concurrent chats. + +This is not a CPU problem: [YJIT speeds up the code your CPU actually executes](/blog/ruby-3-4-yjit-performance-guide/), and here the thread isn't executing anything. Waiting is the workload. + +## A parked fiber costs kilobytes, not a thread + +A fiber pauses and resumes cooperatively; thousands fit in one thread. Since Ruby 3.0, the [fiber scheduler interface](https://docs.ruby-lang.org/en/3.4/Fiber/Scheduler.html) lets a scheduler hook the moments where Ruby would block: `io_wait`, `kernel_sleep`, DNS resolution. + +When a fiber hits a read that isn't ready, the scheduler parks it, runs other fibers, and comes back once the socket has data. + +For an LLM stream, that changes the whole cost model. The 30 seconds of waiting becomes a parked fiber, a few kilobytes of state in a table, instead of an occupied thread. + +## Falcon runs each request in a fiber + +[Falcon](https://github.com/socketry/falcon) is a Rack-compatible HTTP server built on the async gem by Samuel Williams. Its README states the model in one line: "each request is executed within a lightweight fiber and can block on up-stream requests without stalling the entire server process." The async gem underneath it is [built for thousands of clients per process](https://github.com/socketry/async). + +Blocking on an upstream request is the entire job of an LLM proxy endpoint. + +Your action opens a connection to the provider, reads chunks for half a minute, and relays them, and under Falcon each of those open streams is a parked fiber. The hundred streams that needed 34 Puma workers a section ago fit in a single process. + +The production setup is its own post. We've covered [Falcon's benchmarks, config, and migration path](/blog/falcon-web-server-async-ruby-production/) there; this one stays on the LLM-shaped question of what changes in your code and when the switch is worth it. + +## What changes in your Rails code + +Less than you'd expect. The fiber scheduler hooks Ruby's own I/O, so `Net::HTTP`, and everything built on it, yields automatically inside Falcon. The `pg` driver has cooperated with the fiber scheduler since version 1.3. Your models, controllers, and service objects don't know the difference. + +Two settings matter: isolate per-request state by fiber instead of by thread, and raise `pool:` in `config/database.yml` to match the concurrency you actually expect. The first one is a single line: + +```ruby +# config/application.rb +config.active_support.isolation_level = :fiber +``` + +Here's a streaming endpoint using [RubyLLM](https://rubyllm.com/streaming), whose `ask` method yields chunks as they arrive: + +```ruby +class MessagesController < ApplicationController + include ActionController::Live + + def create + response.headers["Content-Type"] = "text/event-stream" + sse = SSE.new(response.stream, event: "delta") + + chat = Chat.find(params[:chat_id]) + chat.to_llm.ask(params[:content]) do |chunk| + sse.write({ text: chunk.content }) if chunk.content + end + ensure + sse&.close + end +end +``` + +The same controller runs on both servers. On Puma, [`ActionController::Live` moves the action onto a separate thread](https://api.rubyonrails.org/classes/ActionController/Live.html) from a `Concurrent::CachedThreadPool`, and the docs carry two warnings worth reading twice: you "must call close on your stream" or the socket can stay open forever, and the default `Rack::ETag` middleware "will buffer your response" - set an `ETag` or `Last-Modified` header yourself to opt out of that buffering. + +One honest caveat about the code above: `Live` spawns its worker thread unconditionally, on Falcon too. The request stops pinning a server thread, but each open stream still costs a pool thread until you drop `ActionController::Live` and write the response body directly - which is the idiomatic shape under Falcon, since the server itself already streams. + +Fibers in a process also share one thread: a CPU-heavy stretch of Ruby, or a C extension doing its own blocking I/O outside Ruby's hooks, stalls every fiber in that worker until it returns. + +## Rate limiting the upstream calls + +Multiplexing hundreds of streams creates a new failure mode: hundreds of simultaneous calls against your OpenAI or Anthropic account, and a wall of 429 responses. + +The async gem ships `Async::Semaphore` for exactly this: + +```ruby +LLM_LIMIT = Async::Semaphore.new(5) + +def stream_reply(chat, content, &render) + LLM_LIMIT.acquire do + chat.to_llm.ask(content, &render) + end +end +``` + +Five requests run against the provider at once. Number six parks, costing nothing, until a slot frees up. Compare that with thread-based Rails, where the queueing happens invisibly in Puma's backlog and you can't see or tune it from application code. + +## When Puma is fine + +```mermaid +flowchart TD + A["Peak concurrent
LLM streams"] --> B{"Under workers
x threads?"} + B --> C["Yes: Puma is fine,
maybe raise threads"] + B --> D["No: past the ceiling"] + D --> E["Background job +
Turbo broadcast,
no held request"] + D --> F["Falcon:
a fiber per stream"] +``` + +Do the division before the migration. Your ceiling is workers times threads, so 3 workers at 3 threads holds 9 concurrent streams, and if your product peaks at 5 people chatting at once, Puma serves them today with zero new operational surface. Raising `RAILS_MAX_THREADS` on an I/O-parked endpoint buys more headroom cheaply, since [Puma's own docs note that blocking I/O is the case where extra MRI threads really do run in parallel](https://github.com/puma/puma). + +Or sidestep the question entirely: move the LLM call into a background job and broadcast chunks over Turbo Streams, so no HTTP request stays open at all. The job is a dozen lines - [the RubyLLM post shows it in full](/blog/rubyllm-rails-getting-started/#streaming-into-a-turbo-view). + +That pattern runs happily on Puma plus [Solid Queue](/blog/rails-8-solid-queue-migration-guide/), and it's where we start on most client apps because it also survives deploys and page reloads better than a raw SSE socket. Its cost is a job worker held for the stream's duration, so the arithmetic moves to your job concurrency instead of your web threads. + +Falcon's costs are real too. Some gems assume thread-local state and thread-sized connection pools, and your team's Puma debugging instincts transfer only partially. Reach for it when concurrent streams are the product, the way they are in the [agent and RAG apps we've been building in Ruby](/blog/getting-started-langchain-ruby-complete-guide/), rather than because fibers sound modern. + +## Where to start + +Measure your peak concurrent streams for a week. If the number stays under workers times threads, raise a thread count and move on. Past that ceiling, prototype one streaming endpoint on Falcon in staging with `isolation_level = :fiber` set, and watch the database pool, since that's the first thing the extra concurrency exhausts. + +If you're adding LLM features to a Rails app and want someone who has shipped this stack in production to look at your traffic profile first, [our Rails team does that assessment](/services/app-web-development/) before any migration work starts. + +**Further reading:** + +- [Falcon](https://github.com/socketry/falcon) - the fiber-per-request server itself +- [async](https://github.com/socketry/async) - the concurrency framework underneath it +- [Fiber::Scheduler docs](https://docs.ruby-lang.org/en/3.4/Fiber/Scheduler.html) - the interface that makes blocking I/O yield +- [ActionController::Live API docs](https://api.rubyonrails.org/classes/ActionController/Live.html) - both warnings quoted above, in context +- [RubyLLM async guide](https://rubyllm.com/async/) - semaphore rate limiting and fiber-safe usage + + diff --git a/content/blog/fibers-async-ruby-llm-streaming-rails/mermaid-f6f87f6e.svg b/content/blog/fibers-async-ruby-llm-streaming-rails/mermaid-f6f87f6e.svg new file mode 100644 index 000000000..e13ce8749 --- /dev/null +++ b/content/blog/fibers-async-ruby-llm-streaming-rails/mermaid-f6f87f6e.svg @@ -0,0 +1 @@ +

Peak concurrent
LLM streams

Under workers
x threads?

Yes: Puma is fine,
maybe raise threads

No: past the ceiling

Background job +
Turbo broadcast,
no held request

Falcon:
a fiber per stream

\ No newline at end of file diff --git a/content/blog/multi-agent-llm-rails-rubyllm/cover.png b/content/blog/multi-agent-llm-rails-rubyllm/cover.png new file mode 100644 index 000000000..73eabe2d3 Binary files /dev/null and b/content/blog/multi-agent-llm-rails-rubyllm/cover.png differ diff --git a/content/blog/multi-agent-llm-rails-rubyllm/index.md b/content/blog/multi-agent-llm-rails-rubyllm/index.md new file mode 100644 index 000000000..91c5c4807 --- /dev/null +++ b/content/blog/multi-agent-llm-rails-rubyllm/index.md @@ -0,0 +1,184 @@ +--- +title: "Multi-Agent LLM Pipelines in Rails" +description: "Our production multi-agent LLM pipeline in Rails: eight RubyLLM agents on a 21-line base class, a 16-line loop, and the deploy that starved the database pool." +date: 2026-08-20 +created_at: 2026-08-20T10:00:00Z +draft: false +author: "JetThoughts Team" +slug: "multi-agent-llm-rails-rubyllm" +keywords: "multi-agent llm rails, rubyllm agents, llm pipeline ruby, rails ai agents" +tags: ["ruby", "rails", "ai", "rubyllm", "llm", "agents"] +canonical_url: "https://jetthoughts.com/blog/multi-agent-llm-rails-rubyllm/" +cover_image: cover.png +cover_image_alt: "Dark blueprint-style cover reading Multi-Agent LLM Pipelines in Rails, with a low-poly ruby gem and chips for a 37-line framework, eight agents on one base class, and Ruby-summed scoring." +metatags: + image: cover.png + og_title: "Multi-Agent LLM Pipelines in Rails" + og_description: "Our production multi-agent LLM pipeline in Rails: eight RubyLLM agents on a 21-line base class, a 16-line loop, and the deploy that starved the database pool." + twitter_title: "Multi-Agent LLM Pipelines in Rails" + twitter_description: "Eight RubyLLM agents on a 21-line base class, a 16-line loop, and the outage where fifteen fibers starved a pool of ten." +--- + +The multi-agent ecosystem wants to sell you an orchestration layer. Agent graphs, message buses, and somewhere a YAML file describing your "crew". We run a multi-agent LLM pipeline in Rails, in production, for a talent-matching product, and the entire framework underneath its eight agents is 37 lines of our own code on top of [RubyLLM](https://rubyllm.com/agents/): a 21-line base class and a 16-line loop. + +The product matches candidates to open roles. One agent expands the search query, another reranks, a third scores each candidate against the role, and a reflector decides whether another round is worth running. Each of them is a plain Ruby class. + +If you want the gem's chat and tool basics first, [our RubyLLM getting-started post](/blog/rubyllm-rails-getting-started/) covers them. This one stays a layer up: how the agents compose, and what broke when they did. + +## The whole thing is 37 lines + +RubyLLM ships an [`Agent` class](https://rubyllm.com/agents/): instructions, an optional schema, tools, and `ask`. Our base class adds exactly one idea to it - configuration the subclasses can override: + +```ruby +class AgentBase < RubyLLM::Agent + class_attribute :model_name, default: AgentConfig.default_model + + model { self.class.model_name } + temperature 0.2 +end +``` + +Twenty-one lines, in the real file. + +The lazy block is the point: `model` accepts a block that resolves at call time, so pointing an agent at a different model is a class-attribute assignment that takes effect without reloading anything. Temperature gets no such block - the gem treats it as a static value, so subclasses that need a different one declare it directly. + +Eight agents subclass it - a scorer, a reflector, a query expander, a scrubber, four more. Every one has the same shape: an instructions heredoc, a [Schematist](https://rubygems.org/gems/schematist) schema for structured output, and one public method that builds a prompt and returns `ask(prompt).content`. When a new step needs an agent, the diff is one small file. + +Deploys don't trust the gem's live model list. A pinned registry in `config/ruby_llm/models.json`, loaded with `RubyLLM.models.load_from_json!`, means the models we reference exist on every boot, whether or not the registry upstream changed overnight. Dev and test run `qwen3:0.6b` on Ollama, a model small enough to answer in milliseconds on a laptop; production runs a large hosted model, and no agent code knows the difference. + +Retries live in gem config too: `request_timeout 300`, `max_retries 10`, a 1.5x backoff. There is no hand-rolled retry loop anywhere in the eight agents, which is eight places a subtle retry bug can't live. + +## Six bounded integers, summed in Ruby + +Determinism matters most in the scorer, so it declares `temperature 0.0` while everything else stays at the 0.2 default. Its schema is the trick most worth stealing. + +Ask a model for a 0-100 score and you get vibes. Bound six integer parts instead and you get something you can audit: + +```ruby +class CandidateScorer < AgentBase + temperature 0.0 + + schema do + integer :hard_skills_score, minimum: 0, maximum: 30 + integer :domain_score, minimum: 0, maximum: 20 + # ...four more subscores, each with its own bounds + end +end +``` + +The total never comes from the model. Ruby sums the parts with `.fetch`, so a missing key raises instead of quietly scoring a candidate on partial data, and the sum gets clamped to 0..100: + +```ruby +SUBSCORE_KEYS.sum { |key| result.fetch(key) }.clamp(0, 100) +``` + +Models are unreliable at their own addition. + +Give one both the subscores and the total to fill in, and sooner or later the parts won't add up to the number it wrote next to them. The schema bounds each part, and arithmetic stays in a language that has never hallucinated a sum. + +Prompts fence user content the same defensive way. A resume goes into the prompt inside pseudo-XML tags - `...` - so instructions and data stay separable, and a resume containing "ignore previous instructions" reads as data to be scored, not orders to follow. + +## A 16-line loop instead of an orchestrator + +Coordinating all of this is a class called `Workflow`: 16 lines that would fit in a code review comment. It holds an array of step lambdas and reduces a context hash through them: + +```ruby +class Workflow + def initialize(steps) + @steps = steps + end + + def call(context) + @steps.reduce(context) do |ctx, step| + step.call(ctx, self) + end + end +end +``` + +Each step takes the context, does its work - retrieve, rerank, score - and returns the context with more in it. The outer loop runs that reduce up to `max_iterations` times: + +```ruby +max_iterations.times do |n| + context = workflow.call(context) + break if context[:stop] + break if enough_strong_candidates?(context) +end +``` + +Two exits, and only one is arithmetic. When enough candidates clear the score bar, the happy exit fires. The other belongs to the reflector: after each round it reads what the round produced and can set `context[:stop]` along with its reasoning, which we log as `"Stopping iterations at N: "`, so three weeks later you can read why the pipeline gave up on a hard role. + +```mermaid +flowchart TD + A["retrieve"] --> B["rerank"] + B --> C["score
(async fan-out)"] + C --> D{"reflector"} + D -->|"sets :stop
with reasoning"| E["done"] + D -->|"enough strong
candidates"| E + D -->|"another round"| A +``` + +Seen-candidate IDs and already-used queries ride along in the context hash between rounds. Round two works down the list with fresh queries instead of re-scoring what round one already saw, which is the difference between iteration and an expensive infinite loop. + +Retrieval itself is ordinary pgvector work; the how-to lives in [our RAG guide](/blog/building-rag-applications-rails-pgvector/). + +## Fifteen fibers, ten connections + +Most of the wall-clock goes to scoring, so it fans out: one async fiber per candidate, capped by a `score_limit` of 15, using the [async gem](https://github.com/socketry/async): + +```ruby +Async do |task| + candidates.first(score_limit).map { |candidate| + task.async { scorer.score(candidate, role) } + }.map(&:wait) +end +``` + +Fifteen multi-second LLM calls finish in roughly the time of the slowest one. Why fibers fit this shape of waiting is [its own post](/blog/fibers-async-ruby-llm-streaming-rails/); what belongs here is the morning they met our database pool. + +On August 16, three minutes after a deploy, the shortlisting job started raising `ActiveRecord::ConnectionTimeoutError`. The deploy had wrapped the per-candidate scoring call in `ActiveRecord::Base.with_connection` as a defensive guard, and the wrapper had looked like harmless hygiene in review. + +[`with_connection`](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/ConnectionPool.html) checks out a connection eagerly and holds it for the whole block - and our block was a multi-second LLM call that issues zero queries. Fifteen fibers each grabbed a connection from a pool of ten, or tried to. Five starved past the 5-second `checkout_timeout` and raised. + +A 6-second non-DB call with a pool of ten gives `ok=10` and five timeouts when wrapped, `ok=15` and none unwrapped. Swap in a 1-second call and the bug vanishes, because every waiter gets served inside the checkout timeout - the block has to outlast the timeout to starve anyone, and a real scoring call does. + +The fix deleted the wrapper and left the rule as a comment: a fiber that issues no query must hold no connection, and if a step ever needs the database mid-fan-out, wrap only that access. Under fiber concurrency, connection checkout is a per-block decision - thinking in whole requests is exactly what shipped the wrapper. + +## The warts + +One tool in the pipeline is clever, and its cleverness has a hole in it. A `RubyLLM::Tool` evaluates filter combinations for the search query; when the model picks skills that match nothing in the live vocabulary, `execute` returns the valid vocabulary in its result, and the model corrects itself on the next call instead of hallucinating filters that will never match. + +The hole: the retry budget - "max 3 attempts" - lives as prose inside the prompt. Nothing enforces it. + +The scorer's `.fetch` and `.clamp` are code-law: Ruby raises on a missing key and clamps the sum no matter what the model emits. The attempt cap is prompt-law, obeyed the way drivers obey a speed sign - if the model ignores it, no code notices. + +Our messages table has token columns, every call logs its token counts into them, and no code has ever read them back - no cost dashboard, no alert, nothing. We are one bad prompt change away from discovering our spend on the invoice. + +## When one agent is enough + +Most products don't need eight agents. + +A summarizer, a support-reply drafter, a RAG chat - each of those is one agent and maybe a loop, and adding more classes just adds more prompts to keep honest. + +Reach for multiple agents when the steps need different settings from each other. Our scorer needs temperature 0.0 and bounded integers; the query expander needs room to be creative and returns a list of strings; one prompt can't hold both sets of rules. + +And if what you want is a prebuilt vocabulary of chains, retrievers, and output parsers, [LangChain.rb exists](/blog/getting-started-langchain-ruby-complete-guide/) and is maintained. We went the other way because 37 lines we wrote is a smaller thing to debug than an abstraction stack we imported. + +## Where to start + +Start with one agent: a base class, one subclass, a Schematist schema with bounds on every number the model emits. Keep the arithmetic in Ruby from day one. + +Add the loop when a second pass over the data is worth paying for. Before the first production deploy, pin the model registry and put retries in gem config so they exist in exactly one place. The 37 lines were the easy part; the work was deciding what the model never gets to do. + +If you're composing LLM agents into a Rails product, our [app and web development team](/services/app-web-development/) has shipped this exact pipeline - outage and all. + +Further reading: + +- [RubyLLM agents guide](https://rubyllm.com/agents/) - instructions, schemas, and the `Agent` class +- [RubyLLM async guide](https://rubyllm.com/async/) - fiber-safe usage and rate limiting +- [ruby_llm on GitHub](https://github.com/crmne/ruby_llm) - source and changelog +- [async on GitHub](https://github.com/socketry/async) - the fiber concurrency gem underneath +- [Rails ConnectionPool docs](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/ConnectionPool.html) - `with_connection` semantics and `checkout_timeout` +- [Schematist](https://rubygems.org/gems/schematist) - the structured-output schema gem + + diff --git a/content/blog/multi-agent-llm-rails-rubyllm/mermaid-d87bf1a1.svg b/content/blog/multi-agent-llm-rails-rubyllm/mermaid-d87bf1a1.svg new file mode 100644 index 000000000..123d02cf0 --- /dev/null +++ b/content/blog/multi-agent-llm-rails-rubyllm/mermaid-d87bf1a1.svg @@ -0,0 +1 @@ +

sets :stop
with reasoning

enough strong
candidates

another round

retrieve

rerank

score
(async fan-out)

reflector

done

\ No newline at end of file diff --git a/content/blog/ruby-3-4-yjit-performance-guide/cover.png b/content/blog/ruby-3-4-yjit-performance-guide/cover.png new file mode 100644 index 000000000..1ebf09809 Binary files /dev/null and b/content/blog/ruby-3-4-yjit-performance-guide/cover.png differ diff --git a/content/blog/ruby-3-4-yjit-performance-guide/index.md b/content/blog/ruby-3-4-yjit-performance-guide/index.md new file mode 100644 index 000000000..ce6fbc89c --- /dev/null +++ b/content/blog/ruby-3-4-yjit-performance-guide/index.md @@ -0,0 +1,131 @@ +--- +dev_to_id: 2159834 +title: "Ruby 4.0 YJIT vs ZJIT: Performance Guide" +description: "Ruby 4.0 ships two JIT compilers. What YJIT delivers on Rails apps in 2026, the two flags worth tuning, and when the experimental ZJIT is worth a try." +date: 2026-08-20 +created_at: "2025-01-20T10:00:00Z" +edited_at: "2026-08-20T10:00:00Z" +draft: false +tags: ["ruby", "rails", "performance", "yjit", "zjit"] +canonical_url: "https://jetthoughts.com/blog/ruby-3-4-yjit-performance-guide/" +cover_image: cover.png +slug: "ruby-3-4-yjit-performance-guide" +metatags: + image: cover.png + og_title: "Ruby 4.0 YJIT vs ZJIT: Performance Guide" + og_description: "Ruby 4.0 ships two JIT compilers. What YJIT delivers on Rails in 2026, the flags worth tuning, and when the experimental ZJIT is worth a try." + twitter_title: "Ruby 4.0 YJIT vs ZJIT: Performance Guide" + twitter_description: "What YJIT delivers on Rails in 2026, the flags worth tuning, and when ZJIT is worth a try." +--- + +> **Updated August 2026.** This guide originally covered YJIT on Ruby 3.4. [Ruby 4.0 shipped on December 25, 2025](https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-released/) with a second JIT compiler, so the advice below now covers both. + +Ruby 4.0 ships two JIT compilers in the same binary. YJIT is the one your Rails app is probably already running without you having configured anything. ZJIT is its successor, and the release notes that introduce it also tell you not to deploy it. + +If you're mid-upgrade, that combination reads as a contradiction. Sorting it out takes about ten minutes of checking and, on most apps, zero new flags. + +## You may already be running YJIT + +Every Rails 8 app has YJIT on out of the box whenever it boots on Ruby 3.3 or newer - the default [arrived back in Rails 7.2](https://rubyonrails.org/2024/8/10/Rails-7-2-0-has-been-released) as [a one-line initializer inside the framework defaults](https://github.com/rails/rails/pull/49947), and it lands silently for anyone who upgrades and runs `load_defaults` at 7.2 or later. + +So before tuning anything, check: + +```bash +bin/rails runner 'puts RubyVM::YJIT.enabled?' +``` + +If that prints `false` on Ruby 3.3+, the usual culprit is an app that climbed to Rails 8 through upgrades without ever adopting the newer framework defaults. Set `config.yjit = true` in `config/application.rb`, or enable it late in boot: + +```ruby +# config/initializers/enable_yjit.rb +RubyVM::YJIT.enable +``` + +Enabling after boot skips a cost the `--yjit` flag pays: initializer and boot-time code runs once, and compiling it wastes JIT memory. On Ruby 4.0, `RubyVM::YJIT.enable` also accepts `mem_size:` and `call_threshold:` keywords, [added in the 4.0 release](https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-released/), so you can tune it without touching command-line flags. + +Plenty of upgrade checklists end right here: it was already on. + +## What YJIT is worth in 2026 + +On the official benchmark suite at [speed.ruby-lang.org](https://speed.ruby-lang.org/), current YJIT runs the headline benchmarks at roughly 2x interpreter speed, and `railsbench` at about 2.2x. The same dashboard shows YJIT on the 4.x line beating YJIT 3.4.7 by 8.4% geomean, with railsbench 17.1% faster, so the 3.4 to 4.0 upgrade is itself a performance change even if you touch nothing else. + +The benchmark says 2x; your app may measure a single-digit gain. Both numbers are honest. Railsbench is CPU-bound Ruby; your checkout endpoint spends most of its time waiting on Postgres and Stripe, and no JIT speeds up waiting. The Rails team's own framing when they made YJIT the default was [15-25% latency improvement](https://rubyonrails.org/2024/8/10/Rails-7-2-0-has-been-released) on real applications. The win lands on Ruby-heavy paths like serialization and view rendering, not on I/O. + +The same logic applies in reverse: a database-bound endpoint needs database work, not a JIT flag. Cheaper wins usually live in the infrastructure around Ruby - [moving jobs to Solid Queue](/blog/rails-8-solid-queue-migration-guide/), [dropping Redis for Solid Cache](/blog/rails-8-solid-cache-performance-redis-migration/), or [switching to Falcon when the bottleneck is waiting on I/O](/blog/falcon-web-server-async-ruby-production/). + +```mermaid +flowchart TD + A["Endpoint is slow"] --> B{"Where does
the time go?"} + B --> C["Ruby CPU:
serialization,
view rendering"] + B --> D["Database or I/O:
queries, external APIs"] + C --> E["JIT territory:
tune mem-size,
watch ratio_in_yjit"] + D --> F["No JIT flag helps:
fix the query, cache,
or job backend"] +``` + +YJIT costs memory. Compiled machine code and its metadata are capped by `--yjit-mem-size`, [128 MiB by default](https://github.com/ruby/ruby/blob/master/doc/jit/yjit.md), on top of your app's normal footprint. On a 512 MB container that is not a rounding error, and it is the first number to revisit when a post-upgrade pod starts flirting with its memory limit. + +## The two flags that matter + +YJIT exposes a dozen options. Two of them earn attention on a production Rails app, per [the official YJIT documentation](https://github.com/ruby/ruby/blob/master/doc/jit/yjit.md): + +- `--yjit-mem-size` (default 128 MiB) - the soft cap on everything YJIT allocates. Raise it if stats show compilation stopping early; lower it on small containers. +- `--yjit-call-threshold` (default 30, rising automatically to 120 on apps with over 40k ISEQs) - how many calls before a method compiles. Most apps never need to touch it. + +To see whether any of this is working, run with `--yjit-stats=quiet` and read the counters: + +```ruby +stats = RubyVM::YJIT.runtime_stats +stats[:ratio_in_yjit] # % of instructions run as machine code; healthy apps sit near 99 +stats[:code_region_size] # bytes of generated code, your mem-size budget in action +stats[:side_exit_count] # how often compiled code fell back to the interpreter +``` + +A `ratio_in_yjit` in the low 90s usually means the memory cap bit before compilation finished. Raise `mem_size:` and measure again. + +Compiled code dies with the process. A worker-killer that recycles Pumas every 30 minutes throws away every compiled block and re-pays the warmup, so [the YJIT docs recommend](https://github.com/ruby/ruby/blob/master/doc/jit/yjit.md) letting processes live as long as memory allows. If you added aggressive worker recycling years ago to mask a leak, it is now taxing your JIT too. + +## ZJIT: the successor you should test, not deploy + +Ruby 4.0's second compiler is [ZJIT, built by the same Shopify team as the next generation of YJIT](https://railsatscale.com/2025-12-24-launch-zjit/). Where YJIT compiles small basic blocks lazily, ZJIT compiles whole methods through an SSA intermediate representation - a deliberately textbook design that, per the launch post, exists so more compiler engineers can contribute to it. The same post details what the bigger compilation unit buys: inline versions of well-known C methods, and register spilling that lets it handle enormous functions. When its type assumptions break, it side-exits back to the interpreter. + +What it cannot do yet is beat YJIT. The release notes are blunt: ZJIT is faster than the interpreter but not as fast as YJIT, and the stated goal for [Ruby 4.1 is to make ZJIT faster than YJIT and production-ready](https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-released/). The launch post goes further: "You should expect crashes and wild performance degradations (or, perhaps, improvements)." + +Trying it takes one flag on a canary or staging box: + +```bash +ruby --zjit myscript.rb +# or +RUBY_ZJIT_ENABLE=1 bin/rails server +# or at runtime +RubyVM::ZJIT.enable +``` + +Building Ruby from source with ZJIT needs Rust 1.85.0 or newer; a prebuilt binary only includes it if it was compiled that way, which official releases are. + +ZJIT's team improves it against reports from real workloads, and a staging box that mirrors your traffic produces exactly those reports. + +## When not to enable a JIT + +Short-lived processes rarely repay compilation. A rake task that runs for 40 seconds spends its life below the call threshold or throwing away code it just compiled. + +Watch the trap here: a `config/initializers/` enable runs for rake tasks and `rails runner` too, not just the server - initializers execute whenever the Rails environment loads. If your cron boxes are memory-tight, guard the call or move it somewhere only the server executes, like Puma's config file. Otherwise leave the framework default on and size memory for it. + +Test suites are a judgment call. Spec processes are short-lived and restart constantly, so the warmup often costs more than the speedup returns; if CI time matters, benchmark one run with YJIT disabled before assuming the default helps you there. + +And on memory-starved containers, do the arithmetic first. The JIT's headroom has to come from somewhere, and an OOM-killed worker is slower than interpreted Ruby. CPU spent inside Ruby is also worth auditing before you tune the compiler that runs it - [compression settings on encrypted columns](/blog/ruby-on-rails-8-custom-compression-for-encrypted-data/) are a classic example of cores burning where no JIT flag fixes anything. + +## The short version + +Check `RubyVM::YJIT.enabled?` before changing anything, because Rails has probably already decided for you. Upgrade to Ruby 4.0 and take the free 17% on Rails-shaped workloads. Leave ZJIT out of production this year, but give it a staging box and file what you find - Ruby 4.1 is where the successor is supposed to overtake its parent. + +If you want a second pair of eyes on a Rails app whose response times stopped making sense, our [Rails development team](/services/app-web-development/) does this work: profiling first, JIT flags only when the profile says Ruby CPU is the bottleneck. + +**Further reading:** + +- [Ruby 4.0.0 release notes](https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-released/) - the primary source for what changed +- [ZJIT launch post on Rails at Scale](https://railsatscale.com/2025-12-24-launch-zjit/) - architecture and roadmap from the team building it +- [speed.ruby-lang.org](https://speed.ruby-lang.org/) - continuously updated JIT benchmarks +- [Official YJIT documentation](https://github.com/ruby/ruby/blob/master/doc/jit/yjit.md) - every flag and counter referenced above +- [Rails 7.2 release announcement](https://rubyonrails.org/2024/8/10/Rails-7-2-0-has-been-released) - the change that made YJIT a default + + diff --git a/content/blog/ruby-3-4-yjit-performance-guide/mermaid-8e788c1f.svg b/content/blog/ruby-3-4-yjit-performance-guide/mermaid-8e788c1f.svg new file mode 100644 index 000000000..2187cc0b0 --- /dev/null +++ b/content/blog/ruby-3-4-yjit-performance-guide/mermaid-8e788c1f.svg @@ -0,0 +1 @@ +

Endpoint is slow

Where does
the time go?

Ruby CPU:
serialization,
view rendering

Database or I/O:
queries, external APIs

JIT territory:
tune mem-size,
watch ratio_in_yjit

No JIT flag helps:
fix the query, cache,
or job backend

\ No newline at end of file diff --git a/content/blog/rubyllm-rails-getting-started/cover.png b/content/blog/rubyllm-rails-getting-started/cover.png new file mode 100644 index 000000000..3e7711f64 Binary files /dev/null and b/content/blog/rubyllm-rails-getting-started/cover.png differ diff --git a/content/blog/rubyllm-rails-getting-started/index.md b/content/blog/rubyllm-rails-getting-started/index.md new file mode 100644 index 000000000..90af618ef --- /dev/null +++ b/content/blog/rubyllm-rails-getting-started/index.md @@ -0,0 +1,197 @@ +--- +title: "RubyLLM in Rails: Chat, Tools, Streaming" +description: "Add LLM chat to Rails with RubyLLM: one API for OpenAI, Claude, and Ollama, acts_as_chat persistence, tool calling, streaming into Turbo, and real trade-offs." +date: 2026-08-20 +created_at: 2026-08-20T10:00:00Z +draft: false +author: "JetThoughts Team" +slug: "rubyllm-rails-getting-started" +keywords: "rubyllm rails, ruby llm gem, rails llm integration, rubyllm tutorial" +tags: ["ruby", "rails", "ai", "rubyllm", "llm"] +canonical_url: "https://jetthoughts.com/blog/rubyllm-rails-getting-started/" +cover_image: cover.png +cover_image_alt: "Dark blueprint-style cover reading RubyLLM in Rails - Chat, Tools, Streaming, with a low-poly ruby gem and chips for ruby_llm 1.16, OpenAI/Claude/Ollama providers, and acts_as_chat persistence." +metatags: + image: cover.png + og_title: "RubyLLM in Rails: Chat, Tools, Streaming" + og_description: "Add LLM chat to Rails with RubyLLM: one API for OpenAI, Claude, and Ollama, acts_as_chat persistence, tool calling, streaming into Turbo, and real trade-offs." + twitter_title: "RubyLLM in Rails: Chat, Tools, Streaming" + twitter_description: "One API for OpenAI, Claude, and Ollama: acts_as_chat persistence, tool calling, streaming into Turbo, and real trade-offs." +--- + +Adding a second LLM provider is where the wrapper class dies. The OpenAI client you wrote in an afternoon buries its text under `choices[0].message.content`; Anthropic shapes its responses differently and streams differently, so your service object grows a case statement and then a retry matrix. Six months in, you're maintaining a worse version of a gem that already exists. + +[RubyLLM](https://rubyllm.com) gives Rails one interface for all of it. `RubyLLM.chat` speaks to OpenAI, Anthropic, Gemini, Ollama, and any OpenAI-compatible endpoint through the same handful of methods, and the whole thing rests on [three dependencies: Faraday, Zeitwerk, and Marcel](https://github.com/crmne/ruby_llm). + +The gem sits at [version 1.16.0 as we write this](https://rubygems.org/gems/ruby_llm) - and there are still jobs where we'd skip it entirely. + +## One interface, three dependencies + +Install is a Gemfile line; Ruby 3.1.3 or newer is required, per the gemspec. + +```ruby +# Gemfile +gem "ruby_llm" +``` + +Configuration lives in an initializer, and you only set keys for the providers you actually use: + +```ruby +# config/initializers/ruby_llm.rb +RubyLLM.configure do |config| + config.openai_api_key = ENV["OPENAI_API_KEY"] + config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"] +end +``` + +That's enough for a working conversation: + +```ruby +chat = RubyLLM.chat +response = chat.ask "Rank these three error messages by clarity: ..." +response.content +``` + +Want a different model? Pass `model:` to `RubyLLM.chat` and nothing else in your code changes - swapping OpenAI for Anthropic during a provider outage becomes a config change instead of a rewrite. + +RubyLLM also speaks Ollama, so the same interface reaches models running on your own hardware; if self-hosting is the direction you're leaning, start with our [guide to running local LLMs in Rails](/blog/working-with-llms-in-ruby-on-rails-simple-guide-llm/). + +## Persistence: chat rows and message rows + +Homegrown wrappers usually fall apart at persistence. RubyLLM's [Rails generator](https://rubyllm.com/rails/) creates migrations for chats, messages, tool calls, and a model registry, then hooks ActiveRecord models onto them (`load_models` fills that registry - the table tracking which models exist and what they can do): + +```bash +bin/rails generate ruby_llm:install +bin/rails db:migrate +bin/rails ruby_llm:load_models +``` + +```ruby +class Chat < ApplicationRecord + acts_as_chat +end + +class Message < ApplicationRecord + acts_as_message +end +``` + +From there, calling `ask` on a `Chat` record persists both sides of the exchange without any code from you: the user message saves first, an empty assistant message appears when the response starts, and that row fills in as the answer completes. When the API call fails, the gem destroys the empty assistant row instead of leaving half a conversation behind. + +You never call `save` yourself. + +One gotcha is documented but easy to blow past: don't add `validates :content, presence: true` to your `Message` model. Assistant rows are created empty by design, so that validation quietly breaks the persistence flow. + +## Tools: the model calls your Ruby + +LLM features start earning their keep when the model can touch live data - a price lookup, an account query - instead of guessing from its training set. A [tool in RubyLLM](https://rubyllm.com/tools/) is a plain class: + +```ruby +class Weather < RubyLLM::Tool + description "Gets current weather for a location" + + param :latitude, desc: "Latitude of the location" + param :longitude, desc: "Longitude of the location" + + def execute(latitude:, longitude:) + # call your weather service here + end +end +``` + +The `param` lines are optional. Since v1.15, RubyLLM [infers the JSON schema](https://rubyllm.com/tools/) straight from `execute`'s keyword arguments, so a simple tool is just a description and a method. + +Attach it and ask: + +```ruby +chat.with_tool(Weather).ask "What's the weather in Berlin?" +``` + +Here's the round trip the gem runs for you: + +```mermaid +flowchart TD + A["chat.ask 'Weather in Berlin?'"] --> B["Model requests a tool call"] + B --> C["RubyLLM runs Weather#execute"] + C --> D["Result goes back to the model"] + D --> E["Model writes the final answer"] + E --> F["ask returns one Message"] +``` + +All six steps happen inside a single `ask` call; you only define `execute`. + +For recoverable failures the documented convention is returning `{ error: "Location too short" }` so the model can react and retry, while real bugs like missing configuration should raise. + +Treat every argument the model passes to `execute` as untrusted form input: skip `eval`, and never interpolate it into SQL. + +## Streaming into a Turbo view + +Eight seconds of spinner is a long time in a chat UI, so pass a block to `ask` and RubyLLM hands you [normalized chunks as they arrive](https://rubyllm.com/streaming/) - every provider, no server-sent-event parsing on your side: + +```ruby +chat.ask "Draft a welcome email" do |chunk| + print chunk.content +end +``` + +The block receives fragments while `ask` still returns the complete `RubyLLM::Message` at the end, so persistence keeps working. + +In Rails this belongs in a background job. Your controller enqueues the job with the user's question; the job appends it to the chat, then calls `chat.complete` - RubyLLM's "generate the reply to whatever is pending" method - and broadcasts each chunk over Turbo Streams: + +```ruby +class ChatStreamJob < ApplicationJob + def perform(chat_id, question) + chat = Chat.find(chat_id) + chat.messages.create!(role: "user", content: question) + chat.complete do |chunk| + next unless chunk.content + Turbo::StreamsChannel.broadcast_append_to( + "chat_#{chat.id}", + target: "chat_#{chat.id}_response", + html: ERB::Util.html_escape(chunk.content) + ) + end + end +end +``` + +That `html_escape` is not optional: model output is the same trust boundary as the tool arguments above, and it can quote user-supplied text containing markup. Escaped chunks keep the demo honest; a production version accumulates into the message row and renders a partial. + +Subscribe in the view and the tokens land as they're generated: + +```html +<%= turbo_stream_from "chat_#{@chat.id}" %> +
+``` + +Caveats before this hits production. Action Cable doesn't guarantee ordering under concurrent processing, so chunks can render out of order; the RubyLLM docs suggest client-side reordering or AnyCable when that bites. ActiveJob retries after a mid-stream failure will re-append chunks, so cap retries or make the job idempotent. + +Streamed responses also hold a connection open for the life of the generation, which changes your server math once many users chat at once. We did that arithmetic in [Ruby fibers for LLM streaming](/blog/fibers-async-ruby-llm-streaming-rails/), and the production server side lives in [our Falcon post](/blog/falcon-web-server-async-ruby-production/). + +## When NOT to use RubyLLM + +Skip it when plain HTTP would do. If you're calling one provider at one endpoint with no conversation state, a small Faraday client and one test cover it - a gem with four migrations and a model registry is more machinery than the job needs. + +If your feature is a pipeline - prompt templates feeding output parsers feeding a vector store - you want an orchestration library rather than a clean client, and that's [LangChain.rb territory](/blog/getting-started-langchain-ruby-complete-guide/), which brings its own surface area to maintain. + +New provider features lag behind any abstraction. When OpenAI or Anthropic ships a beta API, RubyLLM wraps it after a release cycle rather than the day of the announcement, so a product that depends on the newest knob should call that one endpoint directly and keep RubyLLM for the rest. + +For retrieval-augmented generation, RubyLLM covers the embeddings call and nothing else - chunking, storage, and search design stay your problem. Our [pgvector RAG guide](/blog/building-rag-applications-rails-pgvector/) covers that half of the build. + +## Where to start + +Pick the smallest feature in your backlog that touches an LLM - a summarizer, a support-reply drafter - and build it with plain `RubyLLM.chat` before touching the Rails generator. Add persistence and streaming after the plain version proves the feature. + +Test it like any HTTP dependency. Stub `RubyLLM.chat` at the boundary in unit tests and record one cassette for the integration path, because a CI suite that hits a paid API is a flaky bill. + +If you're adding AI features to a Rails product and want a team that has shipped the whole loop in production - persistence, streaming, and the boring parts included - our [app and web development team](/services/app-web-development/) does exactly that. + +Further reading: + +- [RubyLLM documentation](https://rubyllm.com) - the guides are short and current +- [Rails integration guide](https://rubyllm.com/rails/) - generator, `acts_as_chat`, broadcasting +- [Tools guide](https://rubyllm.com/tools/) - schema inference, error conventions, security notes +- [Streaming guide](https://rubyllm.com/streaming/) - chunk anatomy and mid-stream errors +- [ruby_llm on GitHub](https://github.com/crmne/ruby_llm) - source, changelog, issues + + diff --git a/content/blog/rubyllm-rails-getting-started/mermaid-73388fe4.svg b/content/blog/rubyllm-rails-getting-started/mermaid-73388fe4.svg new file mode 100644 index 000000000..e3ebae8d8 --- /dev/null +++ b/content/blog/rubyllm-rails-getting-started/mermaid-73388fe4.svg @@ -0,0 +1 @@ +

chat.ask 'Weather in Berlin?'

Model requests a tool call

RubyLLM runs Weather#execute

Result goes back to the model

Model writes the final answer

ask returns one Message

\ No newline at end of file diff --git a/docs/blog-post-index.md b/docs/blog-post-index.md index 6597a16d1..53347e9bf 100644 --- a/docs/blog-post-index.md +++ b/docs/blog-post-index.md @@ -3,7 +3,7 @@ Auto-generated by `bin/generate-blog-index` — rerun it after adding/removing posts. Use semantic search (qmd / claude-context) for concept queries; this index is for exact slug/tag/title lookups. -**Total posts: 607** (1 drafts) | Last updated: 2026-08-08 +**Total posts: 610** (1 drafts) | Last updated: 2026-08-20 ## By Tag (top 30) @@ -11,20 +11,20 @@ Use semantic search (qmd / claude-context) for concept queries; this index is fo |---|---|---| | startup | 140 | `10-innovative-strategies-improve-developer-experience-in-2024, 4-steps-bring-life-into-struggling-project-startup-management, 47-startups-failed-same-coding-mistake` | | tutorial | 128 | `10-innovative-strategies-improve-developer-experience-in-2024, 5-steps-add-remote-modals-your-rails-app-javascript-ruby, anonymous-block-argument-in-ruby-tutorial` | +| rails | 95 | `5-steps-add-remote-modals-your-rails-app-javascript-ruby, art-of-form-objects-elegant-search, automate-your-deployments-with-kamal-2-github-actions-devops-development` | | productivity | 93 | `10-innovative-strategies-improve-developer-experience-in-2024, 5-free-tools-make-sales-process-easier-leadgeneration, 8-step-sales-process-in-5-min-productivity` | | management | 92 | `10-innovative-strategies-improve-developer-experience-in-2024, 4-steps-bring-life-into-struggling-project-startup-management, ai-agent-onboarding-problem-real-version` | -| rails | 92 | `5-steps-add-remote-modals-your-rails-app-javascript-ruby, art-of-form-objects-elegant-search, automate-your-deployments-with-kamal-2-github-actions-devops-development` | -| ruby | 75 | `5-steps-add-remote-modals-your-rails-app-javascript-ruby, anonymous-block-argument-in-ruby-tutorial, auto-install-system-dependencies-for-ruby-on-rails-programming` | +| ruby | 78 | `5-steps-add-remote-modals-your-rails-app-javascript-ruby, anonymous-block-argument-in-ruby-tutorial, auto-install-system-dependencies-for-ruby-on-rails-programming` | | webdev | 49 | `art-of-form-objects-elegant-search, calming-delivery-chaos-jetthoughts-field-note, change-inputs-placeholder-color-with-css-html` | | development | 37 | `ai-powered-code-reviews-transforming-development-workflows-2025, automate-your-deployments-with-kamal-2-github-actions-devops-development, avoid-data-migrations-in-schema-for-rails-ruby` | | programming | 30 | `ai-agent-onboarding-problem-real-version, ai-forces-what-rails-teams-already, art-of-form-objects-elegant-search` | -| ai | 24 | `47-startups-failed-same-coding-mistake, ai-agent-deleted-production-database-pocketos, ai-agent-onboarding-problem-real-version` | +| ai | 25 | `47-startups-failed-same-coding-mistake, ai-agent-deleted-production-database-pocketos, ai-agent-onboarding-problem-real-version` | | devops | 23 | `align-remote-teams-with-okrs-impact-mapping-management-devops, async-advantage-how-switching-communication-styles, auto-install-system-dependencies-for-ruby-on-rails-programming` | | agile | 22 | `4-steps-bring-life-into-struggling-project-startup-management, async-remote-xp-practices, checklist-for-non-tech-founder-agile` | | testing | 22 | `47-startups-failed-same-coding-mistake, collecting-javascript-code-coverage-with-capybara-in-ruby-on-rails-application-testing, how-we-configure-simplecov-for-our-ruby-on-rails-projects` | +| performance | 18 | `design-rails-json-api-with-performance-in-mind-cache, falcon-web-server-async-ruby-production, falcon-web-server-production-tuning-benchmarks` | | founder | 17 | `47-startups-failed-same-coding-mistake, ai-agent-deleted-production-database-pocketos, ai-code-ownership-accountability` | | css | 16 | `change-inputs-placeholder-color-with-css-html, how-create-circles-in-css-html, how-create-triangles-in-css-html` | -| performance | 16 | `design-rails-json-api-with-performance-in-mind-cache, falcon-web-server-async-ruby-production, falcon-web-server-production-tuning-benchmarks` | | html | 14 | `change-inputs-placeholder-color-with-css-html, how-create-circles-in-css-html, how-create-triangles-in-css-html` | | remote | 14 | `advantages-of-freelance-why-its-really-worth-trying-remote, benefits-of-working-remotely-remote, communication-agreement-in-remote-environment-agile` | | hiring | 11 | `cheap-developers-expensive-without-cto-review, dev-shop-red-flags-checklist, discovering-best-recruitment-companies-in-usa` | @@ -44,56 +44,56 @@ Use semantic search (qmd / claude-context) for concept queries; this index is fo | Date | Slug | Title | |---|---|---| -| 2026-08-08 | `rails-cve-2026-66066-active-storage-rce` | Rails CVE-2026-66066: Patch Isn't Enough | -| 2026-08-08 | `rails-7-eol-unpatched-security-exposure` | Rails 7 End of Life: Pick Your Exit | +| 2026-08-20 | `rubyllm-rails-getting-started` | RubyLLM in Rails: Chat, Tools, Streaming | +| 2026-08-20 | `fibers-async-ruby-llm-streaming-rails` | Ruby Fibers for LLM Streaming in Rails | +| 2026-08-20 | `ruby-3-4-yjit-performance-guide` | Ruby 4.0 YJIT vs ZJIT: Performance Guide | | 2026-08-08 | `migrate-lovable-replit-app-to-rails` | Migrate a Lovable or Replit App to Rails | +| 2026-08-08 | `rails-7-eol-unpatched-security-exposure` | Rails 7 End of Life: Pick Your Exit | +| 2026-08-08 | `rails-cve-2026-66066-active-storage-rce` | Rails CVE-2026-66066: Patch Isn't Enough | | 2026-08-07 | `kamal-2-multi-server-deployment-complete-guide` | Kamal 2 Multi-Server Deployment Guide | | 2026-07-31 | `switch-dev-shops-safely-transition-guide` | Switch Dev Shops Without Losing Progress | +| 2026-07-31 | `cheap-developers-expensive-without-cto-review` | Cheap Developers Are Expensive Without Review | +| 2026-07-31 | `asked-simple-admin-panel-built-spaceship` | Asked for an Admin Panel, Got a Spaceship | | 2026-07-31 | `retros-founder-transparency-tool` | Retros Are Your Transparency Tool | | 2026-07-31 | `dev-shop-sla-requirements-checklist` | What SLAs to Require From Your Dev Shop | | 2026-07-31 | `dev-shop-contract-code-ownership` | Dev Shop Contract: Who Owns the Code? | -| 2026-07-31 | `cheap-developers-expensive-without-cto-review` | Cheap Developers Are Expensive Without Review | -| 2026-07-31 | `asked-simple-admin-panel-built-spaceship` | Asked for an Admin Panel, Got a Spaceship | | 2026-07-24 | `solid-queue-advanced-patterns-retries-concurrency` | Solid Queue: Retries, Concurrency, Monitoring | | 2026-07-22 | `falcon-web-server-production-tuning-benchmarks` | Falcon in Production: Tuning and Benchmarks | | 2026-05-09 | `vibe-coding-disposable-by-design` | Vibe Coding Is Disposable. Stop Shipping It. | | 2026-05-07 | `ai-code-ownership-accountability` | AI Code Has an Owner Problem | +| 2026-05-04 | `claude-code-xp-team-workflow` | How We Replicated a Full Product Team With Claude Code Agents | | 2026-05-04 | `test-driven-development-tdd-in-ruby-step-by-guide-tutorial-bestpractices` | TDD in Ruby: A Step-by-Step Guide | -| 2026-05-04 | `tdd-overkill-myth-lightweight-ruby` | TDD Without the Overkill: Why Lightweight TDD Ships Faster | | 2026-05-04 | `refactor-step-tdd-three-line-discipline-ruby` | Refactor Without Breaking Tests: The 3-Line Discipline in Ruby | -| 2026-05-04 | `claude-code-xp-team-workflow` | How We Replicated a Full Product Team With Claude Code Agents | -| 2026-05-02 | `rails-cve-2026-41316-founder-guide` | Rails CVE-2026-41316: Detection and Patch | +| 2026-05-04 | `tdd-overkill-myth-lightweight-ruby` | TDD Without the Overkill: Why Lightweight TDD Ships Faster | | 2026-05-02 | `quality-tax-ai-mvp-cost` | The Quality Tax: AI MVPs Cost More to Fix | +| 2026-05-02 | `rails-cve-2026-41316-founder-guide` | Rails CVE-2026-41316: Detection and Patch | | 2026-05-01 | `ai-agent-deleted-production-database-pocketos` | 9 Seconds: An AI Agent Wiped a Live SaaS | | 2026-04-27 | `hiring-dev-shop-questions` | 5 Questions Before Hiring a Dev Shop | | 2026-04-26 | `rails-event-structured-logging-8-1` | Rails 8.1: Subscribe to Events, Skip Logs | +| 2026-04-23 | `47-startups-failed-same-coding-mistake` | 47 Startups Failed the Same Way | | 2026-04-23 | `founders-guide-hiring-dev-shop` | Founder's Guide to Hiring a Dev Shop in 2026 | | 2026-04-23 | `dev-shop-red-flags-checklist` | 8 Red Flags You Hired the Wrong Dev Shop | -| 2026-04-23 | `47-startups-failed-same-coding-mistake` | 47 Startups Failed the Same Way | | 2026-04-19 | `vibe-coding-crisis-ai-code-debt` | Vibe Coding Crisis: Why AI Code Breaks | | 2026-04-14 | `rails-8-1-active-job-continuations-end-lost-background-jobs` | Active Job Continuations in Rails 8.1 | | 2026-04-13 | `solid-trifecta-hybrid-redis-rails-8` | Solid Trifecta: When to Keep Redis in Rails 8 | | 2026-04-10 | `fire-dev-shop-guide` | How to Fire Your Dev Shop (Safely) | -| 2026-04-09 | `ruby-on-rails-performance-optimization-patterns-2026` | Ruby on Rails Performance Optimization Patterns for 2026 | | 2026-04-09 | `rails-argon2-has-secure-password-migration-guide` | Rails `has_secure_password` with Argon2: Complete Migration Guide | +| 2026-04-09 | `ruby-on-rails-performance-optimization-patterns-2026` | Ruby on Rails Performance Optimization Patterns for 2026 | | 2026-03-28 | `jetthoughts-top-web-development-agency-2026-techreviewer` | JetThoughts Named Among the Top Web Development Agencies by Techreviewer | | 2025-12-12 | `async-remote-xp-practices` | Async-First Remote Engineering Teams: Adapting XP Practices for Modern Distribut | | 2025-11-18 | `how-technical-leaders-handle-unrealistic-deadlines` | How Technical Leaders Handle Unrealistic Deadlines in SaaS (Without Burning Out | | 2025-10-28 | `rails-8-docker-deployment-production-guide` | Rails 8 Deployment with Docker: Production-Ready Configuration Guide | | 2025-10-28 | `django-technical-debt-cost-calculator-elimination-strategy` | Django Technical Debt Cost Calculator & Elimination Strategy | -| 2025-10-27 | `rails-8-solid-cache-performance-redis-migration` | Rails 8 Solid Cache Performance: Complete Migration from Redis | -| 2025-10-27 | `rails-8-authentication-generator-devise-migration` | Rails 8 Authentication Generator: Complete Migration from Devise | -| 2025-10-27 | `propshaft-vs-sprockets-rails-8-asset-pipeline-migration` | Propshaft vs Sprockets: Complete Rails 8 Asset Pipeline Migration Guide | -| 2025-10-27 | `laravel-performance-monitoring-complete-apm-comparison-guide` | Laravel + Datadog APM: Setup, Cost, and 3 Alternatives | | 2025-10-27 | `laravel-11-migration-guide-production-deployment-strategies` | Laravel 11 Upgrade Guide: Step-by-Step Migration | -| 2025-10-27 | `hotwire-turbo-8-performance-patterns-real-time-rails` | Hotwire Turbo 8 Performance Patterns: Real-Time Rails Applications | +| 2025-10-27 | `laravel-performance-monitoring-complete-apm-comparison-guide` | Laravel + Datadog APM: Setup, Cost, and 3 Alternatives | | 2025-10-27 | `django-5-enterprise-migration-guide-production-strategies` | Django 5.0 Enterprise Migration Guide: Production Deployment Strategies | +| 2025-10-27 | `propshaft-vs-sprockets-rails-8-asset-pipeline-migration` | Propshaft vs Sprockets: Complete Rails 8 Asset Pipeline Migration Guide | +| 2025-10-27 | `rails-8-solid-cache-performance-redis-migration` | Rails 8 Solid Cache Performance: Complete Migration from Redis | +| 2025-10-27 | `hotwire-turbo-8-performance-patterns-real-time-rails` | Hotwire Turbo 8 Performance Patterns: Real-Time Rails Applications | +| 2025-10-27 | `rails-8-authentication-generator-devise-migration` | Rails 8 Authentication Generator: Complete Migration from Devise | | 2025-10-18 | `autogen-crewai-langgraph-ai-agent-frameworks-2025` | LangGraph vs CrewAI vs AutoGen: Which AI Agent Framework? (2026) | -| 2025-10-17 | `ruby-langchain-testing-complete-rspec-guide` | Ruby LangChain Testing Guide 2025 - RSpec + WebMock | | 2025-10-17 | `laravel-ai-integration-tutorial-complete-guide` | Laravel AI Integration Tutorial: Complete Guide 2025 | | 2025-10-17 | `elixir-ai-integration-tutorial-complete-guide` | Elixir AI Integration Tutorial: Complete Guide 2025 | -| 2025-10-16 | `building-rag-applications-rails-pgvector` | Building RAG Applications in Rails 7.1+ with pgvector: Complete Guide | -| 2025-10-15 | `testing-monitoring-llm-applications-production` | Testing and Monitoring LLM Applications: From Non-Deterministic Chaos to Product | ## JetThoughts Process Posts @@ -110,42 +110,42 @@ Use semantic search (qmd / claude-context) for concept queries; this index is fo | 2025-10-02 | `ai-forces-what-rails-teams-already` | AI Forces What Rails Teams Already Knew: Small Teams Ship Faster | | 2025-06-20 | `fixing-slow-engineering-teams-an-extended` | Fixing Slow Engineering Teams — An Extended Field Guide for Founders | | 2025-06-12 | `simplicity-paradox-why-your-startups-technical` | The Simplicity Paradox: Why Your Startup's Technical Strategy Should Be Boring 🎯 | -| 2025-05-19 | `from-pitfalls-profit-how-successfully-implement` | From Pitfalls to Profit: How to Successfully Implement Async | | 2025-05-19 | `async-advantage-how-switching-communication-styles` | The Async Advantage: How Switching Communication Styles Saves $3.2M Annually | +| 2025-05-19 | `from-pitfalls-profit-how-successfully-implement` | From Pitfalls to Profit: How to Successfully Implement Async | | 2025-05-04 | `jtbd-okrs-practical-guide-for-customer` | JTBD + OKRs: A Practical Guide for Customer-Focused Teams | | 2025-04-21 | `solving-kamals-target-failed-become-healthy` | Solving Target Failed to Become Healthy Kamal Error: A Practical Debugging Guide | | 2025-02-06 | `evolution-of-engineering-leadership-in-berlin-insights-from-vp-engineerings-journey-product-management` | The Evolution of Engineering Leadership in Berlin: Insights from a VP Engineerin | | 2025-01-29 | `unlocking-success-how-software-engineer-can-build-thriving-business-in-2025` | Unlocking Success: How a Software Engineer Can Build a Thriving Business in 2025 | | 2025-01-29 | `finding-right-co-founder-guide-for-startups` | Finding The Right Co-Founder: A Guide For Startups | +| 2025-01-27 | `innovative-rails-companies-leading-tech-revolution-in-2025` | Innovative Rails Companies Leading the Tech Revolution in 2025 | | 2025-01-27 | `what-founder-mode-really-means` | What Founder Mode Really Means | | 2025-01-27 | `understanding-fractional-positions-future-of-flexible-employment-in-2025` | Understanding Fractional Positions: The Future of Flexible Employment in 2025 | -| 2025-01-27 | `innovative-rails-companies-leading-tech-revolution-in-2025` | Innovative Rails Companies Leading the Tech Revolution in 2025 | | 2025-01-25 | `unlocking-potential-innovative-strategies-for-making-money-with-software-in-2025` | Unlocking Potential: Innovative Strategies for Making Money with Software in 202 | | 2025-01-25 | `how-convert-customers-with-cold-emails-startup-school` | How To Convert Customers With Cold Emails - Startup School | | 2025-01-24 | `once-you-identify-problem-fix-it-can-always-launch-again` | Once You Identify The Problem And Fix It, You Can Always Launch Again | | 2025-01-23 | `when-machines-learn-delete-an-8` | When Machines Learn to Delete: An 8-Week Experiment in AI Autonomy | | 2025-01-23 | `essential-skills-every-rails-engineer-needs-succeed-in-2025` | The Essential Skills Every Rails Engineer Needs to Succeed in 2025 | | 2025-01-22 | `y-combinator-co-founder-jessica-livingston-on-beginnings-of-yc` | Y Combinator Co-Founder Jessica Livingston on the Beginnings of YC | -| 2025-01-21 | `why-choosing-right-mvp-software-development-company-crucial-for-startup-success-in-2025` | Why Choosing the Right MVP Software Development Company is Crucial for Startup S | | 2025-01-21 | `building-trust-at-scale-how-tooltime-runs-76-microservices-through-empowerment-startup-management` | Building Trust at Scale: How ToolTime Runs 76 Microservices through Empowerment | +| 2025-01-21 | `why-choosing-right-mvp-software-development-company-crucial-for-startup-success-in-2025` | Why Choosing the Right MVP Software Development Company is Crucial for Startup S | | 2025-01-20 | `unlocking-success-innovative-strategies-find-employees-for-free-in-2025` | Unlocking Success: Innovative Strategies to Find Employees for Free in 2025 | | 2025-01-19 | `understanding-distinctions-web-development-software-explained` | Understanding the Distinctions: Web Development and Software Development Explain | | 2025-01-18 | `reviving-defense-technology-silicon-valleys-next-chapter` | Reviving Defense Technology: Silicon Valley's Next Chapter | | 2025-01-17 | `future-of-software-development-web-trends-watch-in-2025` | The Future of Software Development and Web Development: Trends to Watch in 2025 | | 2025-01-16 | `how-yc-was-created-with-jessica-livingston` | How YC Was Created With Jessica Livingston | | 2025-01-15 | `understanding-intersection-of-software-development-web-key-insights-for` | Understanding the Intersection of Software Development and Web Development: Key | -| 2025-01-14 | `ruby-on-rails-in-2025-why-smart-ctos-still-choose-rails-for-rapid-development` | Ruby on Rails in 2025: Why Smart CTOs Still Choose Rails for Rapid Development | | 2025-01-14 | `building-worlds-best-image-diffusion-model` | Building The World's Best Image Diffusion Model | +| 2025-01-14 | `ruby-on-rails-in-2025-why-smart-ctos-still-choose-rails-for-rapid-development` | Ruby on Rails in 2025: Why Smart CTOs Still Choose Rails for Rapid Development | +| 2025-01-13 | `unlocking-opportunities-how-marketplace-jobs-are-transforming-job-market-in-2025` | Unlocking Opportunities: How Marketplace Jobs Are Transforming the Job Market in | | 2025-01-13 | `why-your-startup-needs-single-source-of-truth-how-create-it-tutorial` | Why Your Startup Needs a Single Source of Truth (And How to Create It) | | 2025-01-13 | `unlocking-success-how-mvp-development-services-can-propel-your-startup-forward` | Unlocking Success: How MVP Development Services Can Propel Your Startup Forward | -| 2025-01-13 | `unlocking-opportunities-how-marketplace-jobs-are-transforming-job-market-in-2025` | Unlocking Opportunities: How Marketplace Jobs Are Transforming the Job Market in | -| 2025-01-12 | `shape-up-founders-guide-not-wasting-your-runway-startup-tutorial` | Shape Up: A Founder's Guide to Not Wasting Your Runway | | 2025-01-12 | `outsourcing-trap-why-your-product-deserves-better-startup-tutorial` | The Outsourcing Trap: Why Your Product Deserves Better | +| 2025-01-12 | `shape-up-founders-guide-not-wasting-your-runway-startup-tutorial` | Shape Up: A Founder's Guide to Not Wasting Your Runway | | 2025-01-11 | `innovative-strategies-for-website-development-startups-in-2025` | Innovative Strategies for Website Development for Startups in 2025 | | 2025-01-09 | `unlocking-success-best-software-development-tools-elevate-your-projects-in-2025` | Unlocking Success: The Best Software Development Tools to Elevate Your Projects | -| 2025-01-07 | `weekly-ruby-roundup-highlights-from-4` | Weekly Ruby Roundup: Highlights from Ruby #4 | | 2025-01-07 | `times-when-paranoia-fueled-technological-advancement` | The Times When Paranoia Fueled Technological Advancement | | 2025-01-07 | `innovative-software-development-tools-techniques-for-2025` | Innovative Software Development Tools and Techniques for 2025 | +| 2025-01-07 | `weekly-ruby-roundup-highlights-from-4` | Weekly Ruby Roundup: Highlights from Ruby #4 | | 2025-01-06 | `unlocking-opportunities-rise-of-part-time-executive-roles-in-todays-job-market` | Unlocking Opportunities: The Rise of Part Time Executive Roles in Today's Job Ma | | 2025-01-05 | `over-complicated-pricing-could-kill-sales-process` | Over-Complicated Pricing Could Kill A Sales Process | | 2025-01-05 | `mastering-debugging-insights-from-chelsea-troy-on-ruby-663` | Mastering Debugging: Insights from Chelsea Troy on RUBY 663 | @@ -153,11 +153,11 @@ Use semantic search (qmd / claude-context) for concept queries; this index is fo | 2025-01-03 | `unlocking-power-of-hexagonal-architecture-in-rails-development` | Unlocking the Power of Hexagonal Architecture in Rails Development | | 2025-01-02 | `unlocking-efficiency-how-internal-developer-platforms-transform-software-development-in-2025` | Unlocking Efficiency: How Internal Developer Platforms Transform Software Develo | | 2025-01-02 | `essential-strategies-hire-developers-for-your-startup-in-2025` | Essential Strategies to Hire Developers for Your Startup in 2025 | -| 2025-01-01 | `unveiling-ghost-engineering-insights-from-breaking-change-podcast-v25` | Unveiling Ghost Engineering: Insights from Breaking Change Podcast v25 | | 2025-01-01 | `mastering-user-retention-insights-from-startup-school` | Mastering User Retention: Insights from Startup School | +| 2025-01-01 | `unveiling-ghost-engineering-insights-from-breaking-change-podcast-v25` | Unveiling Ghost Engineering: Insights from Breaking Change Podcast v25 | | 2024-12-31 | `choosing-right-software-development-company-for-startups-2024-guide` | Choosing the Right Software Development Company for Startups: A 2024 Guide | -| 2024-12-30 | `unlocking-opportunities-best-staffing-agencies-elevate-your-career-in-2024` | Unlocking Opportunities: The Best Staffing Agencies to Elevate Your Career in 20 | | 2024-12-30 | `mastering-ruby-on-rails-best-practices-for-efficient-development-in-2024` | Ruby on Rails Best Practices: 8 Production Patterns for 2026 | +| 2024-12-30 | `unlocking-opportunities-best-staffing-agencies-elevate-your-career-in-2024` | Unlocking Opportunities: The Best Staffing Agencies to Elevate Your Career in 20 | | 2024-12-30 | `jason-meller-welcomes-1password-rails-foundation` | Jason Meller Welcomes 1Password to the Rails Foundation | | 2024-12-30 | `how-live-in-social-media-matrix` | How To Live In The Social Media Matrix | | 2024-12-29 | `innovative-ruby-on-rails-projects-boost-your-development-skills-in-2024` | Innovative Ruby on Rails Projects to Boost Your Development Skills in 2024 | @@ -165,23 +165,23 @@ Use semantic search (qmd / claude-context) for concept queries; this index is fo | 2024-12-28 | `reviving-ruby-community-exciting-meetups-across-europe` | Reviving the Ruby Community: Exciting Meetups Across Europe | | 2024-12-28 | `10-innovative-strategies-improve-developer-experience-in-2024` | 10 Innovative Strategies to Improve Developer Experience in 2024 | | 2024-12-27 | `enhancing-productivity-ultimate-developer-experience-tool-for-2024` | Enhancing Productivity: The Ultimate Developer Experience Tool for 2024 | +| 2024-12-26 | `are-we-in-an-ai-hype-cycle` | Are We In An AI Hype Cycle? | | 2024-12-26 | `innovative-companies-using-rails-how-they-leverage-ruby-on-for-success-in-2024` | Innovative Companies Using Rails: How They Leverage Ruby on Rails for Success in | | 2024-12-26 | `exciting-updates-in-ruby-on-rails-sqlite3-extensions-more` | Exciting Updates in Ruby on Rails: SQLite3 Extensions and More | -| 2024-12-26 | `are-we-in-an-ai-hype-cycle` | Are We In An AI Hype Cycle? | | 2024-12-25 | `ideal-tech-startup-team-structure-for-rapid-growth` | The ideal tech startup team structure for rapid growth | | 2024-12-25 | `discover-top-software-companies-in-california-comprehensive-guide-for-2024` | Discover the Top Software Companies in California: A Comprehensive Guide for 202 | | 2024-12-24 | `unlocking-secrets-of-ruby-debugging-from-basics-advanced-tools` | Unlocking the Secrets of Ruby Debugging: From Basics to Advanced Tools | | 2024-12-24 | `transform-your-business-with-expert-front-end-web-development-services` | Transform Your Business with Expert Front End Web Development Services | -| 2024-12-23 | `unlocking-opportunities-how-fractional-jobs-are-redefining-future-of-work` | Unlocking Opportunities: How Fractional Jobs Are Redefining the Future of Work | | 2024-12-23 | `mastering-rails-with-react-comprehensive-guide-for-2024` | Mastering Rails with React: A Comprehensive Guide for 2024 | +| 2024-12-23 | `unlocking-opportunities-how-fractional-jobs-are-redefining-future-of-work` | Unlocking Opportunities: How Fractional Jobs Are Redefining the Future of Work | | 2024-12-23 | `how-influence-decision-makers` | How To Influence Decision Makers | -| 2024-12-22 | `mastering-multiple-returns-in-ruby-power-of-datadefine` | Mastering Multiple Returns in Ruby: The Power of Data.define | | 2024-12-22 | `exploring-future-of-frontend-technology-trends-innovations-for-2025` | Exploring the Future of Frontend Technology: Trends and Innovations for 2025 | -| 2024-12-20 | `mastering-link-creation-in-rails-best-practices-unveiled` | Mastering Link Creation in Rails: Best Practices Unveiled | -| 2024-12-20 | `innovative-strategies-in-software-development-for-startups-navigating-challenges-of-2024` | Innovative Strategies in Software Development for Startups: Navigating the Chall | -| 2024-12-20 | `from-slim-erb-developers-journey-back-classic-templates` | From SLIM to ERB: A Developer's Journey Back to Classic Templates | +| 2024-12-22 | `mastering-multiple-returns-in-ruby-power-of-datadefine` | Mastering Multiple Returns in Ruby: The Power of Data.define | | 2024-12-20 | `essential-strategies-for-building-high-performance-software-development-team-in-2024` | Essential Strategies for Building a High-Performance Software Development Team i | | 2024-12-20 | `choosing-right-tech-stack-for-your-next-project-insights-recommendations` | Choosing the Right Tech Stack for Your Next Project: Insights and Recommendation | +| 2024-12-20 | `mastering-link-creation-in-rails-best-practices-unveiled` | Mastering Link Creation in Rails: Best Practices Unveiled | +| 2024-12-20 | `from-slim-erb-developers-journey-back-classic-templates` | From SLIM to ERB: A Developer's Journey Back to Classic Templates | +| 2024-12-20 | `innovative-strategies-in-software-development-for-startups-navigating-challenges-of-2024` | Innovative Strategies in Software Development for Startups: Navigating the Chall | | 2024-12-19 | `how-find-your-next-startup-idea-lessons-from-y-combinator-management` | How to Find Your Next Startup Idea: Lessons from Y Combinator | | 2024-12-18 | `how-find-technical-vendor-with-confidence-startup-tutorial` | How to find technical vendor with confidence | | 2024-12-16 | `what-every-non-technical-founder-must-know-when-building-tech-product-startup-management` | What Every Non-Technical Founder Must Know When Building a Tech Product | @@ -190,44 +190,44 @@ Use semantic search (qmd / claude-context) for concept queries; this index is fo | 2024-10-11 | `automate-your-deployments-with-kamal-2-github-actions-devops-development` | Automate Your Deployments with Kamal 2 and GitHub Actions | | 2024-09-27 | `why-assigning-tasks-directly-from-backlog-hurts-team-collaboration-efficiency-agile-startup` | Why Assigning Tasks Directly from the Backlog Hurts Team Collaboration and Effic | | 2024-09-24 | `how-fractional-cto-turned-mess-into-stable-product-startup-usecase` | How a Fractional CTO Turned a Mess into a Stable Product | -| 2024-09-11 | `what-do-when-you-have-big-pr-blocking-other-issues-development-productivity` | What to Do When You Have a Big PR Blocking Other Issues | | 2024-09-11 | `red-flags-watch-for-in-big-pr-when-stop-split-or-rework-development-productivity` | Red Flags to Watch for in a Big PR: When to Stop, Split, or Rework | | 2024-09-11 | `how-small-pr-improves-team-productivity-development` | How Small PR Improves Team Productivity | +| 2024-09-11 | `what-do-when-you-have-big-pr-blocking-other-issues-development-productivity` | What to Do When You Have a Big PR Blocking Other Issues | | 2024-08-08 | `deploying-ruby-on-rails-applications-with-kamal-devops-docker` | Deploying Ruby on Rails applications with Kamal | | 2024-08-01 | `prototyping-your-startup-mvp-from-concept-reality-agile` | Prototyping Your Startup MVP: From Concept to Reality | | 2024-07-30 | `navigating-team-transitions-guide-for-non-technical-founders-startup-agile` | Navigating Team Transitions: A Guide for Non-Technical Founders | -| 2024-07-04 | `transforming-titans-outsourcing-odyssey-leadership-agile` | Transforming Titans: A Novel Journey of Agile Leadership in Outsourcing | | 2024-07-04 | `top-3-ways-test-ideas-without-developers-startup-agile` | Top 3 Ways to Test Ideas Without Developers | +| 2024-07-04 | `transforming-titans-outsourcing-odyssey-leadership-agile` | Transforming Titans: A Novel Journey of Agile Leadership in Outsourcing | | 2024-06-12 | `why-when-use-waterfall-vs-agile-business-perspective-management` | Why and When to Use Waterfall vs. Agile: A Business Perspective | -| 2024-06-07 | `how-does-your-company-work-with-clients-understand-their-needs-lean-process` | How does your company work with clients to understand their needs? | | 2024-06-07 | `from-what-start-stop-delivering-bugs-when-there-no-time-for-changes-management-agile` | From what to start to stop delivering bugs when there is no time for changes? | -| 2024-06-05 | `onboarding-tests-into-legacy-project-testing-startup` | Onboarding Tests into Legacy Project | -| 2024-06-05 | `myth-or-reality-can-test-driven-development-in-agile-replace-qa-programming` | Myth or Reality: Can Test-Driven Development in Agile Replace QA? | -| 2024-06-05 | `how-we-temporarily-transformed-our-usual-workflow-for-tight-deadline-agile` | How We Temporarily Transformed Our Usual Workflow for a Tight Deadline | +| 2024-06-07 | `how-does-your-company-work-with-clients-understand-their-needs-lean-process` | How does your company work with clients to understand their needs? | | 2024-06-05 | `how-setup-incremental-design-process-in-startup` | How to Setup Incremental Design Process in a Startup | | 2024-06-05 | `how-jetthoughts-implements-joels-test-deveopment-management` | How JetThoughts implements Joel’s test? | | 2024-06-05 | `how-get-remote-teams-high-perform-agile-development` | How to Get Remote Teams to High Perform | -| 2024-06-05 | `fractional-cto-comprehensive-review-of-first-two-weeks-in-startup-consulting-management` | Fractional CTO: A Comprehensive Review of the First Two Weeks in a Startup | +| 2024-06-05 | `how-we-temporarily-transformed-our-usual-workflow-for-tight-deadline-agile` | How We Temporarily Transformed Our Usual Workflow for a Tight Deadline | | 2024-06-05 | `checklist-for-non-tech-founder-agile` | Checklist for the non-tech founder | +| 2024-06-05 | `onboarding-tests-into-legacy-project-testing-startup` | Onboarding Tests into Legacy Project | +| 2024-06-05 | `myth-or-reality-can-test-driven-development-in-agile-replace-qa-programming` | Myth or Reality: Can Test-Driven Development in Agile Replace QA? | | 2024-06-05 | `4-steps-bring-life-into-struggling-project-startup-management` | 4 Steps to Bring Life into a Struggling Project | -| 2024-05-15 | `why-communication-important-when-you-work-remotely-remote` | Why communication is so important when you work remotely? | -| 2024-05-15 | `typical-day-at-jetthoughts-agile-remote` | A typical day at JetThoughts | -| 2024-05-15 | `things-that-remote-teams-expect-from-product-owner-startup` | Things that remote teams expect from the product owner | +| 2024-06-05 | `fractional-cto-comprehensive-review-of-first-two-weeks-in-startup-consulting-management` | Fractional CTO: A Comprehensive Review of the First Two Weeks in a Startup | | 2024-05-15 | `services-tools-automatize-development-for-remote-teams-workflow-automation` | Services and tools to automatize development for the remote teams | +| 2024-05-15 | `typical-day-at-jetthoughts-agile-remote` | A typical day at JetThoughts | | 2024-05-15 | `how-know-what-your-team-doing-remote-startup` | How to know what your team is doing? | +| 2024-05-15 | `why-communication-important-when-you-work-remotely-remote` | Why communication is so important when you work remotely? | | 2024-05-15 | `effective-project-onboarding-checklist-management-productivity` | Effective project onboarding checklist | | 2024-05-15 | `cons-of-private-chats-for-team-collaboration-communication-process` | Cons of the private chats for team collaboration | -| 2024-05-14 | `what-do-for-developer-when-wip-limit-reached-agile-kanban` | What to do for the developer when the WIP limit is reached | -| 2024-05-14 | `what-activities-are-expected-from-remote-developer-for-effective-collaboration-development-process` | What activities are expected from a remote developer for effective collaboration | +| 2024-05-15 | `things-that-remote-teams-expect-from-product-owner-startup` | Things that remote teams expect from the product owner | +| 2024-05-14 | `cleaning-routines-keep-your-project-without-bugs-agile` | Cleaning routines to keep your project without bugs | | 2024-05-14 | `how-start-an-open-source-project-building-reso-api-js-client-javascript-opensource` | How to start an Open Source project. Building RESO API JS client | | 2024-05-14 | `delivery-flow-for-distributed-remote-teams-agile-kanban` | Delivery Flow for Distributed Remote Teams | -| 2024-05-14 | `cleaning-routines-keep-your-project-without-bugs-agile` | Cleaning routines to keep your project without bugs | +| 2024-05-14 | `what-activities-are-expected-from-remote-developer-for-effective-collaboration-development-process` | What activities are expected from a remote developer for effective collaboration | +| 2024-05-14 | `what-do-for-developer-when-wip-limit-reached-agile-kanban` | What to do for the developer when the WIP limit is reached | | 2024-01-24 | `install-official-firefox-deb-in-dockerfile-docker-devops` | Install Official Firefox .deb in Dockerfile | | 2023-08-02 | `align-remote-teams-with-okrs-impact-mapping-management-devops` | Align remote teams with OKRs and Impact Mapping | | 2022-12-15 | `how-wip-limits-improves-effectiveness-productivity-management` | How WIP Limits improves effectiveness? | | 2022-12-03 | `when-use-microservices-devops-distributedsystems` | When to use Microservices? | -| 2022-09-28 | `tldr-move-cicd-scripts-into-automation-devops-productivity` | TL;DR: Move CI/CD scripts into .automation | | 2022-09-28 | `our-mvp-team-structure-startup-management` | Team Structure for MVP | +| 2022-09-28 | `tldr-move-cicd-scripts-into-automation-devops-productivity` | TL;DR: Move CI/CD scripts into .automation | | 2022-09-22 | `auto-install-system-dependencies-for-ruby-on-rails-programming` | Auto-install system dependencies for Ruby on Rails | | 2022-09-09 | `incremental-lint-fixes-by-github-actions-devops` | Incremental lint fixes by GitHub Actions | | 2022-08-16 | `how-create-circles-in-css-html` | How to create circles in CSS | diff --git a/docs/projects/2510-seo-content-strategy/20-29-strategy/20.09-content-plan-revision-aug-2026.md b/docs/projects/2510-seo-content-strategy/20-29-strategy/20.09-content-plan-revision-aug-2026.md index da755f320..065d7e804 100644 --- a/docs/projects/2510-seo-content-strategy/20-29-strategy/20.09-content-plan-revision-aug-2026.md +++ b/docs/projects/2510-seo-content-strategy/20-29-strategy/20.09-content-plan-revision-aug-2026.md @@ -144,7 +144,7 @@ devise-migration post canonical, merge the unique parts of the new post into it, | F4 sharding | `ruby-on-rails-8-simplifying-sharding-with-new-methods` | Upgrade in place | | F11 Kamal+GHA | `automate-your-deployments-with-kamal-2-github-actions-devops-development` | Upgrade in place | | F13 logging | `rails-event-structured-logging-8-1` | Upgrade in place | -| **F7 Ruby 3.4 YJIT** | — | **Cut - stale version line, Ruby 4.0 is stable** | +| **F7 Ruby 3.4 YJIT** | `ruby-3-4-yjit-performance-guide` (6,310 impr / pos 9.5, 90d to Aug 20) | **Cut as a new post; existing post refreshed in place 2026-08-20** (Aug refresh slot). The live post carried fabricated Shopify/GitHub metrics, two fabricated JT client stories, and an invented roadmap - rewritten as the Ruby 4.0 YJIT vs ZJIT guide, all claims sourced. | The Kamal cluster is at 9 posts after E4. F12 would make 10. **Freeze the cluster** and fix `automating-ssl-certificate-generation-with-traefik-kamal-step-by-guide`, @@ -409,9 +409,52 @@ specialization — only two reach the homepage, and none as a **showable artifac --- +## 12. RubyLLM × Rails series — groomed queue (Paul, 2026-08-20) + +Paul's directive (verbatim intent): "create a lot of posts for RubyLLM and Rails apps - the +basics, the issues, what other tools, AI + Rails tips and tricks, how Fiber helps, how to set +up a multi-agent system with RubyLLM. Use `~/dev/elital` and Reddit as reference." + +Why this fits the plan: AI+Ruby is inside the authority zone that already earns - +`getting-started-langchain-ruby-complete-guide` (6,969 impr, pos 8.5) and +`building-rag-applications-rails-pgvector` rank today. These rows fill the **Rails technical +~2/mo** slots; they are not founder-cluster posts and do not violate §9. + +| # | Working title | Dedup check before drafting | Notes | +|---|---|---|---| +| R1 | RubyLLM basics: chat, tools, streaming in Rails | `working-with-llms-in-ruby-on-rails-simple-guide-llm` exists (dev.to import) - audit; upgrade it or 301 into R1 | Entry post, owns "rubyllm rails" | +| R2 | RubyLLM vs langchainrb vs raw API: picking a Rails LLM stack | LangChain guides exist and rank - R2 must LINK them, not cannibalize | Comparison intent | +| R3 | The issues nobody blogs about: rate limits, token budgets, retries, streaming into Turbo | - | Practitioner pain post; mine `~/dev/elital` for real patterns | +| R4 | Fibers + LLM streaming: why async Ruby matters for AI apps | Ties to `falcon-web-server-async-ruby-production` (site's #2 earner) | Bridges two authority clusters | +| R5 | Multi-agent systems with RubyLLM: tools, handoffs, orchestration | - | Reference architecture from `~/dev/elital`; strongest differentiation, least SERP competition | +| R6 | AI + Rails tips roundup (Reddit-sourced) | - | Likely LinkedIn material, not a blog slot - groom before committing | +| R7 | Evaluating RubyLLM agents: schemas, golden sets, audit logs (Paul, 2026-08-20) | `testing-monitoring-llm-applications-production` exists - R7 is agent-EVAL specific, link it, don't re-cover monitoring | First-hand material ready: per-iteration agent_logs audit trail (STOP/CONTINUE + reasoning persisted), bounded-schema scoring as an eval primitive | +| R8 | Debugging RubyLLM agents in Rails (Paul, 2026-08-20) | - | First-hand material ready: the VCR method+URI matching trap (wire-format change passed a whole suite unnoticed), fakes-over-mocks, key-scrubbing regexes, the active_connection? invariant test that survives transactional pinning | + +Sequencing: R1 (or the upgrade of the existing import) first, then R5 - basics own the head +term, the multi-agent post owns the white space. One per Rails-technical slot; the §1 gate and +§7 weighting still apply. + +**Execution status (2026-08-20, PR #488):** R1 ✅ `rubyllm-rails-getting-started` · +R4 ✅ `fibers-async-ruby-llm-streaming-rails` · R5 ✅ `multi-agent-llm-rails-rubyllm` - +all through the full 4-reviewer loop same day. The old Ollama import (R1's dedup row) +turned out to cover local models only, zero RubyLLM mentions - kept and linked, no 301. +(Groomed note: that import opens with banned "let's dive into" - queue a light surgical +refresh, not a rewrite.) +**R3 verdict: RESCOPE before writing** - its natural territory (testing, monitoring, +retries) collides with the existing `testing-monitoring-llm-applications-production` +(3,349 words, no visual, likely refresh-debt itself); the right move is refreshing THAT +post RubyLLM-aware in a September refresh slot, leaving R3 only cost/token tracking + +provider fallback (possibly one post, possibly folded into the refresh). **R2 caution:** +must frame as decision guide linking the two ranking LangChain guides, not a parallel +comparison page. Stopped at 3 new posts + 1 refresh for the day - a 5th same-day post +compounds the cluster-fingerprint risk voice-rules now documents. + ## Changelog | Date | Change | |---|---| +| 2026-08-20 | Added §12: RubyLLM × Rails series groomed queue (R1-R6) from Paul's directive; fills future Rails-technical slots, sequenced R1 → R5. | +| 2026-08-20 | **August refresh slot executed**: `ruby-3-4-yjit-performance-guide` rewritten in place as the Ruby 4.0 YJIT vs ZJIT guide. Premise audit first: §4's Traefik/Kamal candidate had decayed to 4 impressions / 0 clicks / pos 20 (90d GSC) - refresh there protects nothing; the YJIT post holds 6,310 impressions at pos 9.5 AND carried fabricated claims (invented Shopify $2.4M savings, GitHub results, 2 fake JT client case studies, fictional Ruby 3.5/3.6/4.0 roadmap). De-fabrication + version refresh in one slot. Moved from `content/blog/2025/` flat file into a page bundle (same slug/URL) for local cover + pre-rendered mermaid. | | 2026-08-13 | Added §11 (P4 proof gap) from the third-party audit response. Findings: JT uses commodity-devshop language while blaming devshops (one sentence near-identical to competitor Rubyroid Labs); JT shows less checkable proof than thoughtbot/Rubyroid/SumatoSoft; and every proof artifact `90.10` §5 requires already exists inside the course but is not wired to the claims. Groomed, not scheduled. | | 2026-08-07 | Created. Four-agent re-review (market, SEO, competitor, goal-alignment). Supersedes 20.08's allocation/cadence/projection; keeps its GSC analysis. Headline findings: the bet forbids a content sprint while outreach is stalled; the rescue offer page has zero inbound links from 608 posts; 6 queued rows cannibalize existing posts and 1 already did; real capacity is ~6/month not 10-13; the 435-click projection predates AI Overviews. | diff --git a/docs/workflows/blog-pipeline.md b/docs/workflows/blog-pipeline.md index 15b550cad..f833ff966 100644 --- a/docs/workflows/blog-pipeline.md +++ b/docs/workflows/blog-pipeline.md @@ -436,6 +436,62 @@ Commit message format: N external citations. Primary keyword in first 100 words. ``` +## Batch mode — write the next N posts without stops (validated 2026-08-20, PR #488) + +Paste this to run a multi-post sprint. It layers three batch-proven practices on the +per-post pipeline above: the live-GSC premise audit, real-code mining, and the +same-day-cluster sweep. + +**Agent form (preferred):** spawn `blog-batch-orchestrator` (outer loop: queue → +premise audit → dispatch → batch gates → PR → CI watch) which runs one +`blog-post-coordinator` per post (inner loop: writer → 3 critics → cold-eyes → +ship gates, iterating until PUBLISH-READY or 2 failed rounds). Both are defined +in `.claude/agents/`. Shortest invocation: *"Spawn blog-batch-orchestrator, N=4."* +The prompt below is the inline equivalent for sessions without the agent registry. + +``` +Write the next N blog posts end-to-end, no stops between posts. Feature branch, +one PR for the batch, full pipeline per docs/workflows/blog-pipeline.md. + +PER POST: +1. PICK from the 20.09 groomed queue - audit the premise first: pull live GSC + for the topic and run the dedup check; on collision, upgrade-in-place or + rescope, record the verdict in the plan, take the next row. +2. RESEARCH primary sources only (official docs, release notes, gem source - + fetch, don't recall). Mine our real codebases for first-hand war stories, + sanitized (shapes and lessons yes; prompts, model IDs, proprietary numbers + no). Zero fabricated clients, stats, or quotes. Frame the latest framework/ + language versions as the present; older versions only as cited history. +3. WRITE via a content-creator agent with the full packet (voice guide 90.11, + cadence anchor from blog-writer-reference-samples.md, STEP 4-4d rules, + verified internal links). Two-pass write. +4. REVIEW with three parallel critics - tech (fetch every source, verify every + claim and every line of copyable code), slop/SEO, copy editor. Iterate until + tech is clean, slop >=8/10, editor <=minor. +5. COLD-EYES fresh-context 9-check gate with edit rights; require PUBLISH-READY. +6. SHIP: stitch cover (render + visually inspect), pre-rendered mermaid clearing + the 9px/390px floor, bin/hugo-build + bin/check-post-visuals green, browser + scroll gate desktop+mobile with zero console errors/404s, commit with the + reviewer verdicts quoted. + +AFTER THE LAST POST: run the same-day-cluster sweep from +.okf/content/voice-rules.md across the whole batch (shared intensifiers, +metaphor families, verbatim phrases, cloned CTAs); update plan statuses; okf +maintain; push and open ONE PR with a per-post verdict table; watch CI and +clear flakes. + +STOPS: only if the queue runs dry after rescopes, or a post fails a gate twice - +record why in the plan and continue with the next. Hand back with a localhost +review link per post. +``` + +Why these three additions are load-bearing (evidence from the 2026-08-20 batch): +the premise audit redirected the refresh slot from a 4-impression page to a +6,310-impression page carrying live fabrications; the real-code mining produced +the batch's best material (a commit-documented production outage); and the +cluster sweep caught fingerprints all four per-post reviews passed (one shared +intensifier in all four posts, one metaphor family used 5x). + ## Pipeline phases (reference) | Phase | What | Gate | diff --git a/test/fixtures/screenshots/macos/desktop/careers/_overview.png b/test/fixtures/screenshots/macos/desktop/careers/_overview.png index baaaafe7a..07247e385 100644 Binary files a/test/fixtures/screenshots/macos/desktop/careers/_overview.png and b/test/fixtures/screenshots/macos/desktop/careers/_overview.png differ