A product that learns from you doesn't break loudly — it goes quiet
Kindling's argument is that every edit teaches it. For a stretch of posts it learned nothing: a column from an unapplied migration made the read fail, and nothing said so. Its scoring had gone uniform too — an active account got the same boost as an empty one. Here is what catches that.
A product that learns from you does not break loudly. It goes quiet.
For a stretch of Kindling's life, every post the author edited taught it nothing. No error page. No alert. Drafts still arrived, the queue still moved, the interface looked exactly as it had the week before. What had stopped was the part that reads your edits and carries them into the next draft — and in a product whose whole argument is that every edit teaches, that part is the product.
The answer, before the detail: what catches this class of failure is not better logging. It is a check that knows what personal is supposed to look like — one that can say "this output no longer depends on the person" and "this check could not run", out loud, without a human going to look.
What Kindling does for the person using it
Kindling is for the specialist who knows their field, wants a personal brand, and will not publish something a model wrote for them. It opens with two deliberately polarized takes, you react in your own language, and it shapes that reaction into a post that carries your intonation into English. You edit. You publish. It learns from the edit. The case page reports the first post built this way at 1,708 impressions — 4.5× that author's usual reach — and frames it as the author's result, not a promise.
So "it learns from your edits" is not a feature line here. It is the thing being sold — which is why the failure below is worth a post and a single fixed bug would not be.
One cause, three costumes
In one week we logged three unrelated incidents: post ratings failing, profile saves and onboarding failing, and a topic feed frozen for nine days with thirty-two rows waiting in the queue. Each was written up as a database hiccup. Three hiccups in a week is not a pattern anyone chases.
They were one cause. Schema lives in two places that ship on different schedules, and an ordinary CI run compares neither against the other.
repository 32 migration files
live database 18 applied
gap 14
11 database functions the code called did not exist at all
symptom 1 post ratings fail
symptom 2 profile save + onboarding fail
symptom 3 topic feed frozen 9 days, 32 rows waiting
logged as "database hiccup" x3The fourth was the quiet one, and it never produced a symptom at all. A read on the publish path selected a column defined by migration 0027. It never used the value — the field was in the select list for tidiness. Postgres does not care whether you read a column; it fails on the ones you asked for. The read came back as error 42703 and the function threw before its first step, so post-publication processing had not run for any post published since: no distillation of the author's edits, no aggregation of learned patterns, no lifecycle check, no vector for the archive.
Dropping the field from the select list restored four of the five steps. The fifth depends on the unapplied migration and stays broken until that migration lands — the honest split, not a fix we can claim in full.
The personalization that went uniform
The second discovery is the one we did not expect.
A score clamped with a min of a max returns the upper bound for every input the moment the lower bound exceeds it. No exception, no log, no visible defect. The output simply stops depending on the person.
score = min(max(raw, lower), upper)
lower > upper -> returns upper for every input, always
no exception, no log, nothing in the UI
measured on the live shape:
account with real activity + a live topic -> same boost
empty account -> same boost
and a negative weight, same failure in different clothes:
publishing on a topic LOWERED the band it was meant to raise
four settings blocks had:
no declared domain (probability? count? days?)
no ordering invariant against a sibling numberRead that middle line as a buyer would. Every user would have been handed the identical mix, by a product whose reason to exist is that the mix is yours.
The guard we shipped is not "validate the config". Inside a covered block, every number must declare a domain and every domain must have a number. The ordering invariant is checked after the admin's patch is merged with code defaults, because an admin edit usually carries one side of a pair. An unknown numeric key is rejected outright — previously a misspelled key merged into nothing, changed nothing, and looked like a saved setting.
Taste stays open on purpose: a zero weight, zero queries and a zero half-life all still pass. Thirteen further blocks were left on the weaker rule — the number must be finite — and written down as owed work rather than handed invented domains. Our view: forbidding a setting whose meaning you do not know is its own kind of bug, and naming the debt is cheaper than pretending the coverage is complete. One more note on honesty: a reviewer also flagged a division by zero. It did not reproduce. We settled it by running the code, not by reading it.
We were half right about the check
A check has three outcomes, not two: passed, failed, and could not run. Folding the third into either of the other two destroys it.
Our first correction was worse than the bug, and it took twenty minutes to show.
before could-not-run -> green skip
(a skipped line inside a green job is a line nobody opens;
this is how a 14-migration gap sat unnoticed)
fix #1 could-not-run -> fail, every commit
no credentials in the repo -> a red no commit could fix
20 minutes later the signal meant nothing again
now no credentials -> annotation on run + commit, run stays green,
plus one scheduled red per week
credentials present,
tool not answering -> fail, always
(configured-and-broken != not-configured-yet)
boundary a red pipeline blocks nothing here: branch protection is unavailable
on the current plan and the host deploys on push without waiting.
The check buys visibility. Enforcement is bought separately.The rule we took from it: route the third state by who can act on it and when, not by severity.
The direction of the drift matters too. Repository ahead of database is a deploy about to break — fail the build. Database ahead of repository means somebody applied something outside the process — loud, but not fatal, or the check stays red forever and everyone learns to ignore it.
The one still open
We would rather publish the unfinished edge than a clean ending.
unique index over a nullable key
in Postgres two NULLs are never equal
-> unlimited duplicates allowed on rows with no external key
-> which is exactly the rows the system invents itself:
the calendar trigger, and the cold start
the two guards we believed in were reads, not locks:
"already served this month" -> a read
semantic filter -> a read
two concurrent requests walk straight past both
shipped: cross-instance lock, one refill per user per 60 s
60 s = the route's own max duration, so the lock cannot
outlive the work it guards
second caller is served what is already queued, not an error
fails soft if the shared cache is missing, unreachable, or
slower than 700 ms — refusing to refill on a degraded lock
turns a duplicate row into an empty feed, and the empty
feed is the worse outcome
recorded as: a narrowing of the window, not a guarantee.
The index that closes the class is in a migration
that has not been applied.Why a founder should care about any of this
When a technical buyer hears "AI agency", the objection is not that the studio will fold. It is that the people in it have no software development, project management or security discipline — that the category was chosen because it looked easy. That objection is competence, not survival, and in our experience it is the one a buyer will say out loud.
Our view: a capability demo does not touch it. What answers it is a named live system and a straight account of how failures get caught and rolled back — including the ones nobody would have found out about.
FAQ
How do I check this in my own project tonight? Ask the database what it has actually applied, diff that against the migration files in the repository, in both directions, and count the gap. Ours was fourteen.
Why not just fail the build whenever the check can't run? We tried that first. With no credentials in the repository it failed on every commit for a reason no commit could fix, so the next work arrived into a red pipeline and the signal was worthless inside twenty minutes. A red that nobody can clear is the same decoration as a green skip, in a different color.
Did users feel this? Yes, in three ways in one week — ratings, profile saves, a feed frozen for nine days — and in one way nobody could feel at all, which was the learning being off. We think the invisible one was the expensive one.
Is the scoring problem fully closed? No. Four settings blocks are covered by the new guard; thirteen more are still on the weaker rule and recorded as debt. We would rather name the number than imply full coverage.
Doesn't publishing your own failures cost you work? Our view: it is the only version of this post that proves anything. "We found a bug, we fixed it" would say only that we write bugs like everyone else. What we learned here is that a product built to adapt to a person fails by going uniform, silently, and that the check has to be written against the thing being sold — not against the stack.
Try it
Kindling is live. Two polarized takes, your reaction in your own language, a post in English that still sounds like you — and no score anywhere in the interface, because a number invites you to perform for the meter instead of saying what you mean. It is at kindling.iloblique.com.
If you have something worth building, we'd like to hear about it.