KotaPortal's Palette Was Empty and Nothing Ever Complained
backgroundColor in the file, background-color in the reader: both legal, never meeting. An empty palette with zero errors anywhere.
TL;DR
MockForge's capture emitted camelCase JSON keys, but the derivation step looked up kebab-case names, yielding an empty palette and fonts with zero errors. Every layer was individually correct: getComputedStyle supports both spellings, JSON allows any keys, and getPropertyValue returns empty strings on misses. Fix: producer now emits kebab-case keys, plus a guard script validating shape at the boundary.
Last Thursday afternoon I opened the index.md that MockForge generates after a design-capture run. This one belonged to KotaPortal, a municipal services portal I was rebuilding. The palette section was blank. The fonts list was an empty array. I flipped to the build log expecting a wall of red and found zero errors.
An error I can fix. Empty output with zero errors means somewhere in the pipeline, data is being lost politely.
First Guess: The Capture Was Broken
My working theory was that the capture step had produced nothing. The KotaPortal page had plenty of styles, obviously, so my second theory was that the capture was somehow running against the wrong page, one with no computed styles at all.
I re-ran the capture against a test page with a loud orange button and opened the JSON fragment. The styles were right there: backgroundColor, borderRadius, fontFamily, all populated with sensible values. So the capture was fine. The data existed.
It just wasn't in the shape the next step expected.
The derivation step that builds the design system reads exact kebab-case names: background-color, border-radius, font-family. It looked each one up, missed every time, and collected empty strings. An empty palette and fonts: [] weren't corruption. They were the honest result of a lookup that never matched.
Why Every Layer Stayed Silent
Here's what bothered me: three tools touched this data and none of them flagged anything. Once I traced it, each one was behaving correctly by its own rules.
The capture uses getComputedStyle(), which returns a live CSSStyleDeclaration object [3]. That object exposes property names in both dashed and camel-cased form, so style.backgroundColor and style["background-color"] both work [2]. When I sanity-checked the capture early on, dot access returned values and looked healthy. No reason to suspect anything.
The file itself was valid too. JSON lets any string be a member name; backgroundColor is a perfectly legal key [4]. Nothing in the JSON spec cares that it disagrees with CSS spelling.
And the derivation didn't crash because getPropertyValue() doesn't throw on a miss. Per MDN, if the property isn't set, it returns the empty string [1]. So "background-color" in, "" out, palette empty, exit code zero. Every component locally correct. The failure lived entirely in the gap between two naming conventions, and gaps don't log errors.
That's the part I keep coming back to. A crash would have found this in five minutes. Valid data with the wrong shape found it three days later, in a generated document, at 4pm.
The Fix: Get the Shape Right at the Producer
Two changes, in order of importance.
First, the capture now writes kebab-case keys. The consumer defines the contract, and the consumer wants CSS names. I stopped converting to camelCase keys during serialization entirely; the browser gives me dashed accessors, so there was never a reason to translate in the first place.
Second, a guard script that runs before derivation and refuses to let mismatched keys through:
// check-keys.mjs (make sure every fragment key is kebab-case)
import { readFileSync, readdirSync } from "node:fs";
// run from the project root
const dir = "./capture/fragments";
for (const f of readdirSync(dir).filter((n) => n.endsWith(".json"))) {
const data = JSON.parse(readFileSync(`${dir}/${f}`, "utf8"));
for (const key of Object.keys(data.styles ?? {})) {
if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(key)) {
console.error(`${f}: key tidak kebab-case -> ${key}`);
}
}
}Point the pipeline at this before the derivation step. If it prints anything, stop and fix the fragment. The regex accepts background-color and rejects backgroundColor, Background-Color, and anything weirder.
I considered validating inside the derivation itself instead, but that hides the problem at the wrong layer. The producer should emit the agreed shape; the check script exists so the next shape drift fails loudly and immediately, not in a generated markdown file a human has to notice.
What I Changed About My Process
I used to trust "it parses, it runs" as evidence a handoff between two of my own tools is fine. Now I treat every boundary in that pipeline as a contract that needs one explicit assertion, because this bug proved that locally-correct components can agree on validity and still disagree on meaning. The check script took fifteen minutes to write. Finding the empty palette took an afternoon and cost me some confidence in my own build log.
The build log says zero errors. It has never meant zero problems.