Skip to content

Course: palette pass, discovery diagnosis that retracts the plan's premise, GA4 setup closed - #508

Merged
pftg merged 4 commits into
masterfrom
course-palette-discovery-2026-08-21
Aug 20, 2026
Merged

pftg merged 4 commits into
masterfrom
course-palette-discovery-2026-08-21

Conversation

@pftg

@pftg pftg commented Aug 20, 2026

Copy link
Copy Markdown
Member

Three items Paul picked off the open-queue triage, plus a dashboard he asked for mid-flight.

1. Artifact-trail palette pass (1 file, not 5)

The C2.2 follow-up asked for a palette-unification pass across the five module walkthrough trails, on the grounds green was landing on non-money cards. Rendered all five side by side before touching anything, and the diagnosis was half right.

Four of the five already follow one learnable rule: exactly one tinted card per trail, the module payoff, green when it is money and purple when it is not. M1 ($594 price test) and M5 (cleared deposit) are green; M3 (PASS) and M4 (live MVP) are purple. Only M2 broke it, tinting three of five cards so the eye had nothing to land on.

Fix: untint M2's two lesson-2.5 cards. The 2.6 prototype card keeps its purple.

Deliberately not changed, having looked at the renders:

  • Green big-text on BUILD / PASS / 6.5%. The spec's short line says "green = money only" but its own table says "money/success outcomes", and green reads consistently as the good result in every trail. The spec's letter does not beat a working exhibit ([[feedback-legibility-fix-not-redesign]]).
  • Two "defects" that are not defects - M2's duplicate 2.5 chips and M3's 3.1/3.1/3.2/3.2. Both verified correct against each walkthrough's own lesson links: lesson 2.5 produces both the BUILD verdict and the money answer, and module 3 has two lessons with two artifacts each.

Also renamed .pay -> .done in M2/M3/M4 where the class is purple and marks a non-money card - the misleading name is what generated this backlog item. Rendered PNG hashes identical before and after, so the rename is provably zero-delta.

Gate: content-only. bin/hugo-build green (8/8 validators). M2 scored 4/4 on the new-media visual gate at 720 and at a 390 render; all five still clear bin/check-svg-floor.

2. Course discovery diagnosis - the plan's premise is retracted

Asked why 60+ course URLs sit at "positions 5-13" with ~800 impressions and 2 clicks. Pulled it live instead of re-reading the summary. The premise does not survive.

  • GSC averages position over impressions, so the best-looking course rows are the emptiest: form-your-founding-hypothesis reports position 3.0 on ONE impression.
  • Of the six queries GSC will name across all course pages in 28 days (62 impressions), 57 come from "stripe / collison" anti-reference and dashboard.stripe.com/screenshare - a scraped string and someone trying to reach Stripe support. The four real queries total five impressions at positions 42-97.
  • The one course page with a real high-volume target sits at position 26.6 / 32.6 over 90 days. Not 5-13.
  • The domain ranks fine - every real ranking it holds is a Ruby/Rails/AI query.

So: topical authority, not CTR and not indexation. Which is also why the falsified title/meta experiment could never have worked - you cannot snippet your way up from position 27, and there were no impressions to convert.

The finding that should reorder the plan: AI Assistant delivered 34 sessions at 79% engagement against 2 from the LinkedIn course campaign in the same window, and the course landing is the 15th most-landed page on the site without ranking for anything. LLM retrieval matches on content, not on the domain authority the course lacks.

Full diagnosis with reproduce steps: 2605 50-59-execution/50.05. Retracted the stale clause in all three places carrying it (TASK-TRACKER, 50.03, .okf/log #2) rather than only adding a new doc - it had already been read as encouraging in two separate reads.

3. GA4 setup closed out + a dashboard

  • page_view un-marked as a key event. Until this landed, keyEvents counted ~4,000 page views and every conversion figure was unusable, including yesterday's contact_cta_click.
  • Two Explorations built and shared property-wide, each carrying the bot caveat in its own name.
  • A Reports snapshot dashboard (Paul's mid-flight ask). The property had never had one configured. Set to "User behavior", deliberately not "Marketing performance" - that template leads on channel attribution and conversions, the two numbers this property is worst at.
  • A red annotation, because the snapshot still leads with "Active users 9K" and "15s average engagement", and a saved report that silently repeats the 86%-collapse trap is worse than no report.

Traps recorded for next time: un-marking a key event is not retroactive (the Campaign exploration read 4,063 right after the change); annotation descriptions cap at 150 chars and truncate mid-word; the annotation date field fills its END slot when you type.

Also

Closed the stale half of the C1.2 checklist - all three fabricated-fact items were already applied in the content; only the tracker still said otherwise. Verified each in the files before ticking.

Note on review

Per this session's standing directive I did not dispatch reviewer agents; blocking checks were self-run with grep/hash/render evidence quoted in each commit. Flagging it explicitly since CLAUDE.md's 4-eyes rule normally wants a second pair.

🤖 Generated with Claude Code

pftg and others added 3 commits August 20, 2026 22:12
The C2.2 follow-up asked for a palette-unification pass across the five
module walkthrough trails, on the grounds that green was landing on non-money
cards. Rendering all five side by side says the diagnosis was half right.

Four of the five already follow a learnable rule: exactly one card is tinted,
and it is the module's payoff - green when that payoff is money (M1's $594
price test, M5's cleared deposit), purple when it is not (M3's PASS, M4's live
MVP). M2 was the only one out of line, tinting three of five cards purple, so
the eye had nothing to land on and the rule the other four teach broke.

Fixed by untinting M2's two lesson-2.5 cards. The prototype card (2.6) keeps
its purple - it is the module's terminal artifact and it is not money.

Deliberately NOT changed, having looked at the renders:

- Green big-text on BUILD (M2), PASS (M3) and 6.5% (M1). The spec's short line
  reads "green = money only" but its own table says "money/success outcomes",
  and in every trail green consistently marks the good result. That is a
  working convention; the spec's letter does not beat a working exhibit.
- M2's duplicate "2.5" chips and M3's 3.1/3.1/3.2/3.2. These look like
  copy-paste bugs and are not: module 2 lesson 2.5 produces both the BUILD
  verdict and the money answer, and module 3 has two lessons with two
  artifacts each. Verified against each walkthrough's own lesson links.

Also renamed .pay -> .done in M2/M3/M4, where the class is purple and marks a
non-money card. The misleading name is what generated this backlog item in the
first place. M1/M5 keep .pay because there it is green and it is money.
Rendered hashes before and after are identical for M3 and M4 - the rename is
provably zero-delta.

Gates: content-only class. bin/hugo-build green (8/8 validators). M2 scored
4/4 on the new-media visual gate at 720 and at a 390 render; all five still
clear bin/check-svg-floor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t is retracted

Asked why 60+ course URLs sit at positions 5-13 with ~800 impressions and 2
clicks. Pulled it live rather than re-reading the inherited summary, and the
premise does not survive.

GSC averages position over impressions, so the course's best-looking rows are
its emptiest: `form-your-founding-hypothesis` reports position 3.0 on ONE
impression, the course index 4.3 on three. Those are not rankings.

Of the six queries GSC will name across all course pages in 28 days (62
impressions total), 57 come from `"stripe / collison" anti-reference` and
`dashboard.stripe.com/screenshare` - a scraped string and someone trying to
reach Stripe support. The four real queries total five impressions at positions
42-97. On the one course page with an unambiguous high-volume target, the honest
90-day number is position 26.6 and 32.6, not 5-13.

Meanwhile the domain ranks perfectly well - `rails install dependencies` 5.9,
`sidekiq vs solid queue` 7.4, `langchain tutorial` 179 impressions. Every real
ranking it holds is a Ruby/Rails/AI-engineering query. So this is topical
authority, not CTR and not indexation, which is also why the falsified
title/meta experiment could never have worked: you cannot snippet your way up
from position 27, and there were no impressions to convert.

The finding that changes plans is the asymmetry next door. AI Assistant
delivered 34 sessions at 79% engagement in the same window against 2 from the
LinkedIn course campaign, and the course landing is the 15th most-landed page on
the site without ranking for anything. LLM retrieval matches on content, not on
the domain topical authority the course lacks - it is winning in the channel
where its handicap does not apply, unattended, while the worked channel returns
2.

Full diagnosis with reproduce steps: 2605 50.05. Retracted the stale clause in
the three places that carry it (TASK-TRACKER, 50.03, .okf/log #2) rather than
only adding the new doc - it had already been read as encouraging in two
separate reads.

Gates: docs-only, no build surface touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ot caveat

All four browser-only GA4 items are done. None needed an API - Explorations
have none at all, and the rest were plain UI.

- `page_view` un-marked as a key event. Until this landed, `keyEvents` counted
  ~4,000 page views and every conversion figure on the property was unusable,
  including the `contact_cta_click` marked yesterday. Key events list went 4
  rows to 3.
- Two Explorations built and shared property-wide, each carrying the bot caveat
  in its own name: Campaign performance and Site health.
- A Reports snapshot. The property had NEVER had one - clicking Reports landed
  on a template chooser. Set to "User behavior", deliberately not "Marketing
  performance": that template leads on channel attribution and conversions, the
  two numbers this property is worst at, so it would have put the bot trap on
  the landing screen.
- A red property annotation, because the snapshot still leads with "Active
  users 9K" and "15s average engagement" and a saved report that silently
  repeats the 86%-collapse trap is worse than no report.

Three traps recorded in analytics-access for whoever does this next:

1. Un-marking a key event is NOT retroactive. Key events are stamped at
   processing time, so any window including data before today still counts
   page_view - the Campaign exploration read 4,063 immediately after the
   change. Only windows starting 2026-08-21 give a clean count.
2. Annotation descriptions cap at 150 chars and truncate mid-word silently.
3. The annotation date field is a two-slot range and typing fills the END slot;
   the left slot needs an explicit click, and Save stays disabled until it has
   a value.

Also closed the stale half of the C1.2 checklist: all three fabricated-fact
items were already applied in the content and only the tracker still said
otherwise. Verified each in the files before ticking.

Gates: docs-only; bin/hugo-build green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fa3259db-05c9-48be-aa0b-9ac288338334


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…EO/AEO closed

Paul 2026-08-21, on the discovery diagnosis in 50.05.

Course SEO and AEO are CLOSED, not deferred. The course does not rank and the
cause is domain topical authority, not the pages - and AEO is not the escape
hatch: a 60-day pull gives the course 4 of ~62 AI-assistant sessions, the top
cited page being a job ad. Both channels route on the same dev gravity. Written
into the tracker and widened in 50.03's not-scheduled list, which previously
only ruled out "SEO/title work" - the narrower line would have let an AEO or
schema pass back in.

LinkedIn is the one arrival channel not gated by topical authority, and it has
barely run: 3 posted of 19 planned. Sliced into five task cards (LI-0..LI-D) in
linkedin-posts/content-plan.md for SEPARATE sessions to take - this session
scheduled the work, it is not executing it. Cards touch disjoint files so two
can run concurrently; each carries its own doctrine links, gates, and the
Paul-gated posting rule.

Put them in content-plan.md rather than a new backlog file: it is already the
rolling calendar and its "Next actions" section was a stale 2026-08-14 handoff.
That section is kept, marked superseded, rather than deleted - it still holds
the open story-bank item and the outreach context.

LI-0 exists because the record disagrees with itself. content-plan's Status
column marks trust-signals-poll, ten-interviews-recap and staging-question
"revised" with no `revised:` line in their frontmatter, while the three that DO
carry the 2026-08-20 marker are listed as draft/approved. The table was last
touched 08-17, the revisions landed 08-20. Frontmatter is the artifact and
wins, but which way each of the three resolves needs reading, not assuming - so
it is a task, not a silent fix, and it blocks the other cards.

Gates: docs-only (content-plan.md is `render: never`); bin/hugo-build green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pftg
pftg merged commit dde62bd into master Aug 20, 2026
5 checks passed
@pftg
pftg deleted the course-palette-discovery-2026-08-21 branch August 20, 2026 21:05
pftg added a commit that referenced this pull request Aug 20, 2026
#508 un-marked page_view and #495 marked contact_cta_click, closing the
Phase 0.1 GA4 items this plan still described as outstanding and which I had
been reporting to Paul as blocked.

Carries the caveat that outlives the fix: un-marking is NOT retroactive, so
the ~4,063 historical page-view 'key events' remain in the data and any
before/after read spanning 2026-08-13 to 08-20 compares a polluted before
against a clean after. Date-bound keyEvents queries or read the underlying
event names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pftg added a commit that referenced this pull request Aug 20, 2026
#505)

* Correct ADR-0003: #1a8cff is the logo colour; logos out of scope

Lane A halted its codemod rather than recolour brand assets and the reason
corrects the ADR. After deleting --color-primary the only #1a8cff left in
built output was SVG assets - logo-dark.svg contains exactly one hex value,
no brand definition' was overstated: true of the documented design system,
false of the actual logo.

The decision survives with a better reason - a mark colour is not a UI
accent. The logo identifies, the accent directs; promoting the mark's blue to
'primary' is what put blue bands, blue tags and blue links on a ruby site.
The three logo files stay blue and are OUT OF SCOPE for every design-system
phase, not deferred.

Also records the trap that produced the finding: SVG assets cannot read CSS
custom properties, so ~29 hardcoded icons are invisible to token work and a
recolour must sweep them separately. 20.02 counted CSS references and
literals but never SVG assets - a gap in my plan, not in the execution.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Correct Phase 1a: the !important count, the a11y history, and the link role

Three corrections, all surfaced by the agent executing the plan rather than by
me writing it.

1. The success signal 'delete all 55 !important workarounds' was wrong and
   dangerous. 55 counted every !important in four files; only ~19 are
   anchor-attributable - the rest are @media print rules and legacy
   heading-margin fights, so chasing the number would have deleted print
   styles to hit it. Replaced with the falsifiable version: every !important
   whose comment cites the anchor rule must go, and no other may be touched.
   The trap generalises - blog-single.css's comment says 'same class of fight
   ... both die in Phase 1a'. Same class, different cause.

2. #0066d6 is also an accessibility fix, not only a specificity monster: it
   replaced #1a8cff in Sprint #2 because the brand blue measured 3.37:1 and
   failed AA. Retiring it means landing the replacement at AA or better, not
   merely 'not blue'.

3. Body links decided - --ink-900 text with a --color-ruby underline. Not ruby
   text: ruby is the action colour and a body full of ruby links stops links
   being distinguishable from buttons. WCAG 1.4.1 requires more than colour to
   mark a link, so the underline carries the affordance and the text stays
   calm for long-form reading. Recorded with a do-not-simplify note.

Also adds the SVG-asset row the original measured-surface table omitted (50
files: 47 icons swept, 3 logos deliberately untouched).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* OKF: the white-wash trap, and why 4 of 6 review agents died

Two durable findings from the 3-lane redesign swarm.

architecture/css-pipeline.md - computed style, not source, proves the paint.
Two sections of the new /friday-report/ page computed to
background-color rgba(0,0,0,0): the white behind them was
legacy-theme-skin.css's hardcoded .fl-page-content, which ships after the
page slice and wins on cascade order. Zero visual delta today because both
are #ffffff - which is exactly why it would have sat undetected - and it
detonates the moment a --surface token moves off white, leaving a
half-recoloured page caused by a file nobody touched. Records the detection
method and the id+class fix from new-page.md. Same family as the uppercase
#1A8CFF that survived a case-sensitive sweep.

workflows/review-swarm.md - review agents failed 4 of 6, all on a Fable
credit limit, because work agents carried an explicit opus override while the
reviewers they spawned inherited the default. The asymmetry is the hazard:
work completes and reports success while its gate quietly does not run.
Brief size looks causal rather than incidental - the three that died had long
briefs, the one that returned a verdict had the tightest, and a
consumption-based limit means the sprawling thorough-looking brief is LESS
likely to produce a review. Rules: dead reviewer is not a passed review; idle
is ambiguous so ask for the verdict; pass model opus explicitly; a
coordinator closing a leg itself must say so and invite contradiction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Sync 2608 to reality: GA4 key-event toggles are closed

#508 un-marked page_view and #495 marked contact_cta_click, closing the
Phase 0.1 GA4 items this plan still described as outstanding and which I had
been reporting to Paul as blocked.

Carries the caveat that outlives the fix: un-marking is NOT retroactive, so
the ~4,063 historical page-view 'key events' remain in the data and any
before/after read spanning 2026-08-13 to 08-20 compares a polluted before
against a clean after. Date-bound keyEvents queries or read the underlying
event names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
pftg added a commit that referenced this pull request Aug 20, 2026
…utes

A Codex review found a P1 that made the whole SEO effort a no-op, plus a
bug in my own generator. Both verified in source before fixing.

1. THE REPAIRS WERE NOT DURABLE. lib/sync/post.rb:39-45 re-pulls title
   and description from dev.to unless the frontmatter carries
   `seo_override: true`. Its own comment says "the 10-min sync cron
   clobbers any locally-edited SEO snippet". None of the 141 files I
   edited had the flag, so every rebuilt description and the hand-written
   SimpleCov title would have been reverted within ten minutes of merge -
   silently, with the commits still in history looking like they worked.

   All 140 dev.to-backed files now set it, verified by script: 140 with
   the flag, 0 missing. The SimpleCov post gets it too, since its title
   and description were both hand-written.

2. MY GENERATOR ATE UNDERSCORES. clean() ran gsub(/[*_`]/, "") to strip
   markdown emphasis, which also stripped underscores inside identifiers.
   `stringify_keys` shipped as "stringifykeys" - destroying the exact
   keyword that post targets, in the description Google reads.

   Emphasis is now stripped only when the markers actually wrap
   something, and inline code is unwrapped rather than deleted. Blast
   radius measured rather than assumed: exactly 1 of 141 descriptions
   contained an underscore, so one post was mangled, not many. All 141
   were restored to their pre-commit state and regenerated from source
   with the corrected cleaner - 140 rebuilt, 93 left for a human.

Two factual corrections in the new posts, both verified in the gems:

- R2 claimed langchainrb gives token counts "on every response". The
  Hugging Face, llama.cpp and Replicate response classes never override
  those methods, so they inherit base_response and raise
  NotImplementedError. Now scoped to the mainstream adapters, with the
  exceptions named.

- R3 said a retried POST could duplicate a row write or an email. It
  cannot. chat.rb:233 runs handle_tool_calls only after a response
  returns, and the retry middleware wraps the HTTP request below that -
  so a lost response can double-charge the completion, but the first
  attempt never reached the tool. Duplicate side effects come from
  retrying the job around the call. The post now says so.

Local macOS suite (not CI): 34 runs, 87 assertions, 0 failures, and 53
screenshots compared with no failures. That also answers the CI
Screenshot Tests failure - footer and CTA regions on homepage/services
do not reproduce locally, and the linux baselines date from #494 with
design work landed since (#503 tokens, #508 palette). Drift, not this
diff.

bin/hugo-build 8/8.
pftg added a commit that referenced this pull request Aug 20, 2026
… entries (#520)

* fix(seo): both page-1 posts were serving Google a truncated SERP entry

Two posts rank page-one and take almost no clicks: `solid queue vs
sidekiq` at position 5.2 with 0.86% CTR (116 impressions, 1 click), and
`simplecov` at 10.3 with 0.87% (115 impressions, 1 click). Expected CTR
at position 5 is roughly 6%.

I assumed by analogy to the Kamal post that this was a weak title, then
read the RENDERED output instead of the source, and the real defect was
mechanical.

`layouts/partials/seo/enhanced-meta-tags.html` truncates a blog title to
45 chars (:14), appends " | JetThoughts Blog", then truncates the whole
string at 60 with an ellipsis (:8, :22). A title over 45 therefore gets
cut twice and lands in the SERP trailing an ellipsis AFTER the brand:

  before: Solid Queue vs Sidekiq: Complete Comparison | JetThoughts…
  after:  Solid Queue vs Sidekiq: When Each Wins | JetThoughts Blog

  before: How we configure Simplecov for our Ruby on | JetThoughts…
  after:  How to Configure SimpleCov in Rails | JetThoughts Blog

That is also WHY the pipeline's "title <= 45 chars" rule exists - a rule
I had been following without knowing the mechanism.

Descriptions were broken independently. Solid Queue's ran 162 chars and
lost "included." to the 160 cap. SimpleCov's ended in a literal "..." in
the source file - a dev.to import artifact, since dev.to truncates
descriptions near 100 chars and the importer carried the ellipsis into
our meta tag verbatim.

Both descriptions now answer the query instead of listing the contents,
and both render whole.

Content untouched. Solid Queue is 2,161 words and did not need it;
SimpleCov is 561 words and DOES look thin against SimpleCov's own docs
at position 10 - flagged, not fixed here, because that is a rewrite
rather than a snippet fix.

Also in this commit: the 2026-08-20 P0 gate override recorded in 20.09,
so a later session reads the content sprint as a deliberate call rather
than a gate nobody checked.

Verified in rendered HTML, not source: bin/hugo-build 8/8 validators,
zero ellipsis in either title or description.

* fix(seo): stop truncating post titles twice - 454 of 614 shipped an ellipsis

Paul authorised dropping the brand suffix if it helped. It does, and the
underlying bug was arithmetic the template contradicted itself on.

`enhanced-meta-tags.html` cut a post title to 45, appended
" | JetThoughts Blog" (19 chars) for 64, then cut the result again at 60.
45 + 19 = 64 > 60 ALWAYS, so any title of 42+ characters was truncated
twice and reached the SERP trailing an ellipsis after the brand name:

  How we configure Simplecov for our Ruby on | JetThoughts…
  Solid Queue vs Sidekiq: Complete Comparison | JetThoughts…
  Insights from Zapier's CTO on Managing Remote…

The template's own two constants could never both hold. Its effective
safe length was 41 chars, not the 45 the code implied - which is also
why the blog pipeline's "title <= 45" rule exists, a rule I had been
following without knowing the mechanism.

Measured before the change: 454 of 614 posts (74%) carried titles over
45 chars. After: of 690 rendered post titles, exactly ONE still contains
an ellipsis - "Why AI Hasn't Blown Our Minds…Yet", where it is deliberate
punctuation.

Fix is structural rather than numeric: truncate EXACTLY once. Post titles
take the full 60 with no brand suffix; the second length guard moved
inside the non-blog branch, where the suffix is still appended and still
needs it. Google appends the site name itself when it wants one.

Diagnosis note, because the wrong answer was convincing: reading the
source suggested the blog branch already handled this. Rendered output
disagreed. Two probe markers in the template proved the second truncate
was firing on top of the first - the branch was right and ran twice.
Source-reading would have shipped a no-op.

Gates: bin/hugo-build 8/8 validators. `bin/qtest --changed` reports no
visual-affecting changes, correctly - title and meta tags move no pixels.
og:title and twitter:title verified to follow the same clean string.

* fix(seo): rebuild 141 meta descriptions the dev.to import left mid-sentence

234 of 615 posts served Google a description ending in a bare '...'.
The cause is structural: 529 posts (86% of the blog) came from dev.to,
which truncates its description near 100 chars, and the importer copied
that ellipsis into our frontmatter verbatim.

The full sentence is almost always still in the post body, so these are
rebuilt from the body's first real prose - headings, code fences, images
and list markers skipped.

STRICT on purpose: a description is only written when whole sentences
fit the 158-char budget. My first attempt fell back to a word-boundary
cut and I inspected the output before shipping it - roughly a quarter
ended in dangling phrases:

  "...and create not just a good headline, but a catchy one? No matter
   what your content type is, and if you're either writing a small"
  "...to get the results you envisioned for your"

That is worse than the ellipsis it replaced, because nothing signals to
the reader that the text was cut. So the rule is now sentence-boundary
or skip.

Cost of the stricter rule: 141 rebuilt instead of 220, and 92 left for a
human rather than auto-filled badly. Verified mechanically across all
141 written: zero end in '...', zero end without terminal punctuation.

Remaining 92 need a written description - they are posts whose opening
prose has no complete sentence inside the budget. Not attempted here.

bin/hugo-build: 8/8 validators.

* feat(content): R2 - RubyLLM vs langchainrb for Rails

Queue row R2. Researched by unpacking both gems, not from memory:
ruby_llm 1.16.0, langchainrb 0.19.5, langchainrb_rails 0.1.12.

The thesis came out of the file lists rather than a feature survey. One
gem ships chat/agent/embedding/cost/model-registry; the other ships
vectorsearch/chunker/loader/output_parsers/evals. They are not competing
implementations of one job, so the decision is "do you retrieve from your
own documents", not "which is better". That also satisfies the plan's
constraint that R2 must LINK the LangChain guides rather than cannibalize
them.

Three critics plus a cold-eyes gate ran. What they caught:

- PROVIDER COUNT WAS WRONG. I wrote 10 for ruby_llm; it registers 13
  (lib/ruby_llm.rb:104-116). My `ls providers/ | head -20` truncated the
  listing and I read the cut-off result as complete - the same error as
  an undersized grep window earlier in the session. The correct number
  is better for the post: 13 vs 13 is a tie, so the row settles nothing,
  which is the point. The old draft warned against choosing on provider
  count while printing a wrong one.

- THE STABILITY ROW WAS A CHEAP SHOT. "past 1.0 vs pre-1.0" implied
  quiet upgrades. ruby_llm ships upgrade_to_v1_7/v1_9/v1_10/v1_14
  generators and an acts_as_legacy shim - four schema migrations inside
  minor releases. Now symmetric: budget for migrations on either.

- I DISMISSED A GEM I HAD NOT OPENED. The draft waved at
  langchainrb_rails 0.1.12 with "that version tells you what to expect".
  Opening it proved me wrong: four generators (pgvector, pinecone,
  chroma, prompt), an ActiveRecord hook, a Railtie. The truth argues the
  thesis better than the smirk did - ruby_llm's generators scaffold a
  conversation, langchainrb_rails' scaffold a vector store.

- COST CLAIM OVERSTATED. langchainrb does ship prompt/completion/total
  token counts; what it lacks is price and the cache/thinking split.
  "You supply the price table" replaces "you fly blind".

- FIVE BANNED DEFINITIONAL CONSTRUCTIONS, one slogany flip, one negative
  parallelism, one "teams add last and wish they had added first"
  chiasmus. All removed; sweep now returns zero.

Rejected three critic rewrites that would have fabricated evidence - an
invented client bill going "$340 to $1,900", a count of "two of the last
four apps", an OpenAI format change "found in production". None
happened. Used the one real published incident instead (nine schemas,
1549 green tests, VCR matching on method and URI only) and checked my
citation against the source post, which caught me inflating it to "nine
agent pipelines" when it was nine schemas in one pipeline.

Model id in the sample is claude-sonnet-4-5, not gpt-4o. gpt-4o still
resolves in the registry, but the gem's own README uses a current model
and this post argues that models get retired underneath you.

Gates: bin/hugo-build 8/8. check-post-visuals back at floor 72 (the
decision tree, which routes rather than restating the table). Mermaid
pre-rendered, 499.9px viewBox = 9.36px at 390px against a 9px floor.
Rendered scroll gate at 390px: zero console errors, no page overflow.

Cover generated and verified in rendered output - cold-eyes caught that
the missing cover.png was falling back SILENTLY to the site default
og-default.jpg with no build error. og:image now resolves to the real
file.

Dropped the Raw HTTP column from the table: at 390px it rendered 520px
wide in a 464px container with overflow-x visible, so the whole column
was clipped and unreachable. Its cells all read "you write it" and it
has its own section. Table now fits exactly at 464.

Noted, not fixed: that clipping is a theme-level issue, not specific to
this post - wide tables have no scroll container. Needs its own change
plus a visual regression run.

* feat(content): R3 rescoped - what RubyLLM retries, and for how long

The queue row R3 read "rate limits, token budgets, retries, streaming
into Turbo". Audited against content/blog/ before drafting and three of
those four were already owned by posts that shipped AFTER the row was
groomed:

  token budgets  -> cost-optimization-llm-applications-token-management
  streaming      -> fibers-async-ruby-llm-streaming-rails
  rate limits    -> same post, "Rate limiting the upstream calls"

Writing it as specified would have cannibalised two posts - the same
collision that killed R9 and redirected the Kamal work. Only retries
were unclaimed, so the post is retry semantics. Verdict recorded in
20.09 and the row retired.

Researched by unpacking ruby_llm 1.16.0 and faraday-retry 2.4.0. I was
wrong twice before reading them, which is the reason the post exists:

1. Guessed the 0.1s interval was too fast for a rate limit. Wrong -
   faraday-retry reads Retry-After AND the rate-limit reset header and
   takes the larger, so a 429 gets the provider's number.
2. Then assumed a sane ceiling on that wait. Wrong - max_interval
   defaults to Float::MAX (middleware.rb:55) and ruby_llm never sets it,
   so the guard that would abandon an over-long wait never fires.

The other half is connection.rb:111 adding :post back to Faraday's
IDEMPOTENT_METHODS, which excludes POST by design. Correct for chat
completions, dangerous for a tool call with a side effect.

Fact-checker verified all eight core claims against source and caught:

- retries vs attempts off-by-one (max: 3 is three RETRIES, four attempts)
- request_timeout IS exposed and defaults to 300s. My draft said you
  cannot configure your way out; the real worst case stacks four
  attempts x 300s against up to three Retry-After waits, so well over
  half an hour rather than the five minutes one Retry-After suggests
- "would never retry anything at all" was false - a GET for the model
  list still retries. Narrowed to "a single completion"
- Faraday::RetriableResponse is listed but inert, because retry_statuses
  is never set. Source-true, behaviour-false - now labelled dormant
- the OpenAI source link implied provider header behaviour I had not
  verified. Replaced with a note telling the reader to check their own
  provider's docs

Cold-eyes caught the worst one: I cited our own pool-exhaustion post as
evidence for what happens when a model call is NOT in a job. That
incident happened INSIDE a job. It now reads "a job is not a free pass
either", which also resolves a contradiction with "inside a Sidekiq job
that is fine" three paragraphs earlier. It also pulled the model-
retirement claim back to what the source post actually supports,
matching the correction shipped in #509.

Gates: bin/hugo-build 8/8. check-post-visuals at floor 72 (decision
diagram, 257px viewBox - 12px text renders true-size at 390px, clear of
the 9px floor). Rendered scroll gate at 390px: zero console errors, no
page overflow, table fits at 464. og:image resolves to the post's own
cover derivative, verified in built HTML rather than assumed.

Ran two critics rather than four - the fact-checker and cold-eyes, which
between them caught every shipping defect on R2 while the style critics
caught style. Stating the reduction rather than implying full coverage.

* style(content): plainer English in R2 and R3, and drop the padded source lists

Paul asked for plainer English and less AI-feel. The voice guide's first
gate is exactly this - a sentence the reader has to decode has already
failed, no matter how it scores on the mechanical checks.

The sentences that needed it were the ones carrying a metaphor where a
plain word would do, or two ideas welded together:

  "Both gems brought their own centre of gravity into Rails"
    -> "Each gem brought the thing it is good at into Rails"
  "Providers are a tie at thirteen each, so that row settles nothing"
    -> "Both ship thirteen providers, so that row will not help you choose"
  "Neither gem promises you a quiet upgrade"
    -> "Neither one gives you upgrades for free"
  "That failure is quieter than it sounds"
    -> "That kind of change is easy to miss"
  "A second gem constructing its own requests doubles the surface"
    -> "twice as many places for that to hide"
  "What you keep writing yourself is the boring layer"
    -> "What you write yourself is the dull but necessary part"
  "Source-true, behaviour-inert"
    -> "It is in the code, but nothing reaches it"

Also removed both Sources blocks. Paul flagged them as redundant and the
slop critic had said the same thing earlier - six links, every one a
first-party vendor page the reader can find from the gem name. A trailing
bibliography reads as generated; thoughtbot links where the claim lives.
Each post now ends with one line naming the two things actually read and
telling the reader to check their own installed versions.

Facts unchanged: 13/13 providers, the 1549-test incident, Float::MAX, the
0.1/0.2/0.4 schedule, every version number. bin/hugo-build 8/8.

Tension worth noting for later: #510 standardised 20 posts onto a
"## Sources" heading. That was about naming lists consistently, not about
whether they should exist. This commit is the other half - do not pad one
with generic vendor links just because the heading is there.

* docs(voice): the plain-English rule needed a tech-stream example

Section 0 had one worked example, from a first-person LinkedIn post,
where the defects were borrowed drama and inverted causality. Today's
pass on two tech posts hit a different family, and it is the one that
recurs in the Rails stream: an abstraction standing where a plain word
fits.

Added the seven-row table of what shipped vs what it became - 'centre of
gravity', 'settles nothing', 'quiet upgrade', 'doubles the surface',
'Source-true, behaviour-inert'. Every one of those passed banned-word,
em-dash and slop checks. They fail the only test that matters: the
reader has to translate before they can use the sentence.

Named the tell so it is greppable in review: a noun phrase doing a
verb's job. 'Centre of gravity', 'the surface', 'the boring layer' all
gesture at a shape instead of saying what happens. The fix is the
concrete verb, after which the metaphor is unnecessary.

Also added a 'padded source list' row to the structural-patterns table.
Six first-party vendor links shipped on a draft today; Paul flagged them
as redundant and a slop critic had called the shape a generated-text
tell earlier in the same session. Worth recording because #510 had just
standardised 20 posts onto a '## Sources' heading - making a section
cheap to add is what makes padding it easy, so the naming convention and
this rule have to travel together.

* fix(seo): the description repairs would have been reverted in ten minutes

A Codex review found a P1 that made the whole SEO effort a no-op, plus a
bug in my own generator. Both verified in source before fixing.

1. THE REPAIRS WERE NOT DURABLE. lib/sync/post.rb:39-45 re-pulls title
   and description from dev.to unless the frontmatter carries
   `seo_override: true`. Its own comment says "the 10-min sync cron
   clobbers any locally-edited SEO snippet". None of the 141 files I
   edited had the flag, so every rebuilt description and the hand-written
   SimpleCov title would have been reverted within ten minutes of merge -
   silently, with the commits still in history looking like they worked.

   All 140 dev.to-backed files now set it, verified by script: 140 with
   the flag, 0 missing. The SimpleCov post gets it too, since its title
   and description were both hand-written.

2. MY GENERATOR ATE UNDERSCORES. clean() ran gsub(/[*_`]/, "") to strip
   markdown emphasis, which also stripped underscores inside identifiers.
   `stringify_keys` shipped as "stringifykeys" - destroying the exact
   keyword that post targets, in the description Google reads.

   Emphasis is now stripped only when the markers actually wrap
   something, and inline code is unwrapped rather than deleted. Blast
   radius measured rather than assumed: exactly 1 of 141 descriptions
   contained an underscore, so one post was mangled, not many. All 141
   were restored to their pre-commit state and regenerated from source
   with the corrected cleaner - 140 rebuilt, 93 left for a human.

Two factual corrections in the new posts, both verified in the gems:

- R2 claimed langchainrb gives token counts "on every response". The
  Hugging Face, llama.cpp and Replicate response classes never override
  those methods, so they inherit base_response and raise
  NotImplementedError. Now scoped to the mainstream adapters, with the
  exceptions named.

- R3 said a retried POST could duplicate a row write or an email. It
  cannot. chat.rb:233 runs handle_tool_calls only after a response
  returns, and the retry middleware wraps the HTTP request below that -
  so a lost response can double-charge the completion, but the first
  attempt never reached the tool. Duplicate side effects come from
  retrying the job around the call. The post now says so.

Local macOS suite (not CI): 34 runs, 87 assertions, 0 failures, and 53
screenshots compared with no failures. That also answers the CI
Screenshot Tests failure - footer and CTA regions on homepage/services
do not reproduce locally, and the linux baselines date from #494 with
design work landed since (#503 tokens, #508 palette). Drift, not this
diff.

bin/hugo-build 8/8.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant