One Source of Truth
Code is cheap to regenerate. Recorded data cannot be un-recorded, and a year of orders written against a schema that never made sense is the most expensive bug there is. Deciding what data means is the step AI-assisted workflows skip: you ask for a feature, you get a feature, and nobody asks what a "product" is supposed to mean here. This skill makes you the developer who asks, decides, writes the answer down, and adds the test that keeps it true.
Rot Patterns
Five ways a data model rots. Use these slugs in reports and decision records.
duplicate-concept: one business idea lives in multiple tables under different names (items vs products vs menu_entries), created at different points in the build. Detect: near-synonym table names, overlapping column sets, code that writes to one and reads from another.
undecided-source-of-truth: a value is stored in two places with no declared master, so an edit in one silently diverges from or rewrites the other. Detect: same-shaped columns in related tables, denormalized copies with no constraint, no comment, and no sync logic.
snapshot-vs-reference-ambiguity: a child record either copies a value at write time (immutable snapshot) or points at a live row (reference), and nobody decided which. Editing the menu rewrites what old orders cost. Detect: a foreign key to a mutable row where the domain implies history (orders, invoices, payroll, logs), or a copied value alongside a foreign key to its source.
meaning-drift: an enum or status column accumulated values with overlapping or forgotten semantics ("pending" vs "processing" vs "queued"), or a field's real meaning changed while its name did not. Detect: enums checked inconsistently across code paths, status values written but never read.
half-wired-relation: a table or relation that was started and abandoned. Detect: foreign keys only some code paths populate, join tables with no readers, columns never written outside one migration, relations traversed in only one direction.
Finding the Schema
Look in this order and stop when you have the full picture:
- Declarative schema files:
prisma/schema.prisma, drizzle schema.ts, db/schema.rb, schema.sql, structure.sql, *.dbml.
- ORM model directories:
app/models/ (Rails), models.py per Django app, SQLAlchemy models, TypeORM or MikroORM entities, app/Models/ (Laravel).
- Migration directories:
migrations/, db/migrate/, prisma/migrations/, alembic/versions/. Migrations show when duplicates were introduced, which the current schema alone cannot.
- Raw SQL fallback: search for
CREATE TABLE across *.sql.
- Non-SQL stores: Mongoose schemas in
models/, Firestore or DynamoDB access wrappers. When there is no schema file, the write paths are the schema, so search for the collection or table names.
Always also check the repo root and docs/ for an existing DATA-MODEL.md. If you find no schema at all, say so and ask where persisted data lives instead of guessing.
Guardrail Mode
Run this before any task that adds or changes tables, models, migrations, or persisted fields, even when the request never mentions the schema. "Add a discounts feature" implies persisted data; that is enough.
- Locate the schema and read
DATA-MODEL.md if it exists. Recorded decisions are binding unless the user explicitly overrides them.
- Map the concepts the task touches: list existing tables and models that overlap the new data in name or shape.
- Say the feature back in data-model terms before implementing: "Discounts means the
orders table gains a discount_amount, snapshotted at checkout." One or two plain sentences. A bare "I want [feature]" is a spec for behavior, not for meaning; this sentence is where the user catches the mismatch between what they meant and what the schema is about to say.
- Resist additive bias. Adding a table or column is not the safe default; it is how duplicate concepts and half-wired relations are born. Before any addition, check whether an existing table or column should be modified, merged, or removed instead, and prefer the change that leaves the schema smaller or the same size. Creating a structure parallel to an existing concept requires an explicit user decision. If the task reveals a simplification (two tables that should be one, a column made obsolete), propose it now rather than building around it.
- Ask clarifying questions only where meaning is genuinely ambiguous, three at most. The canonical one: "When X changes later, should records that already captured X keep the old value (snapshot) or show the new one (reference)?" Others worth asking: which table owns this concept, and what happens to dependent records on delete.
- For low-stakes ambiguity, do not block. State the assumption you are making in one line and proceed.
- Record each decision in your response using the Decision Record format, and in
DATA-MODEL.md, creating the file if it does not exist.
- Enforce the decision, do not just record it. A decision that lives only in prose gets re-broken by the next change, because nothing stops a field from being duplicated again. For a snapshot decision, add a database-level guard where the stack supports it (trigger, insert-only column) and always add a regression test in the project's existing test style that mutates the live source and asserts the recorded value is unchanged: edit the menu item, assert an old order total did not move. For an ownership decision, add the uniqueness or foreign key constraint that is missing. Ten minutes of test is the only reason anyone gets to trust the table.
- Only then write the migration and model code, invariants included.
Audit Mode
Run this when asked to audit the data model or check the schema for rot. Never run it uninvited.
- Locate and read the full schema plus
DATA-MODEL.md if present.
- Build a concept inventory: map every table or model to the business concept it represents. Flag any table you cannot name a concept for.
- On large schemas (roughly 25 tables or more), do not sweep everything at once. Audit in passes: first tables recording money, identity, and history (orders, payments, invoices, users, ledgers, audit logs), where rot destroys data; then their direct relations; then the rest. A partial audit must never read as a clean bill of health, so the report always ends by listing the tables not yet audited.
- Sweep all five rot patterns against the inventory. For suspected duplicates and copies, search the code for read and write paths to confirm which side is actually used.
- Rank findings by data damage, not code ugliness: wrong data being recorded right now is high, wrong data recordable on the next edit is medium, merely confusing is low.
- Where a value may have already drifted, say so and provide a one-off reconciliation query. Users who notice numbers moving tend to assume they are misreading the report, so silent drift is often months old by the time anyone asks.
- Produce the Audit Report. Each finding names the simplifying move (merge these tables, drop this column, declare this copy a snapshot), the invariant that would enforce the decision, and the question the user must answer. Never fix findings silently; every resolution is a meaning decision only the user can make.
- Offer to bootstrap or update
DATA-MODEL.md from the findings and to fix findings one at a time.
The Data Dictionary: DATA-MODEL.md
The written-down answers. Create it at the repo root on the first recorded decision, read it at the start of both modes, and update it with every schema decision. One entry per business concept:
## Concept: Order Line
- Meaning: one purchased item on a confirmed order, as sold at that moment
- Owning table: `order_lines`
- Source of truth: `order_lines.price` is an immutable snapshot copied from
`menu_items.price` at order time; never recomputed from the menu
- Related concepts: Menu Item (live catalog entry; price there is editable)
- Enforced by: `tests/orders/price_snapshot.test.ts` (edits a menu item, asserts
an existing order total is unchanged)
- Decided: 2026-08-13, while adding menu price editing
Worked Example
User asks: "Let restaurant owners edit their menu prices."
The naive path is one line: make menu_items.price editable, done. And every past order total that joins through menu_items now silently shows today's prices on last year's receipts.
With this skill you instead:
- Map the concepts:
menu_items, order_lines, and a products table from an earlier build step that only one code path still writes to. Surface that as duplicate-concept before adding anything.
- Say it back: "Editing prices means
menu_items.price becomes mutable, which changes what order_lines joined against it will display."
- Ask the canonical question: "Should a receipt from March still show the March price after you raise prices today?" User: yes, obviously.
- Record the decision: order lines snapshot the price at order time;
menu_items is the live master; products gets merged into menu_items.
- Implement: copy
price onto order_lines at insert, backfill existing rows from current data with an explicit warning that pre-decision history is unrecoverable, and add the regression test that edits a menu item and asserts an old order total is unchanged.
- Write the dictionary entry, including its Enforced by line.
Output Format
After a guardrail decision:
## Data Model Decision
- Concept: [name]
- Question: [what was ambiguous]
- Decision: [snapshot|reference|owner|extend|merge|remove] - [one sentence]
- Enforced by: [constraint, trigger, or test path]
- Recorded in: DATA-MODEL.md
For an audit:
## Data Model Audit
Scanned [n] tables, identified [n] concepts.
### [severity: high|medium|low] [pattern-slug]: [short title]
- Tables: [tables and columns involved]
- Evidence: [what the schema and code paths show]
- Fix: [the simplifying move]
- Invariant: [what would enforce it]
- To resolve: [the question the user must answer]
Clean: [patterns with no findings]
When to Skip
Skip the guardrail questions when:
- The user says "just do it" or is firefighting a production incident
- The change is mechanical with no meaning change: an index, an ORM-tracked rename, a type widening
- The data is explicitly throwaway: spikes, seed data, test fixtures
- The store is a cache or derived view with a declared rebuild path
Even when skipping questions, still state assumptions in one line and update DATA-MODEL.md if the change touches a recorded concept.
1---2name: one-source-of-truth3description: Prevents data model rot in AI-assisted codebases by mapping existing concepts, questioning ambiguous ownership, deciding a single source of truth for every value, and recording what each table and field means in a data dictionary. Use proactively whenever a task adds or changes tables, models, migrations, or persisted fields, even when the user only asked for a feature. Also triggers on "audit my data model", "check my schema for rot", "where is the source of truth", or "update the data dictionary". Do not use for query performance tuning, index-only migrations, non-persisted UI state, or code that reads but never changes the schema.4---56# One Source of Truth78Code is cheap to regenerate. Recorded data cannot be un-recorded, and a year of orders written against a schema that never made sense is the most expensive bug there is. Deciding what data means is the step AI-assisted workflows skip: you ask for a feature, you get a feature, and nobody asks what a "product" is supposed to mean here. This skill makes you the developer who asks, decides, writes the answer down, and adds the test that keeps it true.910## Rot Patterns1112Five ways a data model rots. Use these slugs in reports and decision records.13141. **`duplicate-concept`**: one business idea lives in multiple tables under different names (items vs products vs menu_entries), created at different points in the build. Detect: near-synonym table names, overlapping column sets, code that writes to one and reads from another.152. **`undecided-source-of-truth`**: a value is stored in two places with no declared master, so an edit in one silently diverges from or rewrites the other. Detect: same-shaped columns in related tables, denormalized copies with no constraint, no comment, and no sync logic.163. **`snapshot-vs-reference-ambiguity`**: a child record either copies a value at write time (immutable snapshot) or points at a live row (reference), and nobody decided which. Editing the menu rewrites what old orders cost. Detect: a foreign key to a mutable row where the domain implies history (orders, invoices, payroll, logs), or a copied value alongside a foreign key to its source.174. **`meaning-drift`**: an enum or status column accumulated values with overlapping or forgotten semantics ("pending" vs "processing" vs "queued"), or a field's real meaning changed while its name did not. Detect: enums checked inconsistently across code paths, status values written but never read.185. **`half-wired-relation`**: a table or relation that was started and abandoned. Detect: foreign keys only some code paths populate, join tables with no readers, columns never written outside one migration, relations traversed in only one direction.1920## Finding the Schema2122Look in this order and stop when you have the full picture:23241. Declarative schema files: `prisma/schema.prisma`, drizzle `schema.ts`, `db/schema.rb`, `schema.sql`, `structure.sql`, `*.dbml`.252. ORM model directories: `app/models/` (Rails), `models.py` per Django app, SQLAlchemy models, TypeORM or MikroORM entities, `app/Models/` (Laravel).263. Migration directories: `migrations/`, `db/migrate/`, `prisma/migrations/`, `alembic/versions/`. Migrations show when duplicates were introduced, which the current schema alone cannot.274. Raw SQL fallback: search for `CREATE TABLE` across `*.sql`.285. Non-SQL stores: Mongoose schemas in `models/`, Firestore or DynamoDB access wrappers. When there is no schema file, the write paths are the schema, so search for the collection or table names.2930Always also check the repo root and `docs/` for an existing `DATA-MODEL.md`. If you find no schema at all, say so and ask where persisted data lives instead of guessing.3132## Guardrail Mode3334Run this before any task that adds or changes tables, models, migrations, or persisted fields, even when the request never mentions the schema. "Add a discounts feature" implies persisted data; that is enough.35361. Locate the schema and read `DATA-MODEL.md` if it exists. Recorded decisions are binding unless the user explicitly overrides them.372. Map the concepts the task touches: list existing tables and models that overlap the new data in name or shape.383. Say the feature back in data-model terms before implementing: "Discounts means the `orders` table gains a `discount_amount`, snapshotted at checkout." One or two plain sentences. A bare "I want [feature]" is a spec for behavior, not for meaning; this sentence is where the user catches the mismatch between what they meant and what the schema is about to say.394. Resist additive bias. Adding a table or column is not the safe default; it is how duplicate concepts and half-wired relations are born. Before any addition, check whether an existing table or column should be modified, merged, or removed instead, and prefer the change that leaves the schema smaller or the same size. Creating a structure parallel to an existing concept requires an explicit user decision. If the task reveals a simplification (two tables that should be one, a column made obsolete), propose it now rather than building around it.405. Ask clarifying questions only where meaning is genuinely ambiguous, three at most. The canonical one: "When X changes later, should records that already captured X keep the old value (snapshot) or show the new one (reference)?" Others worth asking: which table owns this concept, and what happens to dependent records on delete.416. For low-stakes ambiguity, do not block. State the assumption you are making in one line and proceed.427. Record each decision in your response using the Decision Record format, and in `DATA-MODEL.md`, creating the file if it does not exist.438. Enforce the decision, do not just record it. A decision that lives only in prose gets re-broken by the next change, because nothing stops a field from being duplicated again. For a snapshot decision, add a database-level guard where the stack supports it (trigger, insert-only column) and always add a regression test in the project's existing test style that mutates the live source and asserts the recorded value is unchanged: edit the menu item, assert an old order total did not move. For an ownership decision, add the uniqueness or foreign key constraint that is missing. Ten minutes of test is the only reason anyone gets to trust the table.449. Only then write the migration and model code, invariants included.4546## Audit Mode4748Run this when asked to audit the data model or check the schema for rot. Never run it uninvited.49501. Locate and read the full schema plus `DATA-MODEL.md` if present.512. Build a concept inventory: map every table or model to the business concept it represents. Flag any table you cannot name a concept for.523. On large schemas (roughly 25 tables or more), do not sweep everything at once. Audit in passes: first tables recording money, identity, and history (orders, payments, invoices, users, ledgers, audit logs), where rot destroys data; then their direct relations; then the rest. A partial audit must never read as a clean bill of health, so the report always ends by listing the tables not yet audited.534. Sweep all five rot patterns against the inventory. For suspected duplicates and copies, search the code for read and write paths to confirm which side is actually used.545. Rank findings by data damage, not code ugliness: wrong data being recorded right now is high, wrong data recordable on the next edit is medium, merely confusing is low.556. Where a value may have already drifted, say so and provide a one-off reconciliation query. Users who notice numbers moving tend to assume they are misreading the report, so silent drift is often months old by the time anyone asks.567. Produce the Audit Report. Each finding names the simplifying move (merge these tables, drop this column, declare this copy a snapshot), the invariant that would enforce the decision, and the question the user must answer. Never fix findings silently; every resolution is a meaning decision only the user can make.578. Offer to bootstrap or update `DATA-MODEL.md` from the findings and to fix findings one at a time.5859## The Data Dictionary: DATA-MODEL.md6061The written-down answers. Create it at the repo root on the first recorded decision, read it at the start of both modes, and update it with every schema decision. One entry per business concept:6263```markdown64## Concept: Order Line65- Meaning: one purchased item on a confirmed order, as sold at that moment66- Owning table: `order_lines`67- Source of truth: `order_lines.price` is an immutable snapshot copied from68 `menu_items.price` at order time; never recomputed from the menu69- Related concepts: Menu Item (live catalog entry; price there is editable)70- Enforced by: `tests/orders/price_snapshot.test.ts` (edits a menu item, asserts71 an existing order total is unchanged)72- Decided: 2026-08-13, while adding menu price editing73```7475## Worked Example7677User asks: "Let restaurant owners edit their menu prices."7879The naive path is one line: make `menu_items.price` editable, done. And every past order total that joins through `menu_items` now silently shows today's prices on last year's receipts.8081With this skill you instead:82831. Map the concepts: `menu_items`, `order_lines`, and a `products` table from an earlier build step that only one code path still writes to. Surface that as `duplicate-concept` before adding anything.842. Say it back: "Editing prices means `menu_items.price` becomes mutable, which changes what `order_lines` joined against it will display."853. Ask the canonical question: "Should a receipt from March still show the March price after you raise prices today?" User: yes, obviously.864. Record the decision: order lines snapshot the price at order time; `menu_items` is the live master; `products` gets merged into `menu_items`.875. Implement: copy `price` onto `order_lines` at insert, backfill existing rows from current data with an explicit warning that pre-decision history is unrecoverable, and add the regression test that edits a menu item and asserts an old order total is unchanged.886. Write the dictionary entry, including its Enforced by line.8990## Output Format9192After a guardrail decision:9394```markdown95## Data Model Decision96- Concept: [name]97- Question: [what was ambiguous]98- Decision: [snapshot|reference|owner|extend|merge|remove] - [one sentence]99- Enforced by: [constraint, trigger, or test path]100- Recorded in: DATA-MODEL.md101```102103For an audit:104105```markdown106## Data Model Audit107Scanned [n] tables, identified [n] concepts.108109### [severity: high|medium|low] [pattern-slug]: [short title]110- Tables: [tables and columns involved]111- Evidence: [what the schema and code paths show]112- Fix: [the simplifying move]113- Invariant: [what would enforce it]114- To resolve: [the question the user must answer]115116Clean: [patterns with no findings]117```118119## When to Skip120121Skip the guardrail questions when:122123- The user says "just do it" or is firefighting a production incident124- The change is mechanical with no meaning change: an index, an ORM-tracked rename, a type widening125- The data is explicitly throwaway: spikes, seed data, test fixtures126- The store is a cache or derived view with a declared rebuild path127128Even when skipping questions, still state assumptions in one line and update `DATA-MODEL.md` if the change touches a recorded concept.