# How to walk this map One job: tell a cold agent which parts of the app are in force, which are decoration, and which words mean two things — before it opens a card. Verified against commit `63732df` (detached HEAD), 2026-08-16. ## The three universes | Universe | Meaning | |---|---| | **live** | In force. Implement and cite against these. | | **leftover** | Still present and still wired, but no longer the main path. Touch only if that path is in scope. | | **ghost** | Named or filed, not wired. **Do not implement against these.** | Everything in `objects/` and `processes/` is `live` unless its frontmatter says otherwise. ### Ghosts — present in the tree, unreachable - **`SchoolsController` + `app/views/schools/*` (9 files).** No route reaches them. `resources :schools` appears only inside `namespace :admin` (`config/routes.rb:58`), which resolves to `Admin::SchoolsController`. Rails scaffold remnant. If you want to change school admin, edit `app/controllers/admin/schools_controller.rb`. - **`admin_v2` / `/admin-new`.** Survives only as a comment (`config/routes.rb:42`). The in-house admin is the live `namespace :admin` at `/admin`. ### Leftovers — wired, but not the path to build on - **`users.classroom_id` direct membership.** See the trap below — this one is load-bearing, not dead. - **`PortfolioTransaction.reason: grade_earnings`** (enum value 3). Marked `# Deprecated, will be removed in future` at `app/models/portfolio_transaction.rb:13` and referenced nowhere else. New earnings use `math_earnings` / `reading_earnings`. - **`docs/old-site/`.** Training material for the predecessor site. ## The traps Four places where the obvious reading of the code is wrong. Each one has bitten or is likely to. ### 1. A student's classroom has two rival sources of truth Both are wired **right now**: | Path | Where | Used by | |---|---|---| | `users.classroom_id` | `db/schema.rb:374`, `app/models/user.rb:20` | `Classroom#students` (`app/models/classroom.rb:20`), `Order.for_teacher` (`app/models/order.rb:40-42`), `ApplicationController` redirects | | `classroom_enrollments` | `app/models/classroom_enrollment.rb` | `Student#current_classrooms` (`app/models/student.rb:24`), `Classroom#current_students` (`app/models/classroom.rb:74`) | `Student#primary_classroom` bridges them and falls back to `classroom_id` (`app/models/student.rb:37-43`, commented "for backward compatibility"). `Student` creation writes **both**: `create_initial_enrollment` fires only when `classroom_id` is present (`app/models/student.rb:10,82-86`). **Consequence:** a student enrolled only via `ClassroomEnrollment` is invisible to `Classroom#students` and to teacher order scoping. Changing either path without the other splits the roster. Start at `objects/org/classroom-enrollment.md`. ### 2. Money is integer cents — except in one method Every column is cents (`amount_cents`, `price_cents`, `worth_cents`). But `Portfolio#cash_balance` returns **dollars** as a float (`app/models/portfolio.rb:16-18` → `cash_on_hand` → `/ 100.0` at `app/models/portfolio.rb:67-69`). Callers must multiply back: `app/models/order.rb:137` does `(user.portfolio&.cash_balance || 0) * 100`. Any new caller that forgets is off by 100×. Detail: `objects/money/portfolio.md`. ### 3. Cash is never stored `portfolios` has exactly three columns — `id`, `user_id`, timestamps (`db/schema.rb:181-186`). There is no balance column. Every balance is a live SUM over `portfolio_transactions`, **plus a subtraction for pending orders and their fee** (`app/models/portfolio.rb:71-100`). Balance is therefore a function of open orders, not just settled history. ### 4. One API key, two homes, two different failure modes | Reader | Source | Missing-key behaviour | |---|---|---| | `AlphaVantageApiClient` | `ENV["ALPHA_VANTAGE_API_KEY"]`, default `nil` (`app/services/alpha_vantage_api_client.rb:11`) | logs an error and returns `nil` (`:31-36`) | | `StockAttributeUpdate` | global `API_KEY` (`app/services/stock_attribute_update.rb:75`) from `config/initializers/api_keys.rb:1`, default `"test-api-key"` | silently queries Alpha Vantage with a junk key | One fact, two homes. If you consolidate, `objects/trading/stock.md` lists what reads it. ## Name collisions, stated once | Product word | Code name | Note | |---|---|---| | SIF dollars | `PortfolioTransaction#amount_cents` | integer cents; no `Money`/`BigDecimal` wrapper | | balance / cash on hand | `Portfolio#cash_balance` | derived; returns **dollars** | | grade (5th–8th) | `Grade`, `grades.level` | `app/models/grade.rb` | | grade (A+…F) | `GradeEntry#math_grade`, `#reading_grade` | `app/models/grade_entry.rb:15` | | admin | `users.admin` boolean | **not** an STI type (`app/models/user.rb:39,43`) | | student / teacher | STI on `users.type` | `Student < User`, `Teacher < User` | | holding / position | `PortfolioStock` rows vs `PortfolioPosition` | rows are append-only lots; the PORO is the aggregate | | gradebook "finalize" | `GradeBook#verified!` then `completed!` | two statuses, one button | | Stocks for Good | `StocksInTheFuture` | `docs/README.md:1` vs `config/application.rb:14` | ## Walking order 1. This file — universes and traps. 2. `objects/_index.md` — find the noun. 3. One card. Follow its `See:` link into the app tree. 4. Before editing: `effects/CONTEXT.md`. Do not read the whole `objects/` folder. The index exists so you do not have to.