Pattern skill for keeping specific knowledge (dataset names, column encodings, layout) in data and only general mechanisms in code — the direction sow-tools must move to be open-sourceable. Grounded in the value_encodings mode zoo and the skills-dataset rename breakage; baseline-tested against a subagent that reproduced both failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3.5 KiB
name, description
| name | description |
|---|---|
| data-driven-design | Use when adding a mode, switch case, config entry, special-cased name, or per-column/per-dataset handler to a tool that processes authored data — or when a tool "knows" a project's layout, dataset names, key prefixes, or format quirks and could not be open sourced as-is. |
Data-Driven Design
The tool implements general mechanisms; the data declares all specific knowledge. Every time code names a thing that lives in data — a dataset, a column, a format quirk — the tool stops being a tool and becomes a private extension of one project's layout.
The test: could this repo be open sourced and build a different project's data without code changes? Every hardcoded name, mode, and special case is a "no".
The two smells
1. The mode zoo. A new data shape arrives and you add a named mode plus a switch case:
- {column: AvailableHeadsMale, mode: external_hex_list, hex_width: 3}
- {column: PreferredAlignments, mode: alignment_set_mask, hex_width: 3}
- {column: ProficiencyFeats, mode: id_hex_list, hex_width: 4}
Three "modes" are one concept — a set of ids with an engine-side encoding — wearing three names. Each mode is code the data could have been. The fix is a smaller vocabulary of orthogonal primitives declared where the column is defined:
"EquipSlots": {"shape": "id_set", "encode": "bitmask", "max": 31}
One parser for id_set, one encoder per encoding. Adding the next column is a data edit, zero code. The mode zoo shrinks: N special cases collapse into shape × encoding.
2. Names in code. datasetRows("skills"), case "skills":, TrimPrefix(key, "skills:") — the tool assumes the project's layout. When data moves (skills → skills/core), the tool breaks even though the data is self-consistent. Names must flow from the data: the spec that references a dataset names it; a key's namespace is whatever precedes its own colon. If code needs a name, some piece of data already knows it — read it from there.
Deciding
Ask of every constant, case, and config knob: whose knowledge is this?
- The mechanism's (how to parse JSON, allocate rows, write a table) → code.
- The data's (which columns exist, how the engine encodes them, what things are called) → data, even if code is faster to write today.
Generalize only when it deletes: a primitive that replaces N modes is lazy; a framework "for future shapes" is not. One new shape may take its case if the case is spelled as a reusable primitive the next shape can declare.
Rationalizations
| Excuse | Reality |
|---|---|
| "Just follow the existing pattern, it's proven" | The pattern is the defect. Each repetition raises the cost of the real fix. |
| "N cases isn't a crisis yet" | The crisis is per-case: every one couples the tool to one project forever. |
| "Release is tonight, generalize later" | Later never comes; tonight's mode is tomorrow's baseline. Declaring shape in data is usually the same hour of work. |
| "This name never changes" | Today's rename broke exactly such a name. Data moves; mechanisms shouldn't care. |
Red flags — stop and re-shape
- A config entry that names both a dataset and a column to select behavior
- A new value in a
mode/kind/typestring enum with its own switch arm - A literal in code that also appears in the data tree (
"skills","skills:") - Per-project constants (row ranges, hex widths, id ceilings) living in Go instead of the dataset that owns them