Files
odysseus/docs/frontend-architecture/handbook.md
T
Alexandre TeixeiraandLéo f1a47f3812 refactor(notes): decompose Notes into feature-owned modules (#6713)
* refactor(notes): decompose Notes into feature-owned modules

* fix(notes): avoid HTML interpolation in reminder picker

* docs(notes): drop checkpoint archives, session logs and publication drafts

The N0-N6 tarballs and raw pytest/node logs are not read by any script or
test, and the logs carry absolute home-directory paths. Once squashed into
dev they would stay in history for good. notes-pr.md and notes-issue.md
were the drafts used to open #6712 and #6713, not documentation.

notes-validation.md keeps the counts, the checkpoint hashes and
implementation.sha256 as the record; local interpreter paths and the local
Docker image name are replaced with portable placeholders.

* fix(notes): keep the state store raw and stop masonry re-observing after close

- state: writes built from views (filter, spread, splice) stored nested
  read-only views in the store, so structuredClone on a stored note threw
  DataCloneError. Store writes now unwrap nested views.
- layout: a save that resolves after closePanel re-renders the leaving pane
  and re-created the masonry ResizeObserver that teardown had disposed.
  Skip creating it once Notes is closed.
- index.js: drop 30 imported or declared names that were never used and
  group the imports at the top. Import order is unchanged.
- Revert unrelated punctuation edits in .gitattributes and keep the header
  comment and docstring first in compare/vote.js and
  test_notes_dom_xss_helpers.py.

Both fixes have a node test that fails on the previous head.

* test(notes): make script rejection assertion case-insensitive

Address CodeQL js/bad-tag-filter alert #1150 in test-only assertion and refresh Notes evidence manifest checksum.

---------

Co-authored-by: Léo <leograndcontact@gmail.com>
2026-10-15 09:29:41 +01:00

232 lines
14 KiB
Markdown

# Odysseus frontend architecture handbook
Status: F1 merged in PR #6701. The complete Notes reference is implemented as an
unpublished review diff on `refactor/notes-reference-decomposition`. Read its
[case study](notes-case-study.md), [module inventory](notes-module-inventory.md)
and [validation receipts](notes-validation.md). Publication and the recipient's
post-integration reproduction remain pending.
Read the [ADR](ADR-001-source-and-deployment.md), [assessment](assessment.md),
[validation report](validation.md) and [Notes roadmap](notes-roadmap.md).
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.
The Notes reference uses internal read-only live views to preserve captured note
identity across existing asynchronous completions. Its state owner supplies all
mutations; the public index exposes operations without the store. Notes deliberately
retains last-completion fetch ordering. Do not infer request-generation guards from
its decomposition. Its page-owned reminders survive panel closure, and panel/editor
listeners and observers have separate disposal scopes.
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.
```sh
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](notes-roadmap.md).
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](notes-roadmap.md); 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.