Files
odysseus/docs/frontend-architecture/handbook.md
T

14 KiB

Odysseus frontend architecture handbook

Status: F1 foundation and continuation guide, pending review. The final Notes reference case study belongs to the comprehensive Notes draft PR; the recipient handoff follows its integration. This document must evolve with reviewed internal checkpoints and distinguish executed work from proposals.

Read the ADR, assessment, validation report and Notes roadmap. The frontend program ends with the Notes reference decomposition and handoff. Calendar, Documents, Email, Chat, Skills, Settings and other later decompositions belong to the designated frontend maintainer; do not start them automatically.

Source, ownership and public APIs

frontend/src/app/ composes public feature APIs and platform ports. Each features/<feature>/index.js is an intentional API, with explicit named exports and a compatibility default only when existing consumers require one. Internals live under domain/, application/, infrastructure/ or ui/ as needed. Do not create empty directories, boilerplate controllers or a framework by convention. Feature names app and shared are reserved.

Keep pure business rules in domain; transitions and workflows in application; HTTP/storage/browser services in infrastructure; DOM rendering and lifecycle in UI. The index composes adapters, state and views. Application receives adapters as explicit arguments or constructor ports, rather than importing concrete infrastructure. Views call application operations; infrastructure does not own business decisions. A public API returns data/operations, not mutable stores or private DOM nodes. Importing an API should define capabilities; explicit lifecycle methods activate them. Legacy side effects remain characterized until a reviewed slice safely changes their placement.

Shared code has a named, proven cross-feature responsibility. shared/domain/ holds pure primitives, shared/platform/ holds narrowly scoped platform adapters, and shared/ui/ holds common DOM capabilities. A named stateless root leaf such as shared/actionMenuOrder.js is also allowed and receives domain restrictions. Do not create a central utils.js collection or move feature business rules into shared simply to satisfy an import rule.

Mechanically checked dependency contract

Importer Permitted maintained-source targets
App composition Other app code, shared code, feature index.js APIs
Feature index Its own responsibility modules and shared code; other feature public indexes
Feature domain Its own domain and shared domain/stateless leaves
Feature application Its own application/domain, shared domain, other feature public indexes
Feature infrastructure Its own infrastructure/domain, shared domain/platform
Feature UI Its own UI/application/domain, shared domain/platform/UI
Shared domain leaf Shared domain leaves only
Shared platform Shared platform/domain only
Shared UI Shared UI/platform/domain only

No feature imports app composition. No shared code imports a feature. No external feature imports private responsibility modules. UI and infrastructure communicate with another feature through a supplied application operation/port. Maintained source dependency cycles, including literal lazy imports, fail the architecture gate. If features need bidirectional coordination, app composition injects a callback or adapter instead of creating mutual imports. Prefer this even when a one-way public index import would pass the gate.

Cross-feature events must document event name, payload, acknowledgement, owner, subscription disposal and delivery timing. Keep them local and specific; F1 adds no global event bus. Preserve odysseus:notes-context-selected and its synchronous accepted acknowledgement in future Notes work. The server remains authoritative for owner filtering and untrusted selected IDs. Frontend APIs cannot grant access.

The checker parses executable ESM, forbids computed imports in new source, requires exact precache entries, rejects unresolved/remapped relative imports, and permits only canonical static JS URLs with one unversioned identity for maintained modules. Domain code cannot mention known browser/HTTP/timer globals or an ambient clock; use now/clock inputs. This is a conservative vocabulary check, including shadowed names, not a semantic purity proof. Randomness, hidden side effects, authorization and lifecycle behavior require behavioral tests.

Legacy imports from new source default to forbidden. If an incremental bridge is necessary, declare legacyImports on that source's manifest entry as a list of { "specifier": "/static/js/example.js?v=existing", "reason": "contract, owner and removal slice" }. Only feature composition, infrastructure or UI boundaries may use it. Exact specifier/reason and target existence are required; unused exceptions fail. Keep the existing query identity. New domain/application modules receive no legacy exception. F1 has zero exceptions. Do not use exceptions to invent new shared platform dependencies on feature internals.

State and lifecycle

Each feature has one authoritative mutable application-state owner. A transition operation owns writes; UI receives a view/snapshot or subscription. Preserve existing objects and initialization timing when extracting a stateful module. Do not instantiate a new store in both the legacy facade and the feature API. Compatibility facades only forward the same live implementation.

Document lifecycle scopes: page, authenticated user, session, mounted panel and editor/draft. Name owners of subscriptions, DOM listeners, timers, observers, pending requests and caches. Mounting must be idempotent; disposal releases the resources it owns. Background reminders may outlive an open panel. Closing the panel must not silently stop those reminders during extraction.

Keep request-generation and selected-session checks around asynchronous completions. Serialize writes where the current contract requires it. Dispose subscriptions explicitly and prevent late updates to a replaced view. Feature state changes on user/session switch must follow current cleanup policy. Browser storage is for preferences/drafts/cache state, never a substitute for backend authorization, credential storage or owner filtering. Define every storage key's owner, retention/reset behavior and compatibility format before moving it.

Creating a module or introducing a feature

  1. Identify responsibility, owner, public consumers, side effects and behavioral evidence before editing. Start with a pure leaf when introducing a feature.
  2. Place only the required source file(s), for example features/example/domain/calculate.js. Add an index only when there is a real supported feature API to compose.
  3. Register each source in frontend/modules.json. A typical new mapping is features/example/domain/calculate.js to js/features/example/domain/calculate.js. Preserve the source/deployment relative graph. The proof's legacy URL mapping is an explicit special case.
  4. For an extracted leaf, the legacy feature coordinator may import its output as a temporary composition bridge. New maintained features use public indexes; the legacy tree is not globally forced into the new boundary rules in F1.
  5. Add the exact output URL to the appropriate explicit SW array, retain lazy evaluation timing and bump the cache name when the graph changes. Declare text eol=lf for both mapped paths in repository-owned .gitattributes files; source-tree or scoped output patterns are allowed. The publisher checks effective Git rules, including nested overrides, and rejects a missing LF rule even if output bytes already match. Do not broaden unrelated legacy checkout rules or automatically rewrite all existing query identities.
  6. Run the publisher, inspect the source/output diff, then run architecture, identity, graph, offline and focused behavior checks. Include both sides of the mapping in review. Retiring output requires a separate reviewed deletion; the publisher does not clean the static tree.
python scripts/build_frontend.py
python scripts/build_frontend.py --check
npm ci --ignore-scripts --no-audit --no-fund
node scripts/check_frontend.mjs
node scripts/check_frontend.mjs --inventory
node --test tests/frontendArchitecture.test.mjs
./venv/bin/python -m pytest -q -rs tests/test_frontend_build.py tests/test_frontend_foundation_offline_js.py tests/test_frontend_module_graph.py tests/test_frontend_module_version_parity.py tests/test_panel_loader_js.py
./venv/bin/python scripts/verify_frontend_runtime.py

Use your actual repository virtualenv path; the F1 environment is recorded in validation. Node 20+ follows current CI. Existing Playwright/browser prerequisites are documented in tests/README.md and tests/KNOWN_FAILURES.md. Missing Node can skip Python browser wrappers, which is a coverage gap, not a pass. The standalone Node gates require locked development dependencies and fail when missing.

The publisher validates all inputs before writing and only overwrites owned outputs whose bytes differ. It is idempotent and working-directory independent. Git must be available for publication and --check; neither runs at application startup. Attribute evaluation uses a private temporary Git repository and ignores global/local overrides, so checkout consistency is reproducible in CI. It adds no runtime configuration or backend imports. CI checks LF attributes, parity and source syntax in existing jobs; the Node job also runs architecture self-tests. Existing runtime identity tests remain authoritative across legacy imports, HTML and SW. The isolated runtime probe is included in the new pytest publication tests.

Review, release and rollback

Review the source/output mapping and exact exported API, then the owner/dependency contract and behavior tests. Check manifests, URL identities, MIME/CSP, load timing, storage compatibility and disposal. Reject unexplained feature behavior changes. Do not repair unrelated defects under an extraction title.

Release from a validated revision with committed output. Native startup and packagers consume static files without a compiler. Run parity before producing a Docker or desktop artifact. Test the actual packaged application on its platform; Linux contract probes cannot certify macOS/Windows executables. Deploy matching shell, facades, graph and manifests as a unit. Keep an authorized path to the last complete artifact. Rollback restores the whole matching set, not just an entry file or cache version. Do not delete a facade until its public consumers have migrated and compatibility tests make that decision reviewable.

Proposal and PR templates

For a decomposition proposal, record: baseline; responsibility map; current public API and consumers; state owner; I/O and lifecycle boundaries; planned slice; allowed dependencies/temporary exceptions; preserved behavior; deterministic characterization tests; deployment/offline changes; overlapping PRs; rollback; explicit non-goals and remaining uncertainties.

For each migration PR, lead with the responsibility moved and resulting boundary. Record source/output paths, API compatibility, state/lifecycle ownership, characterization results, exact validation commands, platform gaps, exception removal plan, upstream dependencies and rollback. Attach a concrete before/after example when needed to establish behavioral equivalence. Preserve the repository PR template and never claim unexecuted checks.

F1 is an entirely separate foundation-only PR. Deliver the complete Notes decomposition, regression tests, compatibility validation and final reference case study through one draft PR targeting official dev, using one Notes branch and worktree after F1 integration. N0 through N6 and the N5 substages are internal engineering checkpoints, not independent PRs or merges. Each checkpoint records focused tests, review evidence, its validated revision or reproducible snapshot, and rollback to the prior matching source/output/facade/precache set. Keep the draft open while those checkpoints progress; mark it ready only after the entire decomposition passes the roadmap's acceptance gates.

Structure the Notes PR description around the final integrated design and link each checkpoint's evidence. The receiving maintainer may use smaller independent PRs for future frontend features at their discretion; the one-PR Notes decision does not impose that delivery size on later work.

Program backlog and final handoff

Priority order: review/integrate F1; resolve root offline certification with the owner of PR #6227; complete the Notes checkpoints and final reference case study within one draft PR following the roadmap; integrate the complete Notes PR after its acceptance gates; finalize the recipient handoff. Only then does the receiving maintainer prioritize Calendar, Documents, Email, Chat, Skills and Settings against current behavior and risk. Existing partial packages and high fan-in helpers need assessment, not automatic replacement.

Final H acceptance requires a designated maintainer, the integrated Notes PR revision and its internal checkpoint receipts, the actual public API/state/lifecycle map and final reference case study included in that PR, validated deployment/offline commands, known exceptions/debt, outstanding PR dependencies, rollback evidence and a reproducible independent setup by the recipient. F1 provides the handbook structure and contracts; it does not claim Notes or the completed handoff have occurred. The initiating architect then returns to separately authorized non-frontend architecture work.