snappy-data-hygiene skill
classify field-name context?readpromptreadreview schema-pathwrite-reversible$ npx snappy-skills install snappy-data-hygiene
You are the upstream gate on schema changes. This file is load-bearing and self-sufficient. You may not read other files in this skill directory unless explicitly handed off — every rule you need is here.
Owner: Robert Boulos.
Before a single column, table, settings key, or stored field is added or modified:
"Is this value canonical or derived? If derived, what's the formula, and does the schema make it impossible to store it inconsistently with that formula?"
If you cannot answer cleanly, STOP and surface the ambiguity to the user. Do not proceed to implementation.
Every piece of data in any system is exactly one of these:
| Kind | Definition | Storage | Mutated by |
|---|---|---|---|
| CANONICAL | A fact or choice nothing else determines | A stored column / row | A user action, external event, or explicit write |
| DERIVED | Fully determined by canonical values via a formula | NOT stored — computed at read | The formula alone, never directly |
If a value is "sometimes stored, sometimes computed" — that's a latent bug. It WILL diverge. Pick one.
Common disguises for derived values that people mistakenly store:
*_cached, *_computed, *_summary, *_count without a refresh mechanismfull_name when first_name + last_name existtotal_price when line items + tax rate existInvoke this skill when you see ANY of the following in a request or codebase:
PUT /settings {key, value}) without validating which keys are allowed to be stored. The door is open to all kinds of illegal state.This is Codd's 3rd Normal Form (1971): no non-key column should depend on another non-key column. Storing a derived value alongside its inputs IS a transitive dependency, and it IS the entire reason you have drift.
If you want to sound smart, call it "eliminating transitive dependencies." If you want to sound like Robert, call it "don't store what you can compute."
When reviewing a proposed schema change, run this flow:
image_style = vibes[active_vibe_id].image_style).GENERATED ALWAYS AS ... STORED, a view, a function, no column at all).agent_settings PUT guard pattern).If any of 3/4/5 come back worst-case, you have a 3NF violation in progress.
| Backend | Derived-value enforcement |
|---|---|
| PostgreSQL | GENERATED ALWAYS AS (<expr>) STORED columns. Views. Materialized views with refresh contracts. |
| Xano | No native generated columns. Enforce in app code: one function owns composition (e.g. Config/resolve_scene), PUT endpoints reject derived-key writes with a precondition error naming the composer. |
| MySQL | GENERATED ALWAYS AS (<expr>) STORED / VIRTUAL. |
| Datomic | By construction — no stored derived state. Everything non-fact is a query. |
| dbt / modern data stack | Contract-based models + tests: block. Lineage graphs surface transitive dependencies. |
| Any ORM (Prisma, SQLAlchemy, Drizzle) | Computed/virtual fields if supported. Otherwise, expose derived values only through model methods, never as writable columns. |
For Xano specifically, see the retroactive playbook at ~/.claude/projects/*/memory/funnel-reject-playbook.md when you find an existing violation.
Key-value settings tables (agent_settings, config, meta, preferences) are structurally anti-normal. The moment you create one, you've opted out of schema-level enforcement because the database just sees strings.
Rules when a KV bag exists or is proposed:
db.get where key=X patterns leak across owners.typescriptimport { classifyField, reviewSchema, getReviewPrompt } from "../snappy-data-hygiene/api.ts";
Or CLI:
bashnpx tsx ~/.claude/skills/snappy-data-hygiene/api.ts prompt # returns canonical review prompt text
npx tsx ~/.claude/skills/snappy-data-hygiene/api.ts classify "field_name" "table/context" # classifies a field as canonical/derived/ambiguous
npx tsx ~/.claude/skills/snappy-data-hygiene/api.ts review path/to/schema.json # reviews a proposed schema for violations
| Function | Purpose | ||
|---|---|---|---|
classifyField(name, context) |
Returns `{ classification: "canonical" \ | "derived" \ | "ambiguous", reasoning, formula? }`. Heuristic-based; ambiguous cases must go to human. |
reviewSchema(schema) |
Static review of a proposed schema (array of {name, type, description}). Returns violations list — transitive dependencies, unscoped KV patterns, unenforced derivations. |
||
getReviewPrompt() |
Returns the canonical review prompt for injection into another agent's context. |
When asked to review a schema change, return:
VERDICT: PASS | FAIL
CLASSIFICATION:
- <field>: CANONICAL — <why>
- <field>: DERIVED from <inputs> via <formula> — <enforcement level>
- <field>: AMBIGUOUS — <what to clarify with the user>
VIOLATIONS:
- <field> stored but derivable from <inputs> — 3NF violation, will drift
- <field> write path bypasses <composer> — divergence risk
- KV bag <table> has no write guard — unknown keys will silently land
- (etc.)
REMEDIATION:
<specific steps: which columns to drop, which composer function to write, which write guard to add>
When asked to propose a new schema, return:
SCHEMA:
<the fields with types>
CLASSIFICATION CHECKLIST:
- Every field declared CANONICAL or DERIVED
- Every DERIVED field has explicit formula
- Every DERIVED field enforced at storage or write-guard level
- KV bags (if any) have an allow-list guard declared
REMAINING DECISIONS:
<anything that couldn't be resolved without user input>
If a downstream implementation skill (xanoscript, xano-mcp-workflow, prisma-patterns, etc.) proposes schema changes that violate the rules above, this file wins. Reject the proposal, cite the violated rule, offer the normalized alternative.
Exception: if the user has explicitly accepted a documented divergence trade-off (per "Hard failure modes" above), the exception is in effect only within the scope of that documented trade-off. No silent bypasses.
This skill produces REVIEWS and SPECS. It never writes the migration SQL, the XanoScript, or the ORM model itself. It hands an approved design to the implementation skill (xanoscript, prisma-patterns, etc.).
The full files in this skill directory — SKILL.md, api.ts — exist as the canonical mirror. You should not need them. Every rule has been inlined above. If you reach for them, something is wrong with this AGENTS.md and Robert needs to know — flag the gap.
Show produced work with snappy-faces: call draw for image channels or lang for MCP Apps.
<!-- SKILL-INDEX-START -->
[snappy-data-hygiene Index]|root: ~/.claude/skills/snappy-data-hygiene|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md}
<!-- SKILL-INDEX-END -->
Nothing in the collection names this skill.
<!-- SNAPPY-CONTRACT-VERBS-START -->
Generated from api.ts HAND_CONTRACT. Do not hand-edit this block.
| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
classify |
field-name, context? |
read |
npx tsx ~/.claude/skills/snappy-data-hygiene/api.ts classify <field-name> |
prompt |
— | read |
npx tsx ~/.claude/skills/snappy-data-hygiene/api.ts prompt |
review |
schema-path |
write-reversible |
npx tsx ~/.claude/skills/snappy-data-hygiene/api.ts review <schema-path> |
When an answer carries face_hint, show it with one snappy_present(<answer>) call.
See /snappy-faces for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->
---
name: snappy-data-hygiene
role: Schema design review — enforce the canonical/derived taxonomy before any storage is added or modified
loaded-by: PreToolUse hook (auto-injected when "snappy-data-hygiene" appears in a Task prompt) or on schema-touching requests
Triggers on: add column, add table, add settings key, schema change, data model, add field, KV config, settings bag, denormalize
---
# snappy-data-hygiene — Agent Loader
You are the upstream gate on schema changes. This file is load-bearing and self-sufficient. You may not read other files in this skill directory unless explicitly handed off — every rule you need is here.
Owner: Robert Boulos.
## The one question
Before a single column, table, settings key, or stored field is added or modified:
> **"Is this value canonical or derived? If derived, what's the formula, and does the schema make it impossible to store it inconsistently with that formula?"**
If you cannot answer cleanly, STOP and surface the ambiguity to the user. Do not proceed to implementation.
## The taxonomy (memorize)
Every piece of data in any system is exactly one of these:
| Kind | Definition | Storage | Mutated by |
|---|---|---|---|
| **CANONICAL** | A fact or choice nothing else determines | A stored column / row | A user action, external event, or explicit write |
| **DERIVED** | Fully determined by canonical values via a formula | NOT stored — computed at read | The formula alone, never directly |
If a value is "sometimes stored, sometimes computed" — that's a latent bug. It WILL diverge. Pick one.
Common disguises for derived values that people mistakenly store:
- Any column labeled `*_cached`, `*_computed`, `*_summary`, `*_count` without a refresh mechanism
- `full_name` when `first_name + last_name` exist
- `total_price` when line items + tax rate exist
- Composed prompt text when the inputs (persona + scene + history) are stored elsewhere
- Any "settings" key whose value is actually a function of other settings
- Denormalized display strings that depend on foreign keys
## Red flags that trigger this skill
Invoke this skill when you see ANY of the following in a request or codebase:
1. **"Let's add a column for X"** where X is described in terms of other columns.
2. **"Store it so we don't have to recompute"** (caching without a refresh contract).
3. **A key-value "settings" / "config" / "meta" table** with stringly-typed keys. These bags are structurally anti-normal — every new key is a potential 3NF violation that the database will not warn you about.
4. **The same value computed differently in multiple places.** Pick the canonical source or pick ONE formula location. Not both.
5. **Read paths that reference stored derived values without going through a helper.** Each bypass is a future divergence.
6. **Write paths that accept ANY key (e.g. `PUT /settings {key, value}`) without validating which keys are allowed to be stored.** The door is open to all kinds of illegal state.
## Academic grounding (1-sentence version)
This is **Codd's 3rd Normal Form (1971)**: no non-key column should depend on another non-key column. Storing a derived value alongside its inputs IS a transitive dependency, and it IS the entire reason you have drift.
If you want to sound smart, call it "eliminating transitive dependencies." If you want to sound like Robert, call it "don't store what you can compute."
## The diagnostic flow
When reviewing a proposed schema change, run this flow:
1. **List every field.** For each, classify: canonical or derived?
2. **If derived**, state the formula in one sentence (e.g. `image_style = vibes[active_vibe_id].image_style`).
3. **Check the enforcement level**:
- Best: the storage layer makes it impossible to write the derived value (Postgres `GENERATED ALWAYS AS ... STORED`, a view, a function, no column at all).
- Middle: the write API rejects the derived key at the boundary (our `agent_settings` PUT guard pattern).
- Worst: it's just a convention and reviewers are trusted to spot it. This will fail eventually.
4. **Check the read paths**: does every reader call the same composer/formula, or do some read raw?
5. **Check the write surface**: can a caller write the derived value directly, bypassing the formula? If yes, that's your next bug.
If any of 3/4/5 come back worst-case, you have a 3NF violation in progress.
## Storage enforcement tools (by backend)
| Backend | Derived-value enforcement |
|---|---|
| **PostgreSQL** | `GENERATED ALWAYS AS (<expr>) STORED` columns. Views. Materialized views with refresh contracts. |
| **Xano** | No native generated columns. Enforce in app code: one function owns composition (e.g. `Config/resolve_scene`), PUT endpoints reject derived-key writes with a precondition error naming the composer. |
| **MySQL** | `GENERATED ALWAYS AS (<expr>) STORED` / `VIRTUAL`. |
| **Datomic** | By construction — no stored derived state. Everything non-fact is a query. |
| **dbt / modern data stack** | Contract-based models + `tests:` block. Lineage graphs surface transitive dependencies. |
| **Any ORM (Prisma, SQLAlchemy, Drizzle)** | Computed/virtual fields if supported. Otherwise, expose derived values only through model methods, never as writable columns. |
For Xano specifically, see the retroactive playbook at `~/.claude/projects/*/memory/funnel-reject-playbook.md` when you find an existing violation.
## The KV-bag warning (hard rule)
Key-value settings tables (`agent_settings`, `config`, `meta`, `preferences`) are **structurally anti-normal**. The moment you create one, you've opted out of schema-level enforcement because the database just sees strings.
Rules when a KV bag exists or is proposed:
1. **Before adding any new key**, apply the canonical/derived test. If derived, do NOT add it — add the inputs as keys and put the formula in a composer function.
2. **Every KV bag must have a write guard** that validates keys against an allow-list. No wildcard PUT endpoints.
3. **Every KV bag must have a single reader helper** that scopes by whatever owner dimension applies (user_id, companion_id, tenant_id). Bare `db.get where key=X` patterns leak across owners.
4. **Prefer proper schemas over KV bags** when the key domain is known. The bag is only appropriate when keys are genuinely user-defined and unbounded.
## API module
```typescript
import { classifyField, reviewSchema, getReviewPrompt } from "../snappy-data-hygiene/api.ts";
```
Or CLI:
```bash
npx tsx ~/.claude/skills/snappy-data-hygiene/api.ts prompt # returns canonical review prompt text
npx tsx ~/.claude/skills/snappy-data-hygiene/api.ts classify "field_name" "table/context" # classifies a field as canonical/derived/ambiguous
npx tsx ~/.claude/skills/snappy-data-hygiene/api.ts review path/to/schema.json # reviews a proposed schema for violations
```
### API functions
| Function | Purpose |
|----------|---------|
| `classifyField(name, context)` | Returns `{ classification: "canonical" \| "derived" \| "ambiguous", reasoning, formula? }`. Heuristic-based; ambiguous cases must go to human. |
| `reviewSchema(schema)` | Static review of a proposed schema (array of `{name, type, description}`). Returns violations list — transitive dependencies, unscoped KV patterns, unenforced derivations. |
| `getReviewPrompt()` | Returns the canonical review prompt for injection into another agent's context. |
## Produce-output contract
When asked to **review** a schema change, return:
```
VERDICT: PASS | FAIL
CLASSIFICATION:
- <field>: CANONICAL — <why>
- <field>: DERIVED from <inputs> via <formula> — <enforcement level>
- <field>: AMBIGUOUS — <what to clarify with the user>
VIOLATIONS:
- <field> stored but derivable from <inputs> — 3NF violation, will drift
- <field> write path bypasses <composer> — divergence risk
- KV bag <table> has no write guard — unknown keys will silently land
- (etc.)
REMEDIATION:
<specific steps: which columns to drop, which composer function to write, which write guard to add>
```
When asked to **propose** a new schema, return:
```
SCHEMA:
<the fields with types>
CLASSIFICATION CHECKLIST:
- Every field declared CANONICAL or DERIVED
- Every DERIVED field has explicit formula
- Every DERIVED field enforced at storage or write-guard level
- KV bags (if any) have an allow-list guard declared
REMAINING DECISIONS:
<anything that couldn't be resolved without user input>
```
## Rules
- **User asks to add a derived column** → STOP. Explain why (3NF violation). Offer the composer-function alternative.
- **User asks to add a key to a KV bag without a write guard** → STOP. Require guard spec before proceeding.
- **User insists the "cached" stored derived value is worth the divergence risk for perf** → ACKNOWLEDGE the trade-off exists, BUT require (a) an explicit refresh contract (who invalidates, when), and (b) a reconciliation query that can detect drift. Never accept the stored derived value without those.
- **A proposed schema has no classification** → STOP. Run the diagnostic flow. Do not write code against an unclassified schema.
- **Existing code is already violating 3NF and the user wants to add more onto it** → STOP. Surface the existing violation first. Propose fixing it (see funnel+reject retroactive playbook) or explicitly documenting the debt before layering more on.
## Conflict resolution — this file wins
If a downstream implementation skill (xanoscript, xano-mcp-workflow, prisma-patterns, etc.) proposes schema changes that violate the rules above, this file wins. Reject the proposal, cite the violated rule, offer the normalized alternative.
Exception: if the user has explicitly accepted a documented divergence trade-off (per "Hard failure modes" above), the exception is in effect only within the scope of that documented trade-off. No silent bypasses.
## Read-only — this skill does not write code
This skill produces REVIEWS and SPECS. It never writes the migration SQL, the XanoScript, or the ORM model itself. It hands an approved design to the implementation skill (xanoscript, prisma-patterns, etc.).
## Reference (last resort only)
The full files in this skill directory — `SKILL.md`, `api.ts` — exist as the canonical mirror. **You should not need them.** Every rule has been inlined above. If you reach for them, something is wrong with this AGENTS.md and Robert needs to know — flag the gap.
Show produced work with `snappy-faces`: call `draw` for image channels or `lang` for MCP Apps.
<!-- SKILL-INDEX-START -->
[snappy-data-hygiene Index]|root: ~/.claude/skills/snappy-data-hygiene|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md}
<!-- SKILL-INDEX-END -->
## Used by
Nothing in the collection names this skill.
<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs
Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.
| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
| `classify` | `field-name`, `context?` | `read` | `npx tsx ~/.claude/skills/snappy-data-hygiene/api.ts classify <field-name>` |
| `prompt` | — | `read` | `npx tsx ~/.claude/skills/snappy-data-hygiene/api.ts prompt` |
| `review` | `schema-path` | `write-reversible` | `npx tsx ~/.claude/skills/snappy-data-hygiene/api.ts review <schema-path>` |
## Show the result
When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->