Typed decisions on the household budget: YNAB state judged by a System One model, never by prose.
  • TypeScript 91.7%
  • Shell 8.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Claude 994c800b80
All checks were successful
gate / gate (push) Successful in 20s
The gate becomes one list, and the purity claim becomes a test
A four-inspector audit of florian/maproom and florian/homelab, re-run on
claude-fable-5-1 with a Go toolchain actually on PATH, converged on four
defects in this repository's own scaffold. All four were inherited shapes:
patterns copied from maproom whose enforcing mechanism did not come with them.

The command list moves into hack/gate.sh, which feedback-loop.yaml and
.forgejo/workflows/gate.yml now both invoke. The previous shape was two literal
copies plus a "change both in the same commit" rule, and the audit measured
that rule in the repository it was borrowed from: maproom's two copies have
never once been edited in the same commit across 215 commits, and their gofmt
steps have already diverged. homelab solves it by extraction — one script, two
callers, "cannot drift, because it is the same file" — and that is the half
worth copying.

The Node version gains a single owner. gate.yml claimed in a comment that
package.json's engines.node owned it while hard-coding node-version: 22 three
lines below; maproom's version of that sentence was true only because
go-version-file: go.mod reads the owner and check-image.sh asserts the one other
copy. The owner here is .nvmrc, not engines.node, because engines is a
compatibility range and a range is not a version a runner can install. The
feared breakage across the --experimental-strip-types boundary was measured and
is not real: node 22.23.3 runs the typecheck and the full suite green.

margin.ts's "Nothing here calls the network" becomes src/no-network.test.ts.
In Go that claim is checkable — go list -deps ./internal/wayfinder returns no
net package, and the audit verified it — but fetch is a global in Node, so a
module reaches the network with no import and no dependency graph shows it. The
test scans imports and globals separately, because module specifiers are string
literals and identifiers are not: the first version stripped strings before
scanning and therefore could not see a single import. Its own self-test caught
that, which is the reason a scan that protects an unread claim must be tested
against source it must reject. Verified by negative control: a fetch injected
into margin.ts turns the suite red with the intended message.

CLAUDE.md gains "What silently misleads here", the homelab section that names
traps where a green result is false. It is the one pattern whose unit is a trap
rather than a file, so it does not shrink with the repository, and this
project's whole subject is failures that look like successes: a 200 that
dropped the ZDR flag, a 0.45/0.40 split that reads as a winner, a null that is
the correct answer, an offline suite that proves nothing about the wire.

Three citations were wrong and are corrected. Root ADR-0037 is the tracker's
self-built client and says nothing about typed decisions, so it cannot be the
answer to "why any of it is shaped this way"; typed-decision-models is not among
homelab's thirteen committed skills; and issue-tracker.md names a different
label subset than the nine created here from maproom's FixedLabels, so the
count leaves prose and the source of truth is named instead.

AGENTS.md is added as a map, not a rulebook: Codex, OpenCode and Cursor read
that filename and never CLAUDE.md, so without it every behavioural rule here is
invisible to those sessions.

Verified from a wiped node_modules: ./hack/gate.sh green, 10/10 tests.
2026-09-24 23:53:00 +00:00
.forgejo/workflows The gate becomes one list, and the purity claim becomes a test 2026-09-24 23:53:00 +00:00
hack The gate becomes one list, and the purity claim becomes a test 2026-09-24 23:53:00 +00:00
src The gate becomes one list, and the purity claim becomes a test 2026-09-24 23:53:00 +00:00
.env.example ynajev begins with the arithmetic that decides whether to trust an answer 2026-09-24 23:23:42 +00:00
.gitignore ynajev begins with the arithmetic that decides whether to trust an answer 2026-09-24 23:23:42 +00:00
.nvmrc The gate becomes one list, and the purity claim becomes a test 2026-09-24 23:53:00 +00:00
AGENTS.md The gate becomes one list, and the purity claim becomes a test 2026-09-24 23:53:00 +00:00
CLAUDE.md The gate becomes one list, and the purity claim becomes a test 2026-09-24 23:53:00 +00:00
feedback-loop.yaml The gate becomes one list, and the purity claim becomes a test 2026-09-24 23:53:00 +00:00
LICENSE ynajev begins with the arithmetic that decides whether to trust an answer 2026-09-24 23:23:42 +00:00
package-lock.json ynajev begins with the arithmetic that decides whether to trust an answer 2026-09-24 23:23:42 +00:00
package.json ynajev begins with the arithmetic that decides whether to trust an answer 2026-09-24 23:23:42 +00:00
PRODUCT.md ynajev begins with the arithmetic that decides whether to trust an answer 2026-09-24 23:23:42 +00:00
README.md The gate becomes one list, and the purity claim becomes a test 2026-09-24 23:53:00 +00:00
tsconfig.build.json ynajev begins with the arithmetic that decides whether to trust an answer 2026-09-24 23:23:42 +00:00
tsconfig.json ynajev begins with the arithmetic that decides whether to trust an answer 2026-09-24 23:23:42 +00:00

ynajev

Typed decisions on the household budget: YNAB state judged by a System One evaluation model, never by prose.

The name is the two halves. yna is YNAB — the household's budget, and the state every question here is asked about. jev is the evaluation model reached through the Vercel AI Gateway, which answers typed questions — choice, score, boolean — with probabilities and no prose at all. ynajev is the layer between them: it projects budget state into a question, asks it, and turns the answer into a decision a script can act on.

It is deliberately not a chat over your finances. Nothing here generates text. A budget question that wants a paragraph is a question for an agent, and this repository is the wrong place for it.

For Read
How an agent behaves here — the rules that change behaviour CLAUDE.md
Who this is for and what it must stay true to PRODUCT.md
What is being planned, in full the wayfinder map in this repository's issues
Why any of it is shaped this way CLAUDE.md, and root ADR-0015 for why the gate sits at the pull request

The two candidate jobs

Named here as the starting scope, and not yet decided — deciding them is what the map is for.

  • Categorisation. An uncategorised transaction arrives from an import. Its payee, memo, amount and account project into a choice over the budget's own category list. A confident answer proposes a category; a thin one leaves the row exactly as a human would have found it.
  • Triage. Which of today's budget facts deserve a human's attention at all — an overspent category, a scheduled charge a balance will not cover, an import that does not match. A score or boolean per fact, so the digest is ranked rather than exhaustive.

The three rules that outrank any feature

1. The margin is the gate, never the top probability. The Gateway sends probabilities and no confidence field. A 0.45 / 0.40 split is a coin flip wearing a confident-looking winner, so every decision thresholds on top1 − top2 (or |p − 0.5| · 2 for a boolean). src/margin.ts is that arithmetic and it is the first code in this repository for that reason.

2. Fail open to what the caller did before. Timeout, non-200, malformed body, thin margin → return nothing and let the caller behave exactly as it did without ynajev. A decision layer that can lose a transaction or suppress a row a human would have seen has failed in a way no accuracy number redeems.

3. Zero data retention, asserted rather than assumed. Every request carries providerOptions.gateway.zeroDataRetention: true. Once per new endpoint, send an invalid value and require HTTP 400 — a 200 means the flag was silently dropped and household transaction text left the box without the retention term.

Eval before build

No question wording ships on taste. The order is: export real budget rows, label them, run the question set, write the go/no-go threshold down, then build. The house skill typed-decision-models holds the procedure and the numbers it demands; this repository does not restate it.

Context

privat — the household budget. Nothing from beisel.works or from INTERSPORT enters this repository, its exports, or its eval sets.

Running it

npm ci
npm run typecheck
npm test

Those three are feedback-loop.yaml — a session's pre-flight — and .forgejo/workflows/gate.yml, the gate at the pull request. They are the same commands in the same order on purpose.