Cylinder Misfire
Diagnostic reasoning for independent auto shops
Live at cylindermisfire.com
This page covers the architecture and the decisions behind it. The product itself is here.
A check engine light isn't a diagnosis. It's a rumor. The scan tool turns that rumor into a code — P0301, cylinder 1 misfire — and then, having discharged its duty to the trade, says nothing about which of the eleven plausible causes is the one in front of you.
Diagnosis is the unpaid part of the job
I built this with a friend who does the work for a living — an engine and auto mechanic — and the thesis is his. A shop gets paid for the repair. The hours spent working out what the repair is get discounted, quoted flat, or absorbed to win the job.
| What the job takes | On the invoice |
|---|---|
| Reproduce the fault | not billed |
| Isolate the root cause | not billed |
| Quote the work | not billed |
| Perform the repair | billed |
So competence is punished: build a reputation for solving the weird ones and the weird ones find you, and an intermittent fault can eat a day that never appears on an invoice. Then the customer takes your diagnosis — the part that took the skill — down the road to whoever fits the part cheaper. You've done unpaid consultancy for a competitor, in your own bay, on your own scan tool.
The target was never “can a model talk about cars”. It was: compress the unpaid part of the day. Everything below falls out of that.
The thing it was never going to be
A hard constraint from the first conversation: not a chat box wired to a language model. Not out of snobbery about chat — the economics forbid it. A plausible wrong answer doesn't save unpaid time, it manufactures it: a part that didn't need replacing, an afternoon in the wrong subsystem, and a customer back with the same fault and a worse attitude. Nobody bills that hour either.
Which makes the usual failure mode disqualifying rather than annoying. Ask a bare model about a common code and it does well; ask about one outside the well-trodden part of its training data and it doesn't say “I don't know” — it produces a description. Confidently, in the right format, with the right sort of words in it. A wrong answer that looks wrong gets caught. One that looks exactly like a right answer gets fitted to someone's car. So the reasoning is grounded by construction: retrieve real complaints, service bulletins and recalls for that specific vehicle, and make the model reason over documents instead of recollection.
The dictionary that couldn't spell misfire
Grounding needs a trustworthy code dictionary, and the open-source ones are community data — mostly excellent, occasionally deranged. One CSV I was about to import had its columns quietly misaligned. Under P0300 — random/multiple cylinder misfire, the code this product is effectively named after — it offered “Cylinder 12 Contribution”. Most engines in a shop don't have twelve cylinders. The ones that do are rarely in for a misfire; they're in for something with a longer invoice.
That one got caught because P0300 is unmissable. The other three thousand wouldn't have been — and that's the real hazard with retrieval. A bad row in a normal table is a bug you can find. A bad row embedded into a vector index is a ghost: it shapes every future retrieval, silently, and you can't grep for “subtly wrong”. So codes carry two independent trust flags, community-verified and OEM-verified, and nothing gets embedded on community trust alone. That left most of the dictionary sitting outside the index waiting on verification. Smaller corpus, much better sleep.
Getting a scan to start itself
My first BLE design had the client open a session by telling the server what it was about to scan — VIN, adapter fingerprint. Reasonable, until you notice that reading the VIN and fingerprinting the adapter are themselves scan operations. I'd designed a chicken that required an egg.
The second problem was time. You can't ask a vehicle what it supports by trying everything:
A tool that saves unpaid time by spending fourteen minutes of it up front isn't a tool, it's a hobby. Both problems have the same fix — split the scan, and let the first phase be dumb on purpose.
Phase 0 is a fixed sequence baked into the client: adapter identification, ELM327 init, the supported-PID bitmasks, VIN, stored codes. It's identical on every OBD-II vehicle since 1996 because the standard mandates it, so it needs no fetching, no negotiation, and no network — which matters, because the interesting bays are always the ones farthest from the router. Only once it's produced a VIN and the capability bitmasks does the server tailor the rest. Every step writes to IndexedDB, so a scan that dies halfway resumes instead of restarting.
Making the answer a contract
Diagnoses come back through a tool-use schema, not as prose the server parses — parsing model prose is fine in a demo and a slow leak in production. The schema uses short field names, which reads worse and roughly halves the output tokens; the model pays by the character to say confidence several hundred times a day.
The response shape changed once, permanently, because of a real car. A CR-V came through testing with two unrelated faults — fuel dilution in the powertrain and a miscalibrated radar — and I had exactly one slot for a primary diagnosis. I could have demoted the second to an “additional observation” and kept the tidy hierarchy, but a car with two problems doesn't have one problem and a footnote. It returns a ranked array now.
Caching answers that aren't identical
The same engine family throws the same codes all week, but live sensor readings are never byte-identical, so a naive cache key never hits. The key is built from what actually changes the answer, with sensor values bucketed so two scans of one fault land on the same entry, and free-text complaints hashed so it stays bounded. It's stamped with a prompt version too, because the subtlest way to ship a bug is to improve a prompt and keep serving month-old reasoning from the version you improved away from. Cache invalidation retains its reputation.
That produced my favorite bug here. A technician can re-run a diagnosis with edits, creating a child session linked to its parent so the history shows how the thinking evolved. But if the edit didn't change anything the key cared about, the child hit the parent's cached row and quietly folded into the parent's history slot instead of appearing as its own session. The lineage looked immaculate. It was also fiction.
Where it is now
Live at cylindermisfire.com, with the diagnose loop, BLE scanning, session history and lineage, and ADAS calibration checks working end to end. Almost none of the hard parts were the AI call. They were the boring questions around it — is this data trustworthy enough to embed, can this run with no signal, does this cache entry still mean what it meant last week — which tracks, because none of those are AI problems.
The unglamorous lesson: most of the work in putting a model in front of a professional is earning the right to have it there. The demo takes an afternoon. Everything after the demo is deciding what happens when it's wrong.