Prove a change to Predict Marketing is sound before claiming it works. Use after any change touching CSV reading, the model protocol, storage, or the report.
Install
npx skillscat add eliquid/predict-marketing-forecasting/verify Install via the SkillsCat registry.
Verifying a change
Read AGENTS.md §8 first. This skill is the longer sweep for changes that touch
the path between a CSV and a stored number.
1. The cheap gates (always)
gofmt -l . # must print nothing
go vet ./... # must print nothing
go test -count=1 ./... # all pass2. Every model, end to end
./predictmarketing models # all three, with their real capabilities
./predictmarketing forecast testdata/example.csv -model timesfm3 -horizon 3
./predictmarketing forecast testdata/example.csv -model chronos2 -horizon 3
./predictmarketing forecast testdata/example.csv -model chronos2ft -horizon 3Each must produce numbers, and each writes its own report file.
chronos2ft reporting "no fine-tuned model yet" is expected on a machine where
nobody has trained one — it is not a failure. Train it, or skip that line.
3. Ground truth — the check that matters most
If you touched anything between the CSV and the database, the stored numbers must
still be exactly what the library returns.
The script reads the runs out of the database, so the two forecasts have to be
stored in it first — on an empty or missing file it dies withsqlite3.OperationalError: no such table: runs.
rm -f /tmp/gt.db
./predictmarketing forecast YOUR.csv -model chronos2 -db /tmp/gt.db
./predictmarketing forecast YOUR.csv -model timesfm3 -db /tmp/gt.db
models/.venv/bin/python examples/ground-truth.py /tmp/gt.db YOUR.csvIt checks every entity that was forecast — the account and each switched-on
campaign — for both models.
Every line must say EXACT. The only permitted difference between stored output
and library output is sorting, and only where the model's own quantiles crossed.
Anything else means the pipeline is altering forecasts.
3a. If you touched campaign handling
./predictmarketing forecast YOUR-EXPORT.csv -model chronos2 -db /tmp/c.db
sqlite3 /tmp/c.db "SELECT COUNT(*) FROM raw" # every input row kept
sqlite3 /tmp/c.db "SELECT DISTINCT entity FROM series" # account + EVERY campaign
sqlite3 /tmp/c.db "SELECT DISTINCT entity FROM forecasts" # account + switched-on onlyThe two entity lists are meant to differ: everything is stored, only the
campaigns the export says are switched on as of its last day are forecast
(AGENTS.md §2a1). series short of a campaign that is in raw is a bug;forecasts short of a paused one is not.
The campaign forecasts should sum to roughly the account forecast — they are
produced independently, so expect a few percent, not an exact match. A large gap
means something is wrong with the aggregation. Note the account total is the sum
of all campaigns including the paused ones, so a file with paused spend in it
will not add up from the forecast entities alone.
3b. If you touched column classification
go test -run TestColumnClassification -v .It pins every real Google Ads column name to its bucket. Two traps: "Max CPC bid"
ends in "id" (match the last word, not a suffix), and CPA is an outcome, not a
setting.
3c. If you touched forecast storage or the accuracy view
go test -run TestAccuracy -v .
./predictmarketing accuracy -db YOUR.db -by-dayactual must be NULL for days that have not happened, and must fill in by itself
once a newer export is imported. Nothing is recomputed — the original forecast
stays exactly as it was made.
4. Deeper sweeps (for risky changes)
go test -race -count=2 ./... # state leaking between tests
go test -run '^$' -fuzz FuzzReadCSV -fuzztime 60s # coverage-guided fuzzingConcurrency:
rm -f /tmp/c.db
for s in A B C D; do (./predictmarketing forecast testdata/example.csv \
-model chronos2 -horizon 2 -series $s -db /tmp/c.db >/dev/null 2>&1) & done; wait
sqlite3 /tmp/c.db "SELECT COUNT(*) FROM runs; PRAGMA integrity_check;" # 4, okRuns from another directory (model paths must resolve against the binary):
cd /tmp && "$OLDPWD/predictmarketing" models5. If you changed the report
./predictmarketing forecast testdata/example.csv -model chronos2 -out /tmp/r.html
grep -c 'src="' /tmp/r.html # must be 0 -- the page must be self-contained
grep -c '</script' assets/htmx.min.js # must be 0 -- inlining would break otherwise6. What "done" means
State what you ran and what it printed. Do not say a change works because it
compiles. If a test fails, work out whether the code or the assertion is wrong
before editing either — several failures here have been stale assertions, and one
was a test helper generating 2026-01-32.
The import workflow
import touches more moving parts than any other command, so check it end to
end rather than only its units:
mkdir -p /tmp/v/data && cp examples/05-campaigns.csv /tmp/v/data/
./predictmarketing import -no-finetune -data /tmp/v/data -db /tmp/v/pm.db-data and -db are not optional here. cd-ing into /tmp/v and runningimport with no flags reads the installation's data/, not the one you just
filled — that is the whole point of defaultPath, checked at the end of this
file. A scratch import has to name its folder.
Four things must be true afterwards:
/tmp/v/data/reports/05-campaigns_models.htmlexists and holds both models- the CSV has moved to
/tmp/v/data/imported/, not been copied or deleted - a file under 90 days is refused and left in place
- the report opens from
file://with both dropdowns working
The last one needs a browser, not a grep: the dropdowns and the crosshair are
JavaScript, and a page that renders in the terminal can still be blank in a
browser. Check in the browser console that the chart scrolls
(scrollWidth > clientWidth), that it opens scrolled to the forecast, and that
a mousemove over the svg fills the readout — on a forecast day with every
model, and on a history day with the actual.
A synthetic mousemove is not enough on its own. The first crosshair put the
readout inside the scroller, where it scrolled out of sight with the content
and showed the user nothing, while exactly that kind of test found it in the DOM
and passed. Look at the page.
Iterating on the drawing: report, not import
Once one import has stored its runs, do not import again to see a chart change.
./predictmarketing report -data /tmp/v/data -db /tmp/v/pm.dbIt redraws data/reports/ from the database — nothing read, nothing forecast,
nothing retrained, nothing written (AGENTS.md §2c). Two things to check after
touching it:
- it refuses cleanly on a database with no stored runs, naming
import - a rate loses its
%sign, and only that; if a number differs from the pageimportwrote, the rebuild inrebuildDatais wrong, not the renderer
Paths must not follow the shell
models/, data/ and pm.db all resolve relative to the installation, never
to the current directory (defaultPath in import.go, installDir inworker.go, HERE in each models/*.py). Check it after touching any of them:
cd /tmp && /path/to/install/predictmarketing importIt must read the install's data/ and write the install's pm.db, and must not
create anything in /tmp. The failure this guards against is silent: a second
empty data/ and a second database, while the models still load correctly, so
it looks like it worked. Forecasts split across databases cannot be scored, andaccuracy then has less history than the user believes.