From 824c2f5f5901bd7480c80aded2fbfbdcfd6ba1ec Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Wed, 2 Sep 2026 21:57:38 +0200 Subject: [PATCH] docs: make AGENTS.md the only agent guide, restructured into five sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AGENTS.md now holds the instructions and CLAUDE.md and GEMINI.md are pointers to it. Three files of instructions become three files that disagree; one file every agent reads cannot. The content is reorganised rather than rewritten, into: the invariants, the guards that fail the build, the validation this codebase deliberately does not perform, the environment, and where to read next. Every guard now names the test that catches it, and the two rules with no test behind them are called out as such, because "only review catches this" is information the reader needs. Restructuring surfaced one rule that was missing here entirely — sw.js must stay unhashed and be served no-cache — which had only ever been written down in ARCHITECTURE.md. Dropped: the `bun init` boilerplate code samples. Every rule they carried is still stated; the hello-world test and the three frontend snippets are not. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 221 +++++++++++++++-- CLAUDE.md | 234 +----------------- GEMINI.md | 7 + docs/TODO.md | 2 +- docs/plans/2026-09-01-budget-app.md | 10 +- docs/specs/2026-09-01-budget-app-design.md | 2 +- .../2026-09-02-design-foundation-design.md | 2 +- docs/specs/2026-09-02-month-view-design.md | 2 +- src/cssPairs.test.ts | 2 +- src/domain/types.ts | 2 +- src/index.css | 2 +- src/store/actions.ts | 2 +- src/ui/components/RuleHistory.tsx | 2 +- 13 files changed, 227 insertions(+), 263 deletions(-) create mode 100644 GEMINI.md diff --git a/AGENTS.md b/AGENTS.md index e002bc6..c239fa3 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,25 +1,206 @@ -# Agent guide +# Budget 2.0 — agent guide -This repository's agent instructions live in **`CLAUDE.md`** — read it first. -It carries the project invariants, the gotchas that cost real bugs, and the -validation this codebase deliberately does not perform. +The instructions for changing this codebase, for any agent or any human. This +is the only copy: `CLAUDE.md` and `GEMINI.md` are pointers to this file, so it +cannot drift into three disagreeing versions. -Then, depending on what you are doing: +Most of the rules below are here because breaking one cost real time. This file +is the short, actionable form; the *reasoning* lives in `docs/ARCHITECTURE.md` +and `docs/specs/`, and is not repeated here. -- **Changing anything structural** → `docs/ARCHITECTURE.md` (layers, data flow, and - why the non-obvious choices are what they are). -- **Picking up queued work** → `docs/TODO.md` (ordered, each item carrying the - reasoning already settled). -- **Making a product decision** → `docs/PRODUCT.md` (who this is for, what must never - break, and what must not be invented). -- **Wondering why something is the way it is** → `docs/specs/` - records the decisions *and the alternatives that were rejected*. +## 1. Invariants -Two rules worth knowing before your first edit: +- **`src/domain/` is pure.** No React, no IndexedDB, no `Date.now()`, no + `new Date()` without an explicit argument. It must be testable with no DOM + (`src/domain/smoke.test.ts` imports it without one). +- **Money is a float, rounded at every boundary.** Use `roundMoney` from + `src/domain/money.ts` after any division, after any FX conversion, and before + persisting. +- **`roundMoney(amount, digits)` takes a DIGIT COUNT, not a currency code.** + Decimal places live in `Dataset.currencies`, because the owner defines + currencies; resolve them with `digitsFor(dataset.currencies, code)`. The + argument is required on purpose — a default of 2 is right for every currency + the app shipped with, so a missed call site would pass every test and be + silently wrong for the first zero-decimal currency added. +- **When splitting an amount, never round the last part independently.** Compute + it as `total - sum(others)` so the parts sum exactly to the whole. +- **`Currency` is an open string code; `Dataset.currencies` is the authority.** + There is no global list of valid currencies. A code is IDENTITY — it keys the + FX table and every stored `Money` — so codes are normalised uppercase, are + never editable, and a currency still referenced cannot be removed + (`currencyUsage` names what refers to it). `parseDatasetJson` validates the + table and every `Money` against it; that boundary check is what replaced the + compile-time safety of the old closed union. +- **The baked FX rates seed a new dataset only.** They must NOT act as a + fallback when a rate is missing: clearing a rate removes the row so + `MissingRateError` surfaces, instead of silently converting money at a number + the owner never chose. +- **`MonthId` is the string `"YYYY-MM"`.** Stored data never contains `Date` + objects. +- **The envelope rollover fold has exactly one implementation**, in + `src/domain/fold.ts`. Year and summary views aggregate over it — they never + reimplement the math. +- **A post's allocation is a dated series**, `Post.rules: RuleVersion[]`, + resolved by `ruleAt(post, monthId)`. `Post.standingRule` no longer exists. A + post with no applicable version allocates zero; a per-month override still + wins outright. +- **Posts are archived, never deleted**, because purchases reference them. + Archived posts still fold. +- **An amount field parses its own currency:** "30$" is thirty dollars. Use + `parseMoneyInput` (`src/ui/moneyInput.ts`) and keep the input `type="text"` — + a number input discards the "$" before the parser ever sees it. +- **`sw.js` must stay unhashed and be served `no-cache`.** A cached or hashed + service worker can never be replaced, which pins users to an old build + forever. +- **A destructive whole-dataset write downloads a backup first.** Both JSON + import and "Reset everything" export the current dataset *before* replacing + it, and abort if that export fails. This is the owner's only copy. -1. **Bun only.** `bun test`, `bun run`, `bunx`. Never npm, jest, vitest or vite. -2. **There IS a browser here** — `bun scripts/screenshot.ts ` - drives headless Chrome and hands you a PNG to look at. Read CLAUDE.md's - "Verification limits" first: the container's fonts lie, and offline, the - install prompt, the `.ods` in a spreadsheet, and how anything *feels* still - need a human. Say so rather than claiming it works. +## 2. Guards that fail the build + +Each of these shipped as a bug once. The test named beside it is why it cannot +ship twice — re-introducing one is a regression, not a style choice. + +- **Never read `event.target.value` inside a `mutate()` callback.** Capture it + into a `const` in the handler. `mutate` defers behind the write queue, and + React resets the input's DOM value first, so you commit the old value. + → `src/ui/eventCapture.test.ts`, source-level because reproducing it needs + real browser render timing that happy-dom does not have. +- **Never define a bare `--accent`.** shadcn owns it as a hover BACKGROUND + paired with `--accent-foreground`; setting one without the other drops hover + contrast below WCAG AA app-wide. Redefine shadcn tokens only in pairs, and put + brand colour on `--primary`, never `--accent`. The app's own tokens are + `--budget-*`. → `src/cssPairs.test.ts` +- **A `var()` naming a token `src/index.css` does not define paints NOTHING, + silently.** An undefined custom property invalidates the whole declaration: + no warning, no throw, no fallback. Renaming `--rule` this way killed the carry + meter and no test noticed. → `src/ui/cssTokens.test.ts`, which matches source + *text* — so it flags a token named in a comment just as readily. +- **Every awaited `store.*` call in `src/ui/` must be guarded** by a `try/catch` + or a `.catch`. IndexedDB fails for reasons no code can prevent (full disk, + evicted storage, private-mode quota), and unguarded it is an unhandled + rejection: the button appears to do nothing and the user is told nothing. + `useMutate` is the guard for most call sites. + → `src/ui/storeWriteErrors.test.ts` +- **`roundMoney` call sites must resolve real digits**, per §1. + → `src/domain/currencyDigits.test.ts` is the only test that can tell a real + implementation from one that assumes 2: it drives a JPY-style zero-decimal + currency through rounding, both FX directions, both split modes, plan + division, the fold and the month view. +- **`process.env.BUN_PUBLIC_*` must be read via `try/catch`** — see + `readBasePathEnv`. Bun inlines the literal only when the variable is set; + unset, the bare reference reaches the browser and throws before the app boots. + A `typeof process` guard does *not* work: it survives inlining and discards + the inlined value, silently breaking subpath deploys. + → `src/ui/basePath.test.ts` covers that accessor. Nothing stops a *new* bare + reference elsewhere, so this one still needs your attention. +- **The pre-paint theme script in `index.html` and `src/ui/theme.ts` must + agree.** The script cannot import the module — it has to run before any module + loads — so it repeats the storage key and the fallback logic. + → `src/ui/theme.test.ts` +- **Anything derived from the base path is asserted for both a root and a + subpath deploy:** the manifest (`src/manifest.test.ts`), the font URLs + (`src/fontCss.test.ts`), the icons (`src/icons.test.ts`), and the service + worker's precache list (`src/swPrecache.test.ts`) — which must contain no + duplicate, because `cache.addAll` rejects on one, which is why the service + worker had never once installed in any environment. + +Two rules with no test behind them. Only review catches these: + +- **A React error boundary does not catch event-handler throws.** Domain + functions that throw must be guarded at the call site when called from one. +- **Do not "fix" a domain throw by making it return a fallback.** Silently wrong + numbers are worse than a visible error in a budgeting app. + +## 3. Deliberately absent validation + +Adding a "missing" guard here is a regression: + +- going over budget, anywhere; +- allocation percentages summing past 100%; +- splits that do not sum to the total (the remainder-absorbing split reconciles + them by design); +- negative amounts — a refund is a normal line. + +## 4. Environment + +**Bun only.** `bun ` (never `node` or `ts-node`), `bun test` (never jest +or vitest), `bun install` (never npm/yarn/pnpm), `bun run - - -``` - -With the following `frontend.tsx`: - -```tsx#frontend.tsx -import React from "react"; -import { createRoot } from "react-dom/client"; - -// import .css files directly and it works -import './index.css'; - -const root = createRoot(document.body); - -export default function Frontend() { - return

Hello, world!

; -} - -root.render(); -``` - -Then, run index.ts - -```sh -bun --hot ./index.ts -``` - -For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`. - -## Project invariants (Budget 2.0) - -- `src/domain/` is pure: no React, no IndexedDB, no `Date.now()`, no `new Date()` - without an explicit argument. It must be testable with no DOM. -- Money is a float. Always round with `roundMoney` from `src/domain/money.ts` - after any division, after any FX conversion, and before persisting. -- **`roundMoney(amount, digits)` takes a DIGIT COUNT, not a currency code.** - Decimal places live in `Dataset.currencies`, because the owner defines - currencies; resolve them with `digitsFor(dataset.currencies, code)`. The - argument is required on purpose — a default of 2 is right for every currency - the app shipped with, so a missed call site would pass every test and be - silently wrong for the first zero-decimal currency added. -- When splitting an amount, never round the last part independently. Compute it - as `total - sum(others)` so the parts sum exactly to the whole. -- `MonthId` is the string `"YYYY-MM"`. Stored data never contains `Date` objects. -- **`Currency` is an open string code; `Dataset.currencies` is the authority.** - There is no global list of valid currencies any more. A code is IDENTITY — it - keys the FX table and every stored `Money` — so codes are normalised - uppercase, are never editable, and a currency still referenced cannot be - removed (`currencyUsage` names what refers to it). `parseDatasetJson` - validates the table and every `Money` against it; that boundary check is - what replaced the compile-time safety of the old closed union. -- The envelope rollover fold has exactly one implementation, in - `src/domain/fold.ts`. Year and summary views aggregate over it — they never - reimplement the math. -- Over-budget is always allowed. Never add validation that blocks it. -- An amount field parses its own currency: "30$" is thirty dollars. Use - `parseMoneyInput` (`src/ui/moneyInput.ts`) and keep the input `type="text"` — - a number input discards the "$" before the parser ever sees it. -- Base path comes from `BUN_PUBLIC_BASE_PATH`; never hardcode a subpath, and - read it through a `try/catch` accessor — see the gotcha below. -- A post's allocation is a dated series, `Post.rules: RuleVersion[]`, resolved by - `ruleAt(post, monthId)`. `Post.standingRule` no longer exists. A post with no - applicable version allocates zero; a per-month override still wins outright. -- Posts are archived, never deleted, because purchases reference them. Archived - posts still fold. - -## Start here - -- `docs/ARCHITECTURE.md` — layers, data flow, and why the non-obvious choices are - what they are. Read it before changing anything structural. -- `docs/TODO.md` — queued work, each item carrying the reasoning already settled. -- `docs/PRODUCT.md` — who this is for and what must never break. -- `docs/specs/` — design decisions, including alternatives rejected. - -## Gotchas that cost real bugs - -Each of these shipped once. Re-introducing one is a regression, not a style -choice. - -- **Never read `event.target.value` inside a `mutate()` callback.** `mutate` - defers behind the write queue; React resets the input's DOM value first, so - you commit the old value. Capture into a `const` in the handler. - `src/ui/eventCapture.test.ts` fails the build otherwise. -- **`process.env.BUN_PUBLIC_*` must be read via `try/catch`** (see - `readBasePathEnv`). Bun inlines the literal only when the variable is set; - unset, the bare reference reaches the browser and throws before the app boots. - A `typeof process` guard does not work — it survives inlining and discards the - inlined value, silently breaking subpath deploys. -- **Never define a bare `--accent` CSS variable.** shadcn owns it as a hover - BACKGROUND paired with `--accent-foreground`; setting one without the other - drops hover contrast below WCAG AA app-wide. Redefine shadcn tokens only in - pairs — `src/cssPairs.test.ts` enforces it — and put brand colour on - `--primary`, never `--accent`. Project tokens are `--budget-*`. -- **A `var(--token)` naming a token `src/index.css` does not define paints - NOTHING, silently.** An undefined custom property invalidates the whole - declaration: no warning, no throw, no fallback. Renaming `--rule` this way - killed the carry meter and no test noticed. `src/ui/cssTokens.test.ts` guards - it. -- **A React error boundary does not catch event-handler throws.** Domain - functions that throw must be guarded at the call site when called from one. -- **Do not "fix" a domain throw by making it return a fallback.** Silently wrong - numbers are worse than a visible error in a budgeting app. - -## Deliberately absent validation - -Adding a "missing" guard here is a regression: - -- going over budget, anywhere; -- allocation percentages summing past 100%; -- splits that do not sum to the total (the remainder-absorbing split reconciles - them by design); -- negative amounts — a refund is a normal line. - -## Verification limits - -**There IS a browser here.** `google-chrome` is on PATH, and -`scripts/screenshot.ts` drives it over CDP — it waits past the async IndexedDB -read (which Chrome's own `--screenshot` does not, which is why the docs used to -claim no browser existed), and it reports console errors and uncaught -exceptions. Read the PNG it writes. Use it before claiming anything about -layout. - -The whole recipe, from a running `bun run dev`: - -```sh -bun scripts/demo-data.ts /tmp/seed.js -bun scripts/screenshot.ts http://localhost:3000/month/2026-09 /tmp/shot.png \ - --eval-file=/tmp/seed.js --reload -``` - -Then read `/tmp/shot.png`. Other flags: `--dark`, `--full`, `--w/--h`, -`--click=`, `--profile=` (keeps a service worker and IndexedDB -across runs — required for anything stateful), `--eval-after-file` (observe a -flow that ends in a navigation from the far side of it). - -Two things that would otherwise waste your time: - -- **`--reload` is not optional.** Chrome starts from a fresh profile each run, - so the app re-seeds itself empty; seeding IndexedDB only takes effect on the - next boot. Without it every screenshot shows an app full of zeros. -- **This container's fontconfig resolves every generic family to Fira Code, a - monospace font.** The UI then renders monospaced in a way no real user sees, - and the last agent to look nearly "fixed" a font bug that did not exist. - `scripts/screenshot-fonts.conf` corrects it and the script applies it - automatically — do not undo that, and do not trust a screenshot taken with - `FONTCONFIG_FILE` set to something else. - -Still **not** verifiable here, so do not claim it: real offline behaviour, the -install and update prompts, opening the generated `.ods` in a spreadsheet, and -anything about how the design *feels*. A screenshot proves a layout exists and -is not broken; it does not prove it is good. Substitute a real check where one -exists — structural validation of the `.ods`, curl against the dev server, -pure-function tests — and state plainly what still needs a human. +This is a pointer, not a copy. Three files of instructions would become three +files that disagree. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..40edcc5 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,7 @@ +# GEMINI.md + +The instructions for this repository live in **[`AGENTS.md`](AGENTS.md)**. Read +that file. + +This is a pointer, not a copy. Three files of instructions would become three +files that disagree. diff --git a/docs/TODO.md b/docs/TODO.md index 1121292..50d3e94 100755 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -59,7 +59,7 @@ The reasoning below is kept because it is the reasoning, not a plan. converting money at a number the user never chose. - Domain purity forces the shape: `createSeedDataset` takes the rates as an **argument**; the env read lives in a new `src/store/bakedRates.ts`, in the - store layer, using the `try/catch` accessor pattern (see CLAUDE.md gotchas). + store layer, using the `try/catch` accessor pattern (see AGENTS.md gotchas). ## 3. Visual redesign — sub-projects 1 and 2 of 4 DONE diff --git a/docs/plans/2026-09-01-budget-app.md b/docs/plans/2026-09-01-budget-app.md index aff9e55..fbf4882 100755 --- a/docs/plans/2026-09-01-budget-app.md +++ b/docs/plans/2026-09-01-budget-app.md @@ -72,7 +72,7 @@ Tests are co-located: `src/domain/money.test.ts` beside `src/domain/money.ts`. **Files:** - Create: `package.json`, `tsconfig.json`, `bunfig.toml`, `build.ts`, `src/index.ts`, `src/index.html`, `src/frontend.tsx`, `src/index.css` (all via `bun init`) -- Modify: `CLAUDE.md` (append project invariants), `.gitignore` +- Modify: `AGENTS.md` (append project invariants), `.gitignore` - Delete: `src/APITester.tsx`, `src/logo.svg`, `src/react.svg` - Test: `src/domain/smoke.test.ts` @@ -158,9 +158,9 @@ Expected: PASS, 1 test. Run: `bun --hot src/index.ts` Expected: server starts, and the printed URL shows "Budget 2.0". Stop it with Ctrl-C. -- [ ] **Step 7: Append project invariants to CLAUDE.md** +- [ ] **Step 7: Append project invariants to AGENTS.md** -The scaffolded `CLAUDE.md` documents Bun idioms. Append the rules specific to this project: +The scaffolded `AGENTS.md` documents Bun idioms. Append the rules specific to this project: ```markdown ## Project invariants (Budget 2.0) @@ -196,7 +196,7 @@ git add -A git commit -m "chore: scaffold Bun + React + shadcn project bun init --react=shadcn, with the demo API routes removed since this app -has no backend. Adds project invariants to CLAUDE.md and a smoke test +has no backend. Adds project invariants to AGENTS.md and a smoke test proving the domain layer imports with no DOM." ``` @@ -253,7 +253,7 @@ export const CURRENCY_DIGITS: Record = { EUR: 2, }; -/** A float amount paired with its currency. See CLAUDE.md for rounding rules. */ +/** A float amount paired with its currency. See AGENTS.md for rounding rules. */ export interface Money { amount: number; currency: Currency; diff --git a/docs/specs/2026-09-01-budget-app-design.md b/docs/specs/2026-09-01-budget-app-design.md index b039fad..c96a73c 100755 --- a/docs/specs/2026-09-01-budget-app-design.md +++ b/docs/specs/2026-09-01-budget-app-design.md @@ -310,7 +310,7 @@ migrations are mandatory rather than optional. ## Agent support files -- `CLAUDE.md` — the `bun init` scaffold provides Bun idioms ("use `bun test`", +- `AGENTS.md` — the `bun init` scaffold provides Bun idioms ("use `bun test`", "don't use vite", HTML imports). This project extends it with its own invariants: the domain layer stays free of React and IndexedDB, money rounds at every boundary, the last distributed part is computed by diff --git a/docs/specs/2026-09-02-design-foundation-design.md b/docs/specs/2026-09-02-design-foundation-design.md index 34febd7..2722f05 100755 --- a/docs/specs/2026-09-02-design-foundation-design.md +++ b/docs/specs/2026-09-02-design-foundation-design.md @@ -48,7 +48,7 @@ colours app components may name. ### The brand teal is `--primary`, never `--accent` -CLAUDE.md's hardest-won rule is "never define a bare `--accent`", paid for with +AGENTS.md's hardest-won rule is "never define a bare `--accent`", paid for with an app-wide contrast bug. The mechanism matters: shadcn uses `--accent` as a hover **background** with `--accent-foreground` as the text on it. Setting one without the other repaints the background and leaves the text behind. diff --git a/docs/specs/2026-09-02-month-view-design.md b/docs/specs/2026-09-02-month-view-design.md index 6373b66..93cbe28 100755 --- a/docs/specs/2026-09-02-month-view-design.md +++ b/docs/specs/2026-09-02-month-view-design.md @@ -52,7 +52,7 @@ below the unchanged month `
`. ### The allocation meter scales to `max(income, allocated)` -Not to income. Over-budget is always allowed (CLAUDE.md), and allocating past +Not to income. Over-budget is always allowed (AGENTS.md), and allocating past income is the ordinary way that shows up here, so the meter has to render it rather than cap at a full bar: the overflow past the income mark paints `--overspend`, and the label reads past 100%. Capping would make diff --git a/src/cssPairs.test.ts b/src/cssPairs.test.ts index a075562..3dac705 100755 --- a/src/cssPairs.test.ts +++ b/src/cssPairs.test.ts @@ -1,7 +1,7 @@ import { test, expect, describe } from "bun:test"; /** - * CLAUDE.md's hardest-won rule, made mechanical. + * AGENTS.md's hardest-won rule, made mechanical. * * shadcn uses several tokens as a BACKGROUND with a paired FOREGROUND for the * text on it. Redefining one without the other repaints the background and diff --git a/src/domain/types.ts b/src/domain/types.ts index 8d0649e..e4b203e 100755 --- a/src/domain/types.ts +++ b/src/domain/types.ts @@ -33,7 +33,7 @@ export const SEED_CURRENCIES: readonly CurrencyDef[] = [ /** Used when a currency has no definition. Two places is right almost always. */ export const DEFAULT_CURRENCY_DIGITS = 2; -/** A float amount paired with its currency. See CLAUDE.md for rounding rules. */ +/** A float amount paired with its currency. See AGENTS.md for rounding rules. */ export interface Money { amount: number; currency: Currency; diff --git a/src/index.css b/src/index.css index 1cb5173..bb534b0 100755 --- a/src/index.css +++ b/src/index.css @@ -25,7 +25,7 @@ * than fighting it means every primitive (dialog, select, badge, button) * inherits both themes for free. * - * WHY THE PAIRS MATTER. CLAUDE.md's rule is "never define a bare `--accent`", + * WHY THE PAIRS MATTER. AGENTS.md's rule is "never define a bare `--accent`", * and it was paid for with an app-wide contrast bug: shadcn uses `--accent` as * a hover BACKGROUND and `--accent-foreground` as the text on it, so setting * one without the other repaints the background and leaves the text behind. diff --git a/src/store/actions.ts b/src/store/actions.ts index 6850ffc..fff5338 100755 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -25,7 +25,7 @@ import type { * store.mutate(), which clones before applying and writes through afterwards. * * Every MONEY amount that flows through here is rounded to its currency's - * minor unit (via roundMoney) before it lands in the draft, per CLAUDE.md's + * minor unit (via roundMoney) before it lands in the draft, per AGENTS.md's * "round before persisting" rule. FxRate.baseUnitsPerOne is deliberately * exempted in setFxRate below — an exchange rate is not money, and rounding * it to cents would be wrong in kind (see the comment there). diff --git a/src/ui/components/RuleHistory.tsx b/src/ui/components/RuleHistory.tsx index 2ed3116..e1eddf4 100755 --- a/src/ui/components/RuleHistory.tsx +++ b/src/ui/components/RuleHistory.tsx @@ -14,7 +14,7 @@ interface Props { * Shape AND range, matching the import validator. `setRuleFrom` sorts the * series with `compareMonths`, which THROWS on a malformed MonthId; useMutate * would catch it and show an error, but a disabled button is the better - * answer, and CLAUDE.md requires guarding a throwing domain call at the site + * answer, and AGENTS.md requires guarding a throwing domain call at the site * that invokes it from a handler. */ const MONTH_ID = /^\d{4}-(0[1-9]|1[0-2])$/;