Every data person has two spreadsheets that describe the same things with different column names. One says Threat_Level, the other says danger_level. One has duplicate rows, the other spells "swamp wraith" three different ways. Fixing that by hand is grave-robbing work: dig out the good parts, sew them together, hope it holds.
For Kiroween 2026 I built the lab for it. FrankenData takes mismatched CSV and XLSX files, proposes how to stitch them together, cleans them, joins them into one typed DuckDB table, and then lets you ask it questions in plain English. All of it runs in your browser. Your rows never leave the tab.
Try it: kiroween-frankendata.vercel.app · Code: GitHub
This post is about how I built it with Kiro, and what I would do the same way again.
Specs first, code second
I did not start by asking Kiro to "build a CSV app". I started with three specs, each with requirements.md, design.md and tasks.md:
| Spec | What it held |
|---|---|
frankendata-file-intake | CSV and XLSX parsing, type inference through messy values (currency, percents, mixed dates, null tokens), loading typed tables into DuckDB-WASM, demo data |
frankendata-ai-stitching | The deterministic rules engine for column matches and cleanups, the approval screen, non-destructive apply and undo, and the optional AI layer with hard limits on what it may do |
frankendata-chat-sql | Ask the Lab: plain English to SQL, a read-only SQL validator, result conversion, charts, CSV export and a rate-limited serverless proxy |
Writing the requirements forced decisions I would otherwise have made by accident. For example: the AI may only re-score matches the rules engine already found, never invent one, and an AI-discovered match can never reach the auto-approve threshold. That rule lives in the spec, so every later task had to respect it.
Then I executed tasks in small batches, a few at a time, and stopped after each batch to run the build, the tests, lint and a serverless check. Big "do everything" prompts went wrong more often and cost more to fix. Small batches with a hard checkpoint almost never did.
Steering docs that load when they matter
Steering docs were how I stopped repeating myself. FrankenData has six, and the part I liked most was the inclusion modes:
- Always on:
product.md,tech.md,structure.mdandsecurity.md. The security doc holds the rules that must never drift: data stays in the browser, only column names and a few sample values ever leave, the proxies fail closed, and no secret ever reaches the client bundle. - Only when editing API files:
api-conventions.md. Node(req, res)handlers, explicit.jsimport extensions, shared helpers, and the rule thatnpm run check:apimust pass. - Only when editing test files:
testing.md. Vitest settings, property tests for invariants, golden DuckDB tests.
Loading API and testing rules only when they apply keeps the agent's context focused on the task in front of it, instead of every rule I ever wrote.
Hooks as guardrails
FrankenData turns plain English into SQL and runs it. The thing standing between a model and a destructive query is the SQL validator: one read-only SELECT or WITH, only over loaded tables, no file or network functions, no extra statements, a row cap.
That file is the one I least wanted an agent to "helpfully" loosen while fixing something else. So I added a hook, sql-validator-guard, that fires whenever sql-validator.ts is edited and asks the agent to re-run the validator's unit and property tests and confirm no forbidden-keyword or file and network check was weakened.
The other three hooks are simpler: run-test-suite, check-api for the serverless function graph, and pre-commit-quality-gate, which runs build, lint and tests before a commit. Small, boring, and they caught real problems.
Properties in the spec became property tests
The design docs list correctness properties, not just features. Kiro turned them into fast-check property tests that throw hundreds of random inputs at each rule. A few of them:
- Undo restores state: after apply then undo, the exact pre-apply tables and contents are back.
- Apply never mutates originals: every original table is byte-for-byte unchanged after a stitch.
- LEFT JOIN row floor: the stitched result never has fewer rows than its anchor table.
- Injection safety: adversarial table names and values never execute injected SQL.
- Bounded AI influence: whatever the model says, a score moves only within fixed bounds.
- Validator rejects forbidden statements: random SQL containing forbidden keywords is always refused.
By the end the suite had 101 test files and 688 tests. That made it safe to keep changing the UI late in the project, because anything that touched the core broke a property loudly.
What broke in production
Passing tests did not mean a working deploy. Three bugs only showed up on the live site:
-
ERR_MODULE_NOT_FOUNDon the AI proxy. Node's ESM resolution needs explicit.jsextensions in imports, and Vite never complained. Kiro fixed the imports and added acheck:apiscript that type-checks, emits, imports and actually invokes the handlers. That rule now lives inapi-conventions.md. -
req.text is not a function. The handlers were written for webRequestobjects, but the runtime passes Node-style(req, res). Kiro rewrote them with a capped body reader, andcheck:apinow calls each handler to prove it. - Dates showing as
1764460800000. DuckDB returned dates as epoch milliseconds. The fix reads the column types and formats dates properly.
Each fix was a short, focused Kiro prompt with the exact error and log line pasted in. Specific errors got specific fixes.
What I would do again
- Write the safety rules into the spec, not into prompts. The AI's limits held for the whole project because they were requirements, not reminders.
- Run tasks in small batches with a hard checkpoint. Build, tests, lint and the serverless check after every batch.
- Scope steering docs with inclusion modes, so each rule shows up only where it applies.
- Put a hook on your most dangerous file. Mine was the SQL validator.
- Budget your credits. I ran out of monthly credits near the end, so I parked the last UI batch on a branch and merged it once it built cleanly.
And the fun part: the app has a little mascot, the Specimen, a creature stitched together from spreadsheet cells. It sleeps on its slab until you load data, comes alive with a lightning strike, gets bandaged when you stitch, and holds up a chart when you get an answer. Poke it.
Try FrankenData: kiroween-frankendata.vercel.app · Code: github.com/Kadaru242003/kiroween-frankendata
Built for the Kiroween 2026 hackathon.


![[AWS] How far can CDK/Terraform stacks run on AWS Emulator? [AWSEmulator]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fqiita-user-contents.imgix.net%2Fhttps%253A%252F%252Fqiita-user-contents.imgix.net%252Fhttps%25253A%25252F%25252Fcdn.qiita.com%25252Fassets%25252Fpublic%25252Farticle-ogp-background-afbab5eb44e0b055cce1258705637a91.png%253Fixlib%253Drb-4.1.1%2526w%253D1200%2526blend64%253DaHR0cHM6Ly9xaWl0YS11c2VyLXByb2ZpbGUtaW1hZ2VzLmltZ2l4Lm5ldC9odHRwcyUzQSUyRiUyRnMzLWFwLW5vcnRoZWFzdC0xLmFtYXpvbmF3cy5jb20lMkZxaWl0YS1pbWFnZS1zdG9yZSUyRjAlMkYxOTI5NDklMkYxYmVjZGE4YTRmMWRiOWU5OTQyZDljOGMwYjZkZDA3NjkwMTM2NjgwJTJGbGFyZ2UucG5nJTNGMTc0MTQ4MzQxMD9peGxpYj1yYi00LjEuMSZhcj0xJTNBMSZmaXQ9Y3JvcCZtYXNrPWVsbGlwc2UmYmc9RkZGRkZGJmZtPXBuZzMyJnM9MDZlM2QxY2EzNTVhN2Y4N2M2ZTQxY2RlYzdmN2M0ODI%2526blend-x%253D120%2526blend-y%253D462%2526blend-w%253D90%2526blend-h%253D90%2526blend-mode%253Dnormal%2526mark64%253DaHR0cHM6Ly9xaWl0YS1vcmdhbml6YXRpb24taW1hZ2VzLmltZ2l4Lm5ldC9odHRwcyUzQSUyRiUyRnMzLWFwLW5vcnRoZWFzdC0xLmFtYXpvbmF3cy5jb20lMkZxaWl0YS1vcmdhbml6YXRpb24taW1hZ2UlMkYxZTIwZGI1ZTdlNDA3ZWFlN2I5NzBlYzk5OTg4MGRjOGMzY2MwZjY0JTJGb3JpZ2luYWwuanBnJTNGMTY2NzI3NDk2Mz9peGxpYj1yYi00LjEuMSZ3PTQ0Jmg9NDQmZml0PWNyb3AmbWFzaz1jb3JuZXJzJmNvcm5lci1yYWRpdXM9OCZiZz1GRkZGRkYmYm9yZGVyPTIlMkNGRkZGRkYmZm09cG5nMzImcz04OTI0NDcwMDk1MDMyZjUxYmZlNzE3ZjVkOGUzYmE3ZA%2526mark-x%253D186%2526mark-y%253D515%2526mark-w%253D40%2526mark-h%253D40%2526s%253D0db13b4aef231a19bc684897cf15021a%3Fixlib%3Drb-4.1.1%26w%3D1200%26fm%3Djpg%26mark64%3DaHR0cHM6Ly9xaWl0YS11c2VyLWNvbnRlbnRzLmltZ2l4Lm5ldC9-dGV4dD9peGxpYj1yYi00LjEuMSZ3PTk2MCZoPTMyNCZ0eHQ9JUUzJTgwJTkwQVdTJUUzJTgwJTkxQVdTJTIwRW11bGF0b3IlRTMlODElQTdDREslMkZUZXJyYWZvcm0lRTMlODElQUUlRTMlODIlQjklRTMlODIlQkYlRTMlODMlODMlRTMlODIlQUYlRTMlODElQUYlRTMlODElQTklRTMlODElOTMlRTMlODElQkUlRTMlODElQTclRTUlOEIlOTUlRTMlODElOEYlRTMlODElOEIlRTMlODAlOTBBV1NFbXVsYXRvciVFMyU4MCU5MSZ0eHQtYWxpZ249bGVmdCUyQ3RvcCZ0eHQtY29sb3I9JTIzMUUyMTIxJnR4dC1mb250PUhpcmFnaW5vJTIwU2FucyUyMFc2JnR4dC1zaXplPTU2JnR4dC1wYWQ9MCZzPTgyMGFkODg1ZTFmOTNiYWMwMGY2OGY3ZjEzMTZhM2Fj%26mark-x%3D120%26mark-y%3D112%26blend64%3DaHR0cHM6Ly9xaWl0YS11c2VyLWNvbnRlbnRzLmltZ2l4Lm5ldC9-dGV4dD9peGxpYj1yYi00LjEuMSZ3PTgzOCZoPTU4JnR4dD0lNDBOYW5hXzc3NyZ0eHQtY29sb3I9JTIzMUUyMTIxJnR4dC1mb250PUhpcmFnaW5vJTIwU2FucyUyMFc2JnR4dC1zaXplPTM2JnR4dC1wYWQ9MCZzPTg1YzQ4MzUyMmVkMGVhNmYxYjhkZjU5MjVjYzhiNjQz%26blend-x%3D242%26blend-y%3D454%26blend-w%3D838%26blend-h%3D46%26blend-fit%3Dcrop%26blend-crop%3Dleft%252Cbottom%26blend-mode%3Dnormal%26txt64%3DS0RESeOCouOCuOODo-OCpOODq-mWi-eZuuOCu-ODs-OCv-ODvOagquW8j-S8muekvg%26txt-x%3D242%26txt-y%3D539%26txt-width%3D838%26txt-clip%3Dend%252Cellipsis%26txt-color%3D%25231E2121%26txt-font%3DHiragino%2520Sans%2520W6%26txt-size%3D28%26s%3Dba21d9d97271ac7f70cc902b1a0ad7bb)