Codebase Architecture · Living Progress Plan
Codebase Rewrite
Incrementally reduce this codebase's largest offenders — starting with
dashboard/server.py, and extending to the CLI, headless, and websocket
layers — to composition roots. Every side effect crosses an injected Interface;
every behavior lives in a focused Object or pure function; every extraction is pinned
by tests before it moves.
Progress
Operating rules
- Program to the Interface. Domain services import ports, never concrete adapters.
- Wire once. Concrete construction belongs in
server.pyor a dedicated application factory. - TDD is mandatory. No production Interface or Object is written before its failing test. Work proceeds Red → Green → Refactor.
- Characterize before moving. Characterization tests pin public output, errors, caching, locking, and failure behavior before legacy code moves.
- Types are executable contracts. Python boundaries use strict Pydantic models; new browser code uses strict TypeScript.
- Keep pure code simple. Pure transforms remain functions; do not create an Interface without a boundary.
- Side effects require ports. Network, process, filesystem, database, clock, logging, metrics, and notifications are injected.
- One slice per change. Interface, Object, adapter, tests, wiring, and compatibility shim travel together.
- No live dependencies in unit tests. Use small fakes that implement the same port contract.
- Preserve HTTP contracts. Status, headers, JSON shapes, timeouts, and route names stay stable unless separately approved.
- Delete completed detail. Once a row meets its exit test, remove it from this ledger. Git is the completion history.
Strict typed contracts and test-first development
Python contract policy
ABCs describe behavior; Pydantic models describe data. Commands, results, API requests/responses, repository records, configuration, events, and adapter payloads use named models derived from one strict base:
from pydantic import BaseModel, ConfigDict
class StrictModel(BaseModel):
model_config = ConfigDict(strict=True, extra="forbid", frozen=True)
- No unvalidated
dict[str, Any]may cross a port. Validate once at the outer adapter, then pass typed models inward. - No implicit coercion at a boundary:
"1"is not accepted where an integer is required. - Unknown fields fail closed through
extra="forbid"; immutable DTOs prevent hidden mutation. - Discriminated unions model variant results such as up/down/starting or receipt/statement/unknown.
- Every public function and method in a rewritten module has complete parameter and return annotations.
Any, unparameterized containers, and blanket type ignores require a written decision-log exception.
Static checking runs with pyright --project dashboard/pyrightconfig.json,
where typeCheckingMode = "strict". Runtime Pydantic validation
complements static checking; neither replaces the other.
TypeScript contract policy
- All new frontend code is TypeScript (
.ts), withstrict,noUncheckedIndexedAccess,exactOptionalPropertyTypes, anduseUnknownInCatchVariables. - Existing JavaScript may remain only as legacy. A touched module is migrated or placed under
// @ts-checkwith complete JSDoc types until migration is practical. - No blind type assertions (
as Foo) for HTTP JSON. A type guard or runtime schema validates unknown input first. - Discriminated unions replace stringly typed status objects and scattered property checks.
bunx tsc --noEmit -p dashboard/tsconfig.jsonmust pass before tests and deployment.- The TypeScript build emits browser JavaScript deterministically; generated output is never hand-edited.
TDD order for every ledger row
- RED: write the consumer behavior test and run it. Record the expected failure; import/contract absence counts as red for a new extraction.
- Add Characterization tests around the legacy seam so the behavior being preserved is explicit.
- Add Shared contract tests that every concrete adapter and fake must pass.
- Add Negative validation tests proving malformed types, missing fields, extra fields, and invalid variants are rejected.
- GREEN: add only enough typed Interface/Object code to make the focused tests pass.
- REFACTOR: remove duplication and improve names while the tests and type checkers stay green.
- Run the focused suite, complete Python suite, complete browser suite, static type checks, and coverage gate.
Required test layers
| Layer | Purpose | Required evidence |
|---|---|---|
| Characterization tests | Freeze current route/service behavior before movement | Success, error, timeout, cache, and concurrency cases |
| Domain unit tests | Drive service behavior through fake ports | No network, filesystem, process, clock, or database access |
| Shared contract tests | Hold real adapters and fakes to one behavioral contract | Parameterized suite runs against every implementation |
| Negative validation tests | Prove strict Pydantic/runtime TypeScript validation | Wrong, missing, extra, and ambiguous data fails closed |
| Adapter integration tests | Verify serialization and external protocol mapping | Fixture servers/files/processes only; no production systems |
| Composition tests | Verify every port is wired once to a valid concrete | Application builds and all routes register without serving |
| HTTP/browser tests | Protect public routes, headers, JSON, navigation, and rendering | Black-box request and DOM assertions |
Target architecture
dashboard/
server.py # construct adapters, start threads/server, compatibility exports
http/ # route registry, controllers, HTTP/WebSocket adapters
agents/ # Letta gateway, directory, messaging, agent health
server_management/ # health probes, logs, SSH, restart orchestration
model_usage/ # usage sources, history, provider health/failover
intake/ # scanners, classification, staging, dispatch, Trainer
finance/ # expense/receipt/report repositories and services
voice/ # existing transcription/cleanup pipeline
tests/ # mirrors the production domains above
The directory names are targets, not a mandate to move everything at once. A slice may begin as one flat module when that keeps the change reviewable.
Definition of done for the rewrite
server.pyis startup and dependency wiring, ideally below 500 lines.DashboardHandleradapts HTTP to registered controllers and contains no business rules.- Every concrete external dependency can be replaced by a test fake through constructor injection.
- All Python boundary data is strict Pydantic; all new frontend modules are strict TypeScript.
- Pyright and TypeScript complete with zero errors and no unexplained suppressions.
test_server.pycontains only composition/compatibility tests; domain tests live beside their domains.- The complete Python and JavaScript dashboard suites pass without contacting live infrastructure.
Active Interface/Object ledger
Each work row is one reviewable extraction. The checkbox is deliberately visual rather than browser-persistent: completion is shared by deleting the row in source control, not by storing private state in one browser.
| Status | Interface / Port | Object / Adapter | Current seam | Exit test |
|---|---|---|---|---|
| 0 — Test and composition foundation | ||||
| ITestEnvironment | DashboardTestEnvironment | Venv capability setup and test isolation | Full suite is green or optional capabilities skip explicitly | |
| Pydantic boundary contract | StrictModel | Untyped dicts, tuples, and optional fields crossing seams | Strict/frozen/extra-forbid models cover every new boundary | |
| IPayloadCodec | PydanticPayloadCodec | Manual JSON decoding, coercion, and response shaping | Negative contract tests reject wrong and extra fields | |
| ITypeCheckGate | PyrightAndTypeScriptGate | Dashboard Python/JS currently outside strict project type gates | Strict Pyright and tsc --noEmit pass in one command | |
| IContractTestSuite | SharedContractTestSuite | Adapter and fake behavior can drift independently | Every port implementation runs the same contract suite | |
| IClock | SystemClock, FixedClock | time.time(), datetime.now(), TTLs | Time-sensitive services run against FixedClock | |
| ICommandRunner | SubprocessCommandRunner | Direct subprocess.run/Popen | Services assert commands without spawning processes | |
| IHttpTransport | UrllibHttpTransport | Direct urllib.request calls | Timeout, HTTP error, and JSON contracts have adapter tests | |
| IActivityRecorder | JsonActivityRecorder, NullActivityRecorder | JSON log helpers and cross-cutting events | No service imports a concrete log writer | |
| 1 — HTTP boundary | ||||
| IRouteController | RouteRegistry | do_GET/do_POST condition chains | Every API route is registered and duplicate routes fail startup | |
| IRequestBodyReader | HttpRequestBodyReader | Body length, decoding, JSON parsing | Malformed/empty/oversized bodies have contract tests | |
| IResponseWriter | DashboardResponseWriter | json_response, error_response, cache headers | Status, headers, and response shapes match characterization tests | |
| IStaticAssetStore | LocalStaticAssetStore | HERE/REPO_ROOT file resolution and serving | Traversal, type, missing-file, and no-cache tests pass | |
| ITerminalSession | PtyTerminalSession | PTY spawn, resize, reap, WebSocket bridge | Frame/session lifecycle tested without a real shell | |
| IDashboardApplication | DashboardApplication | Startup threads, caches, handler globals | server.py only constructs and starts the application | |
| 2 — Letta agents | ||||
| ILettaGateway | UrllibLettaGateway | Agents, thoughts, tool calls, and writes; model reads migrated behind AgentModelOptionsService on 2026-07-30 and raw message retrieval migrated on 2026-07-31 | No agent service constructs a Letta URL; remaining callers keep this row active | |
| IAgentDirectory | CachedAgentDirectory | LETTA_AGENTS, discovery, cache refresh | Registry ordering and cache behavior are unit tested | |
| IAgentMessageService | AgentMessageService | /api/test and message shaping | Reset/send/fallback behavior uses a fake gateway | |
| IAgentHealthProbe | AgentHealthProbe | Required tools, SDK endpoint, send errors | Health matrix is deterministic without live agents | |
| IAgentPromptRunner | LettaCodePromptRunner | Headless CLI command and prompt validation | Permission mode, timeout, and final-result tests stay pinned | |
| IAgentSystemMessageRepository | FileAgentSystemMessageRepository | Per-agent system message files | Missing and unsafe paths fail closed | |
| 3 — Server Management | ||||
| IHealthProbe | HttpHealthProbe, TcpHealthProbe, CommandHealthProbe | HEALTH_CHECKS and check variants | Adding a probe requires an adapter plus one registry entry | |
| IServerRegistry | ServerRegistry | SERVERS config and validation | Invalid dependencies/check names fail at construction | |
| IServerHealthService | ServerHealthService | Status computation, dependency roll-up, cache | All green/yellow/red transitions have clock-controlled tests | |
| IServerController | ServerController | Restart/start command dispatch | Unknown, blocked, starting, and success outcomes are tested | |
| IServerLogSource | FileServerLogSource, RemoteServerLogSource | Tailing, filters, remote Letta log pull | Sequence/filter behavior runs against fixtures | |
| ISshGateway | OpenSshGateway | SSH health, Windows/WSL commands | No health/controller object assembles raw SSH commands | |
| IAvailabilityTracker | AvailabilityTracker | Starting windows and down duration globals | State transitions use injected clock and isolated state | |
| IPcMetricsSource | LocalPcMetricsSource, SshPcMetricsSource | PC monitor extraction, cache, rate calculation | Parsing is pure and collection uses fake sources | |
| 4 — Model usage and provider health | ||||
| IModelUsageSource | CodexUsageSource, ClaudeUsageSource, AntigravityUsageSource | Token extraction and remote source registry | Each source passes one shared contract suite | |
| IModelUsageService | ModelUsageService | model_stats, labels, classification | Service contains no provider-specific condition chain | |
| IUsageHistoryStore | JsonUsageHistoryStore | Samples, pruning, burn rate, slow leak history | Corrupt/missing history degrades safely | |
| IProviderHealthMonitor | ChatGptProviderHealthMonitor | Zero-token polling and fleet error flags | One probe updates the complete provider fleet deterministically | |
| IProviderFailoverStrategy | ChatGptTokenFailoverStrategy | Standby headroom, swap command, cooldown | Decision logic and command adapter are independently tested | |
| 5 — Scanner and intake workflow | ||||
| IScannerDevice | WindowsWiaScanner | Device selection, locking, WIA invocation | Scanner choice never depends on enumeration order | |
| IScannerRegistry | ScannerRegistry | Window/Freezer configuration | Unknown and duplicate devices fail at construction | |
| IScannerDiagnostics | HpScannerDiagnostics | 2,232-line scanner/fix/diagnostic region | Every LED and remediation message has fixture tests | |
| IPrinterRepairService | DeskJetPrinterRepairService | fix_deskjet_printer | Repair never claims success without device evidence | |
| IDocumentClassifier | IntakeFacadeClassifier | Facade invocation and classification shaping | Ready/busy/offline/error cases use one typed result | |
| IDocumentStager | LocalAndRemoteDocumentStager | Local copy and Win10 mirror | Local authority and nonfatal mirror failure are pinned | |
| IAgentConversationFactory | MazdaConversationFactory | Per-scan Letta conversation creation | Conversation failure stops dispatch without side effects | |
| IDocumentDispatcher | MazdaDocumentDispatcher | 482-line scan message and notify call | Message builder is pure; gateway owns delivery | |
| ITrainerNotifier | DetachedTrainerNotifier, NullTrainerNotifier | Trainer command and detached spawn | Tests cannot spawn a real Trainer by construction | |
| IIntakeEventStore | JsonIntakeEventStore | Recent report pointer and expense-stored events | Merge, dedupe, pruning, and corrupt JSON are tested | |
| IDocumentIntakeService | DocumentIntakeService | process_scanned_document, process_pdf_document | Workflow is an injected coordinator with no module globals | |
| 6 — ROL Finance | ||||
| IExpenseRepository | MySqlExpenseRepository | Expense lookup, category changes, notes | Services never import DB connection helpers | |
| IReceiptRepository | FileReceiptRepository | Receipt mounts, index, matching, URL mapping | Mount/index policy has contract tests and injected clock | |
| IReportRepository | FileReportRepository | Report aliases, discovery, status, row lookup | Path safety and ambiguity rules are isolated | |
| IReportRowWriter | HtmlReportRowWriter | Static row category/color updates | Writes are idempotent and fixture snapshots stay stable | |
| IExpenseCategorizationService | ExpenseCategorizationService | recategorize_expense and undo orchestration | DB, report, taxonomy, undo ports are constructor-injected | |
| IReceiptLookupService | ReceiptLookupService | Lookup/presence/source-document resolution | Every matching tier is tested without real files or DB | |
| ISupportingDocumentService | SupportingDocumentService | Supporting-document lookup/open flow. 2026-08-06 startup-repair checkpoint: a broken uncommitted composition-root import referenced the absent supporting_document_application.py, so the dashboard crashed before binding port 8765. Restored the existing in-server compatibility implementation and removed the dead adapter wiring: server.py 11,126→11,066 (−60, −0.539% observed working-file delta; slice-attributable −60), 11,066 vs 10,486 baseline (+580, +5.53%). Focused supporting-document suite: 21 passed. The full paired run had one unrelated pre-existing annotation fixture failure. Next deletion target remains the legacy in-server flow after a real typed module is added and tested. | Uses existing annotation port and injected repositories | |
| IFinanceReportService | FinanceReportService | Recent intake/report and receipt-only HTML. 2026-08-02 partial: equivalent duplicate-row projection moved behind pure recent_intake_view.py policy; live Window scan 561/1519 now collapses without merging different merchants. server.py 11,046→11,050 (+4, +0.036%); 11,050 vs 10,486 baseline (+564, +5.38%). Last three commits: +90, +53, 0 lines (net +143); HEAD→worktree +4. Non-negative because this slice adds composition-root wiring for the extracted policy; next deletion target is the remaining build_recent_intake_html renderer into FinanceReportService. | Rendering is separate from data acquisition 2026-08-12 Toyota receptionist slice: frontend transcript state and injected cheap-model intent strategy are complete, but this is a voice/receptionist boundary rather than a FinanceReportService extraction. Server composition wiring added voice/receptionist.py and /api/receptionist-intent; HEAD→worktree is 11,359→11,234 lines (−125 observed, including unrelated concurrent intake/report deletion), with +12 lines attributable to the receptionist import/route. Focused JS 50 passed; focused Python 26 passed. | |
| IRecentScanRepository | MySqlRecentScanRepository | Recent scans and month status queries | Query results map to typed records behind one port | |
| IVendorReviewService | VendorReviewService | Vendor keys, pending review, set vendor | Validation, categorization, and persistence are isolated | |
Established examples — do not move backward
These are outside the active count because they already demonstrate the intended shape.
category_taxonomy.py.category_undo.py.document_annotation.py.voice/.statement_review.py.js/abstract/, concretes under js/implementation/.One-slice workflow
- Choose exactly one active row; record its current callers and mutable globals.
- Write the failing consumer test first, run it, and save the RED command plus failure.
- Add characterization, shared-contract, and negative-validation tests before production code.
- Define the smallest fully typed ABC and strict Pydantic models in the owning domain. The abstract module imports no concrete adapter.
- Implement only enough typed behavior for GREEN, including a minimal fake that passes the same contract tests.
- Inject the port into the service/controller; construct the concrete only at the composition root.
- Leave a temporary forwarding function in
server.pywhen compatibility requires it. - Move relevant tests from
test_server.pyinto the matching domain test file. - Refactor with tests green, then run Pyright, TypeScript, focused tests, full suites, coverage, and the relevant manual smoke check.
- Record current line counts, delete this work row, and commit the shrinking plan with the implementation.
Required evidence in each handoff or PR
Slice:
Observed RED:
RED command and failure:
Interface:
Pydantic model(s):
TypeScript type(s):
Concrete Object(s):
Callers migrated:
Compatibility shim:
Focused tests:
Contract/negative tests:
Pyright result:
TypeScript result:
Branch coverage:
Full Python result:
Full JavaScript result:
server.py lines before/after:
test_server.py lines before/after:
Ledger row removed:
Decision log
2026-08-03 SSH compatibility slice: The Windows 10 Host SSH probe now routes through an injected ISshGateway port and OpenSshGateway Adapter, with ConfiguredIdentityStrategy owning deployment-specific key selection and ICommandRunner isolating subprocess execution. This replaces the previous target-specific command branch and makes the credential policy testable without a network. The focused gateway/server suite is green (368 tests), and all live dashboard instances now return CONNECTED — DESKTOP-SHDBATI. The working file measured 11,061 lines / 518,879 bytes after this slice versus 11,046 / 518,026 at HEAD: +15 lines, +853 bytes, +0.136%; the observed +15 includes +11 lines from the already-present SSH fallback patch and unrelated working-tree edits, so the slice-attributable delta is +4 lines (the compatibility shim/import). Current count is 575 above the 10,486 historical baseline (+5.48%). Last three commits are +90, +53, 0 lines; HEAD→worktree is +15 lines / +853 bytes. The non-negative delta is explained by introducing the port/adapter and its tests before deleting the compatibility shim; the next deletion target is ssh_test forwarding and the remaining raw SSH configuration in server.py.
- 2026-07-29
- Use an incremental ports-and-adapters rewrite; do not replace the stdlib HTTP server or change routes as part of extraction.
- 2026-07-29
- Interfaces are required at meaningful boundaries and all cross-cutting side effects. Pure transforms remain functions.
- 2026-07-29
- Completed work rows are deleted instead of archived here, so this page becomes smaller as the rewrite advances.
- 2026-07-29
- Begin with the green test baseline, then model usage as the pilot production slice.
- 2026-07-29
- Adopt strict Pydantic v2 models for every Python boundary and strict TypeScript for all new frontend work; static and runtime validation are both required.
- 2026-07-29
- Adopt strict TDD: witness RED before production code, reach GREEN minimally, then refactor; new domain/application modules require 100% branch coverage.
- 2026-07-30
- Completed the model-read sub-slice of
ILettaGateway: RED waspytest -q tests/test_letta_gateway.pyfailing because theagentsboundary did not exist; GREEN was 14 focused gateway/model-option tests. Added strict frozen Pydantic contracts,FakeLettaGateway,UrllibLettaGateway, composition-root wiring, and a temporaryagent_model_payloadforwarding shim. The ledger row remains because messages, thoughts, tool calls, and write paths are not migrated. - 2026-07-31
- Completed the raw-message-read sub-slice of
ILettaGateway: RED waspytest -q tests/test_letta_gateway.pyfailing to import the not-yet-createdLettaAgentMessage; GREEN was 34 focused gateway tests plus 360test_server.pytests. Added a strict immutable normalized message contract, shared fake/urllib behavior, fail-closed envelope and field validation, URL ownership inUrllibLettaGateway, and a temporaryletta_messagesforwarding shim. Installed/configured Pyright and branch coverage for the rewrittenagents/boundary; strict checking reports zero errors and the focused suite covers 229 statements and 90 branches at 100%. GoF dependency tests now prohibit application-service imports of concrete adapters and require the composition-root gateway plus compatibility shim to exposeILettaGatewayexplicitly;UrllibLettaGatewayis constructed only inserver.py. The public activity-view shapes and 25-second timeout remain unchanged. The ledger row remains for roster, thought/tool-call application services, and write paths.