diff --git a/Architecture.md b/Architecture.md index d4ead0f..767766b 100644 --- a/Architecture.md +++ b/Architecture.md @@ -2,7 +2,7 @@ Small FastAPI app rendering Jinja2 templates with HTMX-driven partial updates. SQLite is the only storage. SQLAlchemy 2.x is the ORM; Alembic -manages schema. +manages schema. `uv` manages Python deps. ## Modules @@ -11,33 +11,46 @@ src/quartermaster/ main.py FastAPI app factory routes.py Budget configuration HTTP handlers routes_month.py Monthly view HTTP handlers - service.py Budget queries, totals, target logic, zero-amount - month_service.py Snapshot, deviation, per-month CRUD, month zero - models.py SQLAlchemy models and Section enum + service.py Budget queries, totals, target, zero-amount, group views + month_service.py Month snapshot, deviation, per-month CRUD, lifecycle, group views + groups.py Group enum, labels, section->group mapping, default open + models.py SQLAlchemy models, Section enum, MonthState enum db.py Engine, session, PRAGMA foreign_keys=ON config.py DB URL resolution templates/ Jinja2 templates (base, index, month, partials) - static/app.css All styling + static/ + app.css Full stylesheet (Barlow Condensed + ledger layout) + brand/ Logo assets (full, mark-wide, shield-wide) ``` -## Sections +## Sections and groups ``` -Section enum (budget + month): - income, fixed_bill, debt_minimum, food, subscription, other +Section enum (budget + month) + income, fixed_bill, debt_minimum, food, subscription, other, sinking_fund + +Group enum (display-layer) + income -> [income] + committed -> [fixed_bill, debt_minimum] + Primary Debt Target card + savings -> [sinking_fund] + flexible -> [food, subscription, other] + +Default open: income=yes, committed=no, savings=no, flexible=yes ``` -Primary Debt Target is NOT a section. It is a singleton pointer that -selects one of the Debt Minimums rows as the current focus. Excluded -from the zero-amount math since it has no total of its own. +The Primary Debt Target is NOT a section. It is a singleton pointer that +selects one of the Debt Minimums rows as the current focus. It is excluded +from zero-amount math since it has no total of its own (its amount is +already counted inside Debt Minimums). ## Budget data model ``` entry - id, section, name, amount, created_at, updated_at + id, section, name, amount, notes (nullable, up to 1024 chars), + created_at, updated_at -debt_target (singleton, id = 1) +debt_target (singleton, id = 1) id, debt_minimum_id FK entry(id) ON DELETE SET NULL, updated_at ``` @@ -48,24 +61,27 @@ the entry itself. ``` month - id, year_month UNIQUE, created_at + id, year_month UNIQUE ("YYYY-MM"), + state ('planning' | 'active' | 'closed', default 'planning'), + activated_at NULL, closed_at NULL, + created_at month_entry id, month_id FK month(id) ON DELETE CASCADE, - section, name, planned, applied, + section, name, planned, applied, notes NULL, origin_name NULL, origin_planned NULL, source_entry_id FK entry(id) ON DELETE SET NULL, created_at, updated_at -month_debt_target (singleton per month) +month_debt_target (singleton per month) month_id PK FK month(id) ON DELETE CASCADE, month_entry_id FK month_entry(id) ON DELETE SET NULL, updated_at ``` Creating a month copies every budget entry into `month_entry` and -preserves `origin_name` / `origin_planned`. `source_entry_id` links -back to the budget but is informational only; the snapshot is +preserves `origin_name`, `origin_planned`, and `notes`. `source_entry_id` +links back to the budget but is informational only; the snapshot is self-contained so the month keeps working even after the referenced budget row is deleted. @@ -73,69 +89,139 @@ budget row is deleted. Computed from `(name, planned)` vs `(origin_name, origin_planned)`: -* `unchanged` — both match. No tag. -* `edited` — name or planned differs. Orange tint + "modified" tag. -* `new_in_month` — no origin (row added after snapshot). Blue tint + - "new this month" tag. +* `unchanged` — both match. No tag, progress bar renders in the section + baseline colour. +* `edited` — name or planned differs. "Modified" tag in ochre; progress + bar recoloured to indicate drift. +* `new_in_month` — no origin (row added after snapshot). "New this month" + tag in indigo; progress bar tinted indigo. + +Notes changes do NOT flip the deviation state. Notes are annotation, not +plan drift. ### Per-month debt target Copied from the budget's current debt target at creation time, resolved through `source_entry_id`. Fully editable per month. +## Month lifecycle + +Three explicit states. Transitions happen via dedicated routes and are +validated on the server. + +``` +Planning -- POST /month/{ym}/activate --> Active +Active -- POST /month/{ym}/close --> Closed (applied zero must be $0.00) +Closed -- POST /month/{ym}/reopen --> Active +``` + +Each transition stamps or clears the relevant timestamp. The close gate +is authoritative on the server; the UI disables the Close button with a +tooltip when the balance is nonzero but would not be trusted alone. + +`month_service.ensure_editable(month)` is the guard that every mutation +route calls. When the month is closed it raises `MonthLifecycleError`, +which the route layer translates to an HTTP 400. Inputs on a closed +month carry `disabled`; delete buttons, add forms, and the target form +are omitted entirely from rendered HTML. + +Reopen does NOT reverse any sweep — nothing is ever swept automatically. +Reopening just sets the state back to Active and lets you edit again. + ## Zero-amount calculation -`sum(income.amount) - sum(non_income.amount)` with -`debt_target` excluded. On the budget page there is one value; on the -month page there are two (Planned and Applied), each computed over the -relevant column. +On both pages: + +``` +zero = sum(income.amount | .applied | .planned) + - sum(non_income.amount | .applied | .planned) +``` + +Debt Target is excluded from the calculation because it is a pointer, +not a totalable section. + +On the budget page there is one value. On the month page there are two: +Planned (over the planned column) and Applied (over the applied column). Colour tone: -* zero → green (`tone-zero`) -* positive → amber (`tone-positive`): unassigned income -* negative → red (`tone-negative`): over-budget +* `tone-zero` → sage (balanced) +* `tone-positive` → ochre (unassigned, under-budget) +* `tone-negative` → burgundy (over-budget) ## Routes ### Budget (`src/quartermaster/routes.py`) ``` -GET / index page -POST /sections/{section}/entries add entry +GET / index page +POST /sections/{section}/entries add entry DELETE /entries/{entry_id} remove entry -POST /debt-target set / clear debt target +POST /entries/{entry_id}/notes update entry notes +POST /debt-target set / clear debt target ``` ### Month (`src/quartermaster/routes_month.py`) ``` -GET /month/{year_month} view or create-flow page -POST /month/{year_month}/create snapshot the budget -POST /month/{year_month}/sections/{section}/entries add month entry -POST /month/{year_month}/entries/{entry_id} update (name / planned / applied) -DELETE /month/{year_month}/entries/{entry_id} remove month entry -POST /month/{year_month}/target set / clear per-month target +GET /month/{year_month} view page or create-flow +POST /month/{year_month}/create snapshot the budget (lands in Planning) +POST /month/{year_month}/activate Planning -> Active +POST /month/{year_month}/close Active -> Closed (requires applied zero = 0) +POST /month/{year_month}/reopen Closed -> Active +POST /month/{year_month}/sections/{section}/entries add month entry +POST /month/{year_month}/entries/{entry_id} update (name / planned / applied / notes) +DELETE /month/{year_month}/entries/{entry_id} remove month entry +POST /month/{year_month}/target set / clear per-month target ``` All mutation routes return the section partial plus OOB swaps for: -* The debt target card when a Debt Minimums row was added, edited, or - deleted -* The Zero Amount widget on every entry change +* The debt target card when a Debt Minimums row was added / edited / deleted +* The Zero Amount widget +* All four group totals (`#group-total-{group}`) -OOB swap is cheaper than re-rendering the full page and keeps totals -in sync without a reload. +OOB is used so section-level changes keep the page-level summary widgets +accurate without a reload. ## HTMX conventions -* Each month entry row carries three inline inputs (`name`, `planned`, - `applied`) with `hx-post` and `hx-trigger="change"`. Each input - sends only its own field; the server accepts any subset. +* Month entry rows carry three inline inputs (`name`, `planned`, + `applied`) plus a notes input, each with `hx-post` and + `hx-trigger="change"`. Each input sends only its own field; the server + accepts any subset. * Section partials swap via `hx-target="#section-{section}"` + `hx-swap="outerHTML"`. * OOB swaps use `hx-swap-oob="outerHTML"` on the top-level element of the partial so the same id can be re-rendered anywhere in the page. +* Transition buttons (activate, close, reopen) POST with `hx-swap="none"` + and the server returns 204 + `HX-Redirect: /month/{year_month}` so the + page re-renders cleanly in the new state. + +## Visual identity + +* **Typography**: Barlow Condensed (300-800 + italic) imported from + Google Fonts. Barlow proportional as a secondary pair. All numeric + columns carry `font-variant-numeric: tabular-nums` plus + `font-feature-settings: "lnum" 1, "tnum" 1`. +* **Palette** (CSS variables in `app.css`): + * `--paper #f5efe0` warm cream background with layered radial gradients + for depth + * `--ink #1a1814` warm near-black + * `--accent #732629` burgundy, sampled from the logo shield + * `--ochre #9c6b1a` under-budget / deviation + * `--sage #2d4a30` balanced / at-plan + * `--indigo #353a5e` new-in-month +* **Layout primitives**: + * `.zero-widget` grid at the top of each page; logo on the left, zero + in the centre, flanking labels + * `details.group` collapsible groups with a hairline CSS chevron that + rotates on `[open]` + * `.section` with a small-caps header and an inline subtotal + * `table.entries` with a 4-column grid (name / planned / applied / + actions) and a 2px progress bar on each row's bottom border + * `.target-section` with a burgundy left bar and `↳` margin glyph + * `.state-badge` tracked-caps label with bullet separators ## Testing @@ -144,3 +230,4 @@ in sync without a reload. share the same in-memory engine across threads via `StaticPool`. * Backup script tests shell out and assert on filenames and sqlite round-trips. +* Full suite runs in under 4 seconds. diff --git a/DevelopmentGuide.md b/DevelopmentGuide.md index c15cc45..587e306 100644 --- a/DevelopmentGuide.md +++ b/DevelopmentGuide.md @@ -4,8 +4,7 @@ * Python 3.12+ * [uv](https://github.com/astral-sh/uv) for dependency management -* SQLite (the library, not the CLI; the backup script uses Python's - sqlite3 module so no `sqlite3` binary is required) +* SQLite via Python's `sqlite3` module (no external binary required) ## Setup @@ -35,22 +34,25 @@ uv run pytest ``` Tests use an in-memory SQLite engine; no separate migration step -needed. +needed. Full suite runs in under 4 seconds. ## Developing against a throwaway DB -Dev churn that needs a clean schema (autogenerating a new migration, -loading a scratch dataset) should target a throwaway path, never -`./quartermaster.db`. See [Operations](Operations) for why. +**Do not smoke-test or experiment against the live `./quartermaster.db`.** +See the DB safety rule in [Operations](Operations) for the full +reasoning. For any dev churn that mutates data (seeding fake entries, +exercising the HTMX flow, generating new alembic migrations), use a +throwaway path: ```sh -QUARTERMASTER_DB_URL=sqlite:////tmp/qm-dev.db \ -QUARTERMASTER_BACKUP_DIR=/tmp/qm-dev-backups \ - uv run alembic upgrade head +export QUARTERMASTER_DB_URL=sqlite:////tmp/qm-dev.db +export QUARTERMASTER_BACKUP_DIR=/tmp/qm-dev-backups +uv run alembic upgrade head +uv run uvicorn quartermaster.main:app --reload -QUARTERMASTER_DB_URL=sqlite:////tmp/qm-dev.db \ -QUARTERMASTER_BACKUP_DIR=/tmp/qm-dev-backups \ - uv run uvicorn quartermaster.main:app --reload +# when done +unset QUARTERMASTER_DB_URL QUARTERMASTER_BACKUP_DIR +rm -rf /tmp/qm-dev.db /tmp/qm-dev-backups ``` ## Project layout @@ -59,38 +61,50 @@ QUARTERMASTER_BACKUP_DIR=/tmp/qm-dev-backups \ src/quartermaster/ main.py FastAPI app factory routes.py Budget configuration handlers - routes_month.py Monthly view handlers - service.py Budget service: entries, target, zero-amount - month_service.py Month service: snapshot, deviation, zero-amount - models.py SQLAlchemy models, Section enum + routes_month.py Monthly view handlers + lifecycle transitions + service.py Budget service: entries, target, zero-amount, groups + month_service.py Month service: snapshot, deviation, lifecycle, groups + groups.py Group enum, labels, section->group mapping + models.py SQLAlchemy models, Section enum, MonthState enum db.py Engine, session, PRAGMA foreign_keys=ON config.py DB URL resolution templates/ - base.html + base.html Barlow Condensed imports + favicon index.html Budget config page month.html Month view page - month_create.html Create-flow for missing month + month_create.html Create-flow for a missing month partials/ - section.html Budget section card - target_card.html Budget debt target card - budget_zero.html Budget Zero Amount widget - month_section.html Month section card - month_target.html Month debt target card - month_zero.html Month Zero Amount widget (Planned/Applied) - month_nav.html Prev / next / picker / back-to-config - static/app.css + section.html Budget section card + target_card.html Budget debt target card + budget_zero.html Budget Zero Amount widget (with logo) + budget_group_totals.html OOB swap for all four budget group totals + month_section.html Month section card with inline HTMX edits + month_target.html Month debt target card + month_zero.html Month Zero Amount widget (with logo) + month_group_totals.html OOB swap for all four month group totals + month_nav.html prev / title / next / state / lifecycle button + static/ + app.css Full stylesheet (Barlow Condensed + ledger layout) + brand/ Optimised logo assets alembic/ env.py Includes backup-db.sh hook before migrations versions/ Migration files scripts/ backup-db.sh sqlite3.Connection.backup wrapper tests/ - test_service.py - test_routes.py - test_month_service.py - test_month_routes.py - test_zero_amount.py - test_backup_script.py + test_service.py Budget service + test_routes.py Budget routes + test_month_service.py Month snapshot + CRUD + test_month_routes.py Month routes + test_zero_amount.py Zero-amount math + widgets + test_groups.py Group mapping + subtotals + default-open + test_notes.py Notes field + snapshot copy + test_month_lifecycle.py Transitions + close gate + edit locking + test_backup_script.py Backup script shell behaviour +CLAUDE.md DB safety rule, repo-level instructions +docs/ + wiki/ Cloned wiki (gitignored) + mockups/ Design history + mockup HTML ``` ## Conventions @@ -108,24 +122,23 @@ chore/- tooling, deps, docs ### Atomic commits -Each commit is one logical change. Typical PRs in this repo have 3-5 -commits grouped by layer (data model, service, routes + templates, -tests, docs). Commit message subject under 72 chars, body under 80, -trailers include `Refs #` and the `Co-Authored-By` line for -Claude sessions. +Each commit is one logical change. Typical PRs in this repo have 3-6 +commits grouped by layer (data model, service, routes, templates, +tests, docs). Commit subjects under 72 chars, body under 80, trailers +include `Refs #` and the Claude co-author line. ### Merge flow -PRs merge via the Forgejo API (or web UI merge button), NOT `git merge` -locally. After the merge call: +PRs merge via the Forgejo API (or the web UI merge button), NOT +`git merge` locally. After the merge call: -1. Delete the remote branch. +1. Delete the remote branch via the API. 2. `git checkout main && git pull --ff-only`. 3. `git branch -d ` locally. 4. Close the tracking issue manually via the API (the `Closes #N` keyword is unreliable on this Forgejo instance). -## Adding a schema change +### Adding a schema change 1. Edit `src/quartermaster/models.py`. 2. `uv run alembic revision --autogenerate -m "short description"`. @@ -134,9 +147,9 @@ locally. After the merge call: if needed (autogenerate misses server defaults, check constraints, and index renames). 4. `uv run alembic upgrade head`. The backup hook runs again. -5. Write tests, update templates / routes. +5. Write tests, update templates and routes. -## UI verification +### UI verification Type checks and pytest do not verify UI behaviour. For any change that affects the HTML, also: @@ -144,5 +157,18 @@ affects the HTML, also: 1. Start the server against a throwaway DB. 2. Curl the affected endpoints and check for the expected classes and text in the response bodies. -3. If practical, open the page in a browser and exercise the feature. - Claude cannot do this step from the CLI; ask the human. +3. Open the page in a browser and exercise the feature. +4. Claude cannot run a real browser from the CLI; the human eyeball is + the final check before merge. + +### Design workflow + +UI experiments live under `docs/mockups/` as self-contained HTML files +(typography + stylesheet inline, real shared logo assets). Iterate +there first, then port the shipped direction into +`src/quartermaster/static/app.css` and the Jinja templates. + +Typography discipline: stick with Barlow Condensed for the family; use +weight, size, letter-spacing, and italic for hierarchy. Reserve +Fraunces / editorial serifs for future documentation or design +explorations; they are not installed for the app. diff --git a/Home.md b/Home.md index f5b727a..329fc5a 100644 --- a/Home.md +++ b/Home.md @@ -7,30 +7,65 @@ real spending on the applied side. ## Pages -* [Architecture](Architecture) — data model, routes, snapshot semantics, - zero-amount calculation -* [DevelopmentGuide](DevelopmentGuide) — setup, run, test, project - layout, conventions +* [Architecture](Architecture) — data model, route map, snapshot + semantics, lifecycle states, deviation rules, zero-amount calculation, + visual identity +* [DevelopmentGuide](DevelopmentGuide) — prerequisites, setup, run, test, + project layout, conventions, throwaway-DB pattern * [Operations](Operations) — backup script, restore, DB safety rule, - alembic hook, throwaway-DB pattern -* [Roadmap](Roadmap) — shipped and deferred work + alembic hook +* [Roadmap](Roadmap) — shipped features and what's deferred ## At a glance -* `/` — the budget configuration (the plan). One card per section with - name + amount entries. Primary Debt Target points at a Debt Minimums - row. -* `/month/YYYY-MM` — a snapshot of the budget for a specific month with - an `applied` column per entry. Deviations from the snapshot are - visibly tagged. -* Top of every page: a Zero Amount header. Green when every dollar is - assigned, amber when there is unassigned income, red when over-budget. +* `/` — the budget configuration (the plan). Sections organised into four + collapsible groups: **Income**, **Committed** (fixed bills + debt + minimums + Primary Debt Target), **Savings** (sinking funds), and + **Flexible** (food, subscriptions, other). +* `/month/YYYY-MM` — a snapshot of the budget for a specific calendar + month. Adds an `applied` column per entry to track actuals. Rows carry + deviation tags when edited or added post-snapshot. +* **Zero Amount header** on both pages. Green when every dollar is + assigned, amber when unassigned income remains, red when over-budget. + The logo anchors the left of the hero; Applied / Planned flank the big + number on month pages. +* **Per-entry notes** — free-text annotation on every row. Copied through + at snapshot time; editable inline. + +## Month lifecycle + +Each month moves through three explicit states: + +| State | Editable? | Transition | +|---|---|---| +| Planning | yes | *Activate* → Active | +| Active | yes | *Close* → Closed (requires applied zero = $0.00) | +| Closed | no | *Reopen* → Active | + +Nothing sweeps automatically. The Primary Debt Target is a hint about +where leftover applied dollars belong; filling it is the user's job. + +## Visual identity + +* **Typography**: Barlow Condensed throughout (weights 300-800 + italic), + Barlow proportional as a secondary pair. Tabular lining figures in + every numeric column. +* **Palette**: warm cream paper, warm near-black ink, burgundy accent + (`#732629`, sampled from the logo shield). Sage / ochre / indigo for + balance / under-spend / new-this-month states. +* **Layout**: ledger-style density. Entry rows carry a 2-pixel progress + bar along the bottom border that fills to the applied/planned ratio + and overshoots in burgundy when over-budget. +* **Brand**: the logo's shield encloses a Q, a $, and a `$0` — literally + the zero-based thesis. It sits in the left column of every zero hero. ## Status -Shipped: initial scaffold, monthly snapshot view with deviation tags, -database backup script with alembic auto-hook, zero-amount header. See -the [Roadmap](Roadmap) for what is next. +Shipped: initial scaffold, monthly snapshot with deviation tags, database +backup script with alembic auto-hook, zero-amount header, section groups +with collapsible headers, sinking funds section, per-entry notes, month +lifecycle with the balance gate, UI redesign in Barlow Condensed + logo. +See the [Roadmap](Roadmap) for what is next. ## Code diff --git a/Operations.md b/Operations.md index 53a3ac1..32d5139 100644 --- a/Operations.md +++ b/Operations.md @@ -105,6 +105,13 @@ You forgot `uv run alembic upgrade head` after cloning. The backup hook will print "no database at ..., nothing to back up" the first time, which is expected. +### "no such column: month_entry.notes" (or similar) + +You pulled code with a new column but did not apply the migration. +Run `uv run alembic upgrade head`. The backup hook backs up the live +DB first, then the migration adds the missing column. Common after +a pull that includes schema work (notes field, month lifecycle). + ### Alembic reports a revision it cannot locate The DB is on a schema version whose migration file is not in the @@ -115,3 +122,17 @@ or downgrade the DB to a known-good revision before continuing. Intentional. The script is sqlite-specific. Switch the URL back to sqlite:///... or do the backup manually via your Postgres tooling. + +## Current schema + +Applied migrations at time of writing: + +| Revision | Description | +|---|---| +| `f1ccdc4bc1bf` | initial schema (`entry`, `debt_target`) | +| `03ebe3c07262` | add month snapshot tables (`month`, `month_entry`, `month_debt_target`) | +| `ec804bdf366d` | add `notes` column to `entry` and `month_entry` | +| `a4ec4f8f6e9f` | add month lifecycle columns (`state`, `activated_at`, `closed_at`) | + +After pulling new code, `uv run alembic upgrade head` walks the chain +and the backup hook fires between each hop. diff --git a/Roadmap.md b/Roadmap.md index 0cdd877..2fffa46 100644 --- a/Roadmap.md +++ b/Roadmap.md @@ -8,11 +8,16 @@ | 3 | Monthly budget view with snapshot-from-config and applied tracking | 2026-04-17 | | 5 | DB backup script invoked before every schema or destructive DB operation | 2026-04-17 | | 7 | Zero Amount header at the top of budget and month pages | 2026-04-17 | +| 9 | Gitignore the local wiki checkout at docs/wiki/ | 2026-04-17 | +| 11 | Section groups with collapsible headers + Sinking Funds section | 2026-04-17 | +| 13 | Notes field per entry | 2026-04-17 | +| 15 | Month lifecycle: Planning, Active, Closed with reconciliation gate | 2026-04-17 | +| 17 | UI redesign: condensed-sans ledger style with logo in the zero hero | 2026-04-17 | -## Deferred / on the horizon +## Deferred -Things we have explicitly called out as future work. Not yet issues; -file one when the time comes. +Things we have explicitly called out as future work. File an issue when +the time comes. ### Transaction log behind applied @@ -22,23 +27,25 @@ Implies a new `month_transaction` table, a UI for entering transactions, and a migration path that preserves existing applied values as an opening balance. -### Month close-out / carryover +### Closed-month "archived" visual treatment -When a month ends, allow rolling remaining amounts into the next -month's planned values. Useful for categories where underspend rolls -forward (e.g. Food) and debatable for others (e.g. Rent). Likely a -per-section policy. +Closed months currently carry a muted state badge and disabled inputs. +A deeper treatment would desaturate the page palette, add a subtle +"Closed" watermark, and visually retire the closed month from the +editing canvas. ### Copy-forward month A "new month" that snapshots a previous month rather than the budget config. Useful when reality has drifted far enough from the plan that -last month is a better starting point. +last month is a better starting point. Would augment `create_month` to +accept a `from_month_id` parameter. -### Cross-month summary / charts +### Cross-month summary and charts -A view that shows how zero, applied, and category totals have moved -over time. Would use the existing `month_entry` data, no new schema. +A view that shows how zero-amount, applied, and category totals have +moved over time. Would use the existing `month_entry` data, no new +schema. First pass: a sparkline per category across the last N months. ### CSV import / export @@ -51,11 +58,18 @@ Single-user, local-only today. If the app ever runs on a LAN or is exposed externally, add a simple password gate or run behind an SSO-aware reverse proxy. -### Styling pass +### localStorage persistence of collapsed groups -Current CSS is functional but minimal. A typography and spacing pass -would not change semantics but would improve the "looks like a -spreadsheet" feel. +Today the open/closed state of section groups resets on every page +load. A tiny `localStorage` hook would remember which groups a given +browser last had open. + +### Budget entry name / amount edit + +Budget entries currently require delete-and-recreate to change the +name or amount. Notes are editable inline. Extending the inline edit +pattern to name and amount on the budget page would mirror what the +month page already allows. ## Out of scope (probably forever)