This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady
I'm a data engineer. Most of my mornings used to start the same way: open the folder where the nightly exports landed, open each CSV, scroll, squint, and try to work out whether anything was wrong before someone downstream noticed. Usually nothing was. That's what makes it so tiring: you do the same check every day so you can catch the one day something breaks.
MorningDrop does that check overnight so I don't have to. It's a small local agent you run from cron. When it wakes up it:
latest.txt I can read before coffee.
On a quiet night where everything passed, it doesn't even wake the model. Sloths conserve energy. 🦥
It's for anyone who gets data they don't control: vendor CSVs, storefront exports, CRM dumps, the "can you just drop it in the shared folder" files.
Recorded on a CPU-only box. The ~15–30 s Gemma wait is trimmed.
The sample drops in the repo have the kind of problems real exports have: a supplier that started sending usd instead of USD partway through the file, refunds arriving as quantity = -1, a customer with "usa" as a country code, and a brand-new shipments file nobody wrote a contract for. This is a real run with gemma3:4b on a CPU-only box:
$ morningdrop run
The orders_2026-10-24.csv file failed for 63 rows due to currency and quantity issues.
Prioritize fixing the currency and quantity validation rules in the orders contract.
report: reports/2026-10-25.md (21.7s, summary by model)
The full report adds suggested fixes and the evidence behind them:
## Suggested fixes
- `orders_2026-10-24.csv` · `currency` · fix_data: Correct currency values to USD, EUR, or GBP.
- `orders_2026-10-24.csv` · `quantity` · fix_data: Ensure quantity values are greater than or equal to 1.
- `orders_2026-10-24.csv` · `customer_id` · fix_data: Validate customer_id against pattern: cus_[0-9]{5}
| File | Contract | Rows | Passed | Failed | Status |
|----------------------------|-----------|-----:|-------:|-------:|--------|
| orders_2026-10-24.csv | orders | 240 | 177 | 63 | 🔴 |
| customers_2026-10-24.jsonl | customers | 120 | 116 | 4 | 🟡 |
| shipments_2026-10-24.csv | — | 80 | 0 | 0 | 🟡 |
### shipments_2026-10-24.csv
- No contract yet. Drafted one at contracts/_proposed/shipments.yaml; review it and move it into contracts/ to start enforcing.
The next night, after the exporter is fixed, the whole report is two lines and Gemma never gets called:
1 drop(s) checked overnight; every row passed.
Everything passed. Nothing to do.
Your data drops get checked overnight. You get two lines in the morning.
MorningDrop is a small local agent for anyone who wakes up to a folder of nightly exports: vendor CSVs, storefront dumps, CRM extracts. While you sleep it
reports/YYYY-MM-DD.md, .json and a two-line No rows leave your machine. No API keys. No per-call bill.
Recorded on a CPU-only box with asciinema + agg; the ~15–30 s Gemma wait is trimmed. Raw cast: docs/demo.cast.
$ morningdrop run
The orders_2026-10-24.csv file failed for 63 rows due to currency and quantity issues
Prioritize fixing the currency and quantity validation rules in
… MIT licensed, with tests (offline, using scripted fake models), CI on Python 3.9 and 3.12, and a CI job that installs Ollama and runs real Gemma end to end on a plain CPU runner.
The pipeline is boring on purpose. Everything that has a correct answer is done in plain code. The model only does the one thing it's good at: turning 60 × enum_violation on currency into a sentence a tired human will act on.
contractgate, which ships DataContractGate's validator; I use it offline, so no server and no network. CSV cells are coerced to the contract's types first. A value that doesn't parse stays a string, so the validator reports an honest type_mismatch instead of my code hiding it.contracts/_proposed/. It never enforces a contract you haven't reviewed. format so the model is steered toward the right shape from the start. Any OpenAI-compatible local server (llama.cpp, vLLM, LM Studio) works too.morningdrop run from cron, launchd or a systemd timer (examples in the repo), or morningdrop watch to poll.
Small local models are great until they aren't. While building this I watched them:
gemma3:270m`` gemma3:1b``gemma3:4b
Shape-checking alone wouldn't have caught most of that. So the model's answer goes through two layers:
summary_contract.yaml`` status must be green, yellow or red; each line must be 10–160 characters; every fix needs file, field, kind and suggestion. It's the same engine and the same contract format as the data side, pointed at LLM output.
If either layer fails, the violations go back to Gemma as a repair prompt, with the temperature nudged up so a stuck model can get unstuck. If only some of the fixes are wrong, those are quarantined and the rest are kept, the same way DataContractGate quarantines bad rows. If the model still can't get it right, or Ollama isn't even running, a plain template writes the report from the same facts.
The result: every morning report is correct, and the model makes it nicer to read. Data contracts at the edges of the pipeline, and an output contract at the edge of the model.
With the final prompt, gemma3:4b passes on the first try in about 20–35 seconds on a CPU-only box. The 1B and 270M models still get caught and fall back to the template, which is exactly what should happen.
For this tool, open weights aren't a nice-to-have. They're the reason it can exist.
A closed API would probably write slightly nicer sentences. But it would make me ship my data somewhere else every night, and that's what I was trying to stop worrying about.
MorningDrop saves me 15–30 minutes every morning. It also checks more than I do by hand in that time: every row of every file against its contract, instead of the scroll-and-squint spot check I actually had time for, which is how 60 lowercase currency codes at the bottom of a 240-row file slip through.
On a clean night, the two lines in latest.txt are the whole job. On a bad night, the report names the file, the field, the row count and a first fix before I've opened anything. When a new dataset shows up, its first contract is already drafted for me to review.