Data schemas
This is the reference for every data file AI Operations Dashboard reads. If your dashboard panel doesn't render or npm run check:data is failing, this is the page that tells you what shape the file expects.
Every schema below is enforced by `scripts/validate-data.mjs`. That validator runs as part of npm run build via the check:data step โ so a malformed data file blocks the build. To run it on its own:
npm run check:dataThe script prints a โ or โ per file, lists exact errors, and exits 1 if anything failed.
data/pending.json
What's open. Editable in any text editor. The Pending panel renders one card per item, sorted by priority then dateAdded.
interface PendingFile {
items: PendingItem[]
}
interface PendingItem {
/** Required. The card headline. */
title: string
/** Optional. Stable slug for cross-referencing. */
id?: string
/** Optional. One short paragraph: why this matters. */
why?: string
/** Optional. high | medium | low. Drives sort order + tint. */
priority?: "high" | "medium" | "low"
/** Optional. Who's executing. */
doer?: "you" | "ai" | "team" | "external" | "claude" | "human" | "mixed"
/** Optional. Repo path the item lives under. */
ref?: string
/** Optional. YYYY-MM-DD. */
dateAdded?: string
/** Optional. Ordered checklist rendered as a numbered list. */
steps?: string[]
/** Optional. Free-form filter tags. */
tags?: string[]
/** Optional. Link back to a sprint sub-task id (e.g. "S3.1"). */
sprintId?: string
}Example:
{
"items": [
{
"id": "wire-stripe",
"title": "Wire up Stripe for paid tier",
"why": "Free signups have no path to convert. Need checkout + webhook before launch.",
"priority": "high",
"doer": "you",
"dateAdded": "2026-05-15",
"steps": [
"Create Stripe account (test mode)",
"Add STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET to .env",
"Build /api/checkout/session route",
"Build /api/checkout/webhook route"
],
"tags": ["billing", "launch-blocker"]
}
]
}Common mistakes
- Forgetting
title. The validator rejects items without a non-empty title. - Using
priority: "urgent"orpriority: "p1". Onlyhigh/medium/loware accepted. - Putting
stepsas a single string instead of an array of strings.
data/usage.json
AI spend rollup. Powers the watchdog banner at the top of the dashboard and the Usage panel. Regenerate via npm run data:aggregate-usage (rolls data/usage-log.jsonl into this file).
interface UsageFile {
/** Required. Spend (USD) since 00:00 local today. */
spendToday: number
/** Required. Spend (USD) for the current calendar month. */
spendThisMonth: number
/** Required. Spend (USD) for the current quarter. */
spendThisQuarter: number
/** Optional. Human labels for the banner ("May 2026", "2026-Q2"). */
monthLabel?: string
quarterLabel?: string
/** Optional. ISO-8601 timestamp of the last aggregation. */
lastUpdated?: string
/** Optional. Banner color thresholds in USD. */
thresholds?: {
dailyYellow?: number
dailyRed?: number
[k: string]: number | undefined
}
/** Optional. Per-day rollup for the spend sparkline. */
daily?: Array<{
date: string // YYYY-MM-DD
spend: number
tokens?: number
}>
/** Optional. Per-model rollup. Two accepted shapes. */
byModel?:
| Array<{ model: string; spend: number; tokens?: number }>
| Record<string, number | { spend: number; tokens?: number }>
}Example:
{
"spendToday": 0.42,
"spendThisMonth": 19.374,
"spendThisQuarter": 30.809,
"monthLabel": "May 2026",
"quarterLabel": "2026-Q2",
"lastUpdated": "2026-05-23T16:11:35.839Z",
"thresholds": { "dailyYellow": 1, "dailyRed": 10 },
"daily": [
{ "date": "2026-05-22", "spend": 1.08, "tokens": 92400 },
{ "date": "2026-05-23", "spend": 0.42, "tokens": 38100 }
],
"byModel": [
{ "model": "claude-opus-4-7", "spend": 17.463, "tokens": 893200 },
{ "model": "claude-sonnet-4-6", "spend": 12.121, "tokens": 2214700 }
]
}Common mistakes
- Writing dollar amounts as strings (
"19.37") instead of numbers (19.37). The validator only accepts finite numbers. - Putting
thresholdsat the top level instead of nested. It must bethresholds: { dailyRed: 10 }, notdailyRed: 10. - Mixing the two
byModelshapes within a single file. Pick one and stick with it.
data/scheduled-jobs.json
Every recurring job that touches the project โ cron, git hooks, scheduled agents, manual reviews. The Activity panel renders this file unchanged.
interface ScheduledJobsFile {
jobs: ScheduledJob[]
}
interface ScheduledJob {
/** Required. Stable slug. Must be unique across jobs. */
id: string
/** Required. Human-readable display name. */
name: string
/** Required. When it fires ("daily 04:00 PT", "every git push", "weekly Sunday"). */
cadence: string
/** Required. One sentence โ what the job does + why it exists. */
purpose: string
/** Optional. cron | git-hook | manual | gha | etc. */
kind?: string
/** Optional. ISO-8601 timestamp of the most recent run. */
lastRun?: string
/** Optional. ok | error | skipped. */
lastStatus?: string
}Common mistakes
- Duplicate
idacross two entries โ the validator flags this; the dashboard would render only one card. - Forgetting
purpose. If you can't write one sentence about why this job exists, you don't need the job.
data/recent-commits.json
Frozen git log snapshot. The Activity panel reads this file because serverless platforms don't ship .git. Regenerate via npm run data:freeze-commits.
interface RecentCommitsFile {
/** Optional. ISO-8601 timestamp of the freeze. */
generatedAt?: string
/** Optional. The `--since` window used (e.g. "180 days ago"). */
since?: string
commits: Commit[]
}
interface Commit {
hash: string // full sha
shortHash: string // 7-char sha
date: string // YYYY-MM-DD
timestamp: string // ISO-8601
author: string
subject: string // first line of the commit message
}Common mistakes
- Committing a stale
recent-commits.jsonfrom a shallow clone. If you have a pre-push hook that runsdata:freeze-commits, the local file is always richer than the CI clone โ the freeze script should refuse to overwrite a richer dataset. - Missing
shortHashbecause you generated the file by hand. Use the script.
data/connected-repos.json
Registry of repositories linked into AI Operations Dashboard. Each entry describes one external repo that emits signals (pending items, usage logs, commit freezes, etc.) into the AI Operations Dashboard. The Connected Repos panel renders one card per entry.
interface ConnectedReposFile {
repos: ConnectedRepo[]
}
interface ConnectedRepo {
/** Required. Stable slug. Must be unique across the file. */
id: string
/** Required. Display name shown on the card. */
name: string
/** Required. Absolute filesystem path on the host machine. Must start with "/". */
path: string
/** Required. ISO-8601 date string โ when the repo was linked. */
linkedAt: string
/** Required. List of signal kinds the repo emits (e.g. "pending", "usage", "commits"). */
emitSignals: string[]
/** Optional. ISO-8601 timestamp of the most recent emission. May be null. */
lastReportedAt?: string | null
/** Optional. Non-negative integer โ how many writes this repo has produced. */
writeCount?: number
/** Optional. Path to a frozen commits snapshot for this repo. May be null. */
commitsSnapshotPath?: string | null
}Example:
{
"repos": [
{
"id": "woodwiki",
"name": "Woodwiki",
"path": "/Users/ahamade/Documents/GitHub/woodwiki",
"linkedAt": "2026-06-02T14:30:00Z",
"emitSignals": ["pending", "usage", "commits"],
"lastReportedAt": "2026-06-02T18:11:42Z",
"writeCount": 47,
"commitsSnapshotPath": "/Users/ahamade/Documents/GitHub/woodwiki/data/recent-commits.json"
}
]
}Common mistakes
- Using a relative path (
./woodwikior../woodwiki). Paths must be absolute and start with/โ the validator rejects anything else. AI Operations Dashboard runs from a different working directory than its connected repos, so relative paths can't be resolved consistently. - Forgetting `emitSignals`. Even if the list is empty for now, the field must be present as an array. The dashboard uses it to decide which panels to surface for the repo.
- Writing `linkedAt` as a unix timestamp (
1717340400) instead of an ISO date string ("2026-06-02T14:30:00Z"). The validator only accepts ISO-8601 /YYYY-MM-DDform. - Duplicate `id` across two entries. The validator flags this; the dashboard would render only one card.
- `writeCount` as a string (
"47") or negative number. Must be a non-negative integer.
docs/operator-log.md
Chronological work log, newest entry on top. Parsed live by the Operator Log panel.
Required heading shape (per entry):
## YYYY-MM-DD HH:MM โ short title
**Why:** one sentence.
**What shipped:** what landed.
**Tags:** comma-separated tags
**Next:** next concrete step.The validator checks that at least one entry exists (a ## YYYY-MM-DD โฆ heading). A file with zero entries emits a warning, not an error โ but the panel will render empty.
Common mistakes
- Using
###(level-3) headings. The parser only counts##. - Hyphen instead of em-dash. Either works for the file to parse, but
โis the convention. - Entry order. Newest goes on top, always.
knowledge/postmortems/*.md
One file per incident. The Postmortems panel walks this directory, parses frontmatter, and renders severity-tinted cards.
Required shape (frontmatter + body):
---
title: AI spend leaked through env var; $40 burned overnight
date: 2026-05-12
severity: p1
---
# AI spend leaked through env var; $40 burned overnight
## What happened
โฆ
## Root cause
โฆ
## Fix
- Commit hash + summary
## Prevention
- Concrete steps (each should become a Pending item)| Field | Required? | Notes | |---|---|---| | title | yes | The card headline. | | date | yes | YYYY-MM-DD. | | severity | recommended | p1 / p2 / p3. Validator warns if missing. |
Common mistakes
- Forgetting the closing
---on the frontmatter. The parser silently fails to read frontmatter at all โ validator marks the file as missingtitleanddate. - Putting the title in the body but not the frontmatter. The panel reads frontmatter.
plans/execution-status.md
Live execution state for the Plan panel. Hero panel reads **Last update:**, **Current state:**, **Next sub-task on resume:**. The sub-task ledger table drives the per-sprint boards.
Required shape (excerpt):
# Execution status
**Last update:** 2026-05-18 11:00 PDT
**Current state:** v0.1 scaffold complete ยท docs + landing in flight
**Next sub-task on resume:** S1.7 โ record 90-second demo video
## Sub-task ledger
| ID | Sub-task | Date | Commit | Status |
|---|---|---|---|---|
| S1.1 | Folder scaffold | 2026-05-16 | a1b2c3cf | โ shipped |The validator checks:
**Last update:**exists (error if missing โ the dashboard hero relies on it).- A markdown table with header
ID | Sub-task | โฆ | Statusexists (warning if missing).
Common mistakes
- Renaming the field to
**Updated:**or**Last updated:**. The dashboard parses the exact stringLast update. - Dropping the
IDcolumn from the ledger table. The 5-column form is required.
data/savings.json
Generated by `scripts/build-savings.mjs` (the Token Savings collector). It reads the global claude-mem SQLite DB (via the sqlite3 CLI) and per-repo Graphify (graphify benchmark + graphify-out/cost.json), computes token/dollar savings, and writes a committed, sanitized snapshot. Derived โ never hand-edit. Refresh with npm run data:collect-savings, the data:build chain, or the auto-installed SessionStart hook.
type SavingsData = {
generatedAt: string // UTC ISO with Z
generatedAtLocal: string
config: {
dollarsPerMillionTokens: number
recallTokensPerObservation: number
graphifyAssumedQueriesPerDay: number
scopeMode: string // "global" | "projects"
scopeProjects: string[]
scopeExclude: string[] // deny-list / privacy control
}
combined: { savedTokens: number; savedDollars: number } // claude-mem only (the hard number)
cloudMem: {
observations: number; workTokens: number; distinctDays: number
recallTokensPerObservation: number; recallTokens: number
savedTokens: number; savedDollars: number; efficiencyPct: number // capped 99.9, never 100
byProject: { project: string; observations: number; workTokens: number; inRepo: boolean }[]
daily: { day: string; observations: number; workTokens: number; cumulativeWorkTokens: number }[]
dbFound: boolean
} | null // null when the claude-mem DB is absent
graphify: {
corpusWords?: number|null; corpusTokensNaive?: number|null; nodes?: number|null; edges?: number|null
avgQueryTokens?: number|null; reductionFactor?: number|null
savedTokensPerQuery: number|null; savedDollarsPerQuery: number|null
build: { totalInputTokens: number; totalOutputTokens: number; runs: number; lastBuilt: string|null } | null
buildTokens: number|null; buildCostDollars: number|null
notDetected?: boolean; reason?: string
} | null
graphifyHistory: { day: string; reductionFactor: number|null; corpusTokensNaive: number|null;
avgQueryTokens: number|null; nodes: number|null; edges: number|null; buildTokens: number|null }[]
errors: string[] // collector is fail-safe: problems land here, exit code stays 0
}Privacy: the snapshot is machine-global by default โ it lists every project name in the claude-mem DB (this is a private repo, so that's accepted; see the operator decision). Trim specific projects with scope.exclude in .ai-ops-dashboard/savings.config.json. The absolute DB path is never written โ only cloudMem.dbFound (a boolean).
Common mistakes
- Hand-editing the numbers โ it's regenerated; edit
.ai-ops-dashboard/savings.config.jsoninstead. - Letting
efficiencyPctreach 100 โ it is capped at 99.9 by design (the validator rejects โฅ100). - Adding a
config.dbPathโ forbidden (privacy leak); the validator rejects it. - Treating a missing file as fatal โ the collector needs the local claude-mem DB, so on CI/Vercel the
file simply may be stale; validateSavings reports a warning, not an error.
data/savings-history.json
A bare JSON array (one row per day) of Graphify reduction snapshots that accrues over time โ the only savings file that keeps history (claude-mem daily is rebuilt from the DB each run). Upserted by day, sorted ascending. Same row shape as SavingsData.graphifyHistory[]. Derived โ never hand-edit.
Adding a new schema
When you ship a new dashboard panel that reads a data file:
- Add the file shape to this doc (interface block + example + common mistakes).
- Add a
validateX()function to `scripts/validate-data.mjs`. - Register it in the
validatorsarray insidemain(). - Run
npm run check:datato confirm both the happy path and an intentionally-broken file are caught.
The convention is one validator function per file or per directory, returning a result object with errors[] + warnings[]. Don't reach for a third-party JSON-schema library โ the validator is meant to be readable and edited by anyone with five minutes of Node experience.