5.3 KiB
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 :schoolsappears only insidenamespace :admin(config/routes.rb:58), which resolves toAdmin::SchoolsController. Rails scaffold remnant. If you want to change school admin, editapp/controllers/admin/schools_controller.rb.admin_v2//admin-new. Survives only as a comment (config/routes.rb:42). The in-house admin is the livenamespace :adminat/admin.
Leftovers — wired, but not the path to build on
users.classroom_iddirect 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 futureatapp/models/portfolio_transaction.rb:13and referenced nowhere else. New earnings usemath_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
- This file — universes and traps.
objects/_index.md— find the noun.- One card. Follow its
See:link into the app tree. - Before editing:
effects/CONTEXT.md.
Do not read the whole objects/ folder. The index exists so you do not have to.