Files
odysseus/docs/frontend-architecture/notes-case-study.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

261 lines
16 KiB
Markdown

# Notes reference decomposition
Implementation started: 2026-10-13. Final validation: 2026-10-14.
Foundation: [PR #6701](https://github.com/odysseus-dev/odysseus/pull/6701),
`1cc56cb9d78f3e2399d1d61155b71d771a17b85b`. Branch was safely updated via
non-destructive fast-forward to latest official `upstream/dev` at
`86b886c611338282af6d4a717246ea4e256f58df`, incorporating PRs #6704 and #6693.
Neither changed a file in the Notes diff. The independent AGY audit returned
CONDITIONAL PASS; all pre-publication corrective actions (issue template conformance,
PR template conformance, visual evidence paths, and em-dash removal) have been completed.
The entire implementation belongs to one Notes PR. N0 through N6 are internal
checkpoints, not separate branches, commits or PRs. The [validation receipts](notes-validation.md),
[module inventory](notes-module-inventory.md) and [changed-file inventory](notes-changed-files.md)
accompany this case study.
## Responsibility map and size
The original `static/js/notes.js` contained 5,471 lines. It mixed mutable Notes
data, view preferences, selection, undo, CRUD, date calculations, reminders,
browser persistence, editor autosave, rendering, canvas tools and window management.
Its import evaluated storage preferences and scheduled reminder initialization.
| Original responsibility | Maintained owner |
| --- | --- |
| Completion, tags, recurrence, date labels and serialization | `domain/recurrence.js`, `domain/noteRules.js` |
| Notes data and selection/filter/editing state | `application/state.js`, instantiated once in `index.js` |
| CRUD, undo and fetch completion ordering | `application/workflows.js`, `cardOperations.js`, `deletion.js` |
| Checklist mutations and rollback | `application/checklistOperations.js` |
| Editor optimistic submission and recovery | `application/submission.js` |
| Serialized autosave and synchronous backup | `application/drafts.js`; form collection in `ui/draftEditor.js` |
| Filtering, searching and ordering | `application/queries.js`; controls in `ui/filters.js` and `toolbar.js` |
| Selected-note attachment and bulk requests | `application/selection.js`; controls in `ui/selection.js` |
| Existing agent-solve workflow | `application/agentActions.js`, through supplied HTTP/session ports |
| HTTP, persistence and browser notifications | `infrastructure/http.js`, `storage.js`, `notifications.js` |
| Background scheduler and delivery decisions | `application/reminders.js` |
| Panel mounting, closing, keyboard handling and navigation | `ui/panel.js` |
| Docking, fullscreen bounds and stacking | `ui/windowLayout.js`, using existing shared capabilities |
| Cards, card gestures, quick add and masonry | `ui/cardRendering.js`, `cardInteractions.js`, `cardChecklist.js`, `cardReorder.js`, `quickAdd.js`, `layout.js` |
| Editor types, colors, reminders and checklists | `ui/editor.js`, `colors.js`, `reminderPicker.js`, `checklist.js` |
| Drawing, local drawing undo and PNG upload | `ui/drawing.js` |
| Mobile reader/editor and reorder gestures | `ui/mobileEditor.js`, `mobileReorder.js`, `checklistReorder.js` |
| Clipboard, safe rendering and reminder display | `ui/clipboard.js`, `text.js`, `reminderDisplay.js` |
| Owned document/window listener disposal | `ui/lifecycle.js`, with panel and observer disposal callbacks |
| Composition and supported exports | `index.js`; five-line legacy facade at `static/js/notes.js` |
The feature has 41 maintained modules: two domain, eleven application, three
infrastructure, twenty-four UI and one index. Source is counted once, excluding
byte-identical deployment copies. Domain has 356 lines, application 826,
infrastructure 112, UI 4,628, and composition 518: 6,440 maintained feature lines.
The module inventory records final per-file counts. The legacy entry shrank
from 5,471 to five lines. Interfaces and explicit composition increase the total
source size; reducing total lines was not the architectural objective.
One 28-line stateless confetti renderer also moved to `shared/ui/confetti.js`.
Notes and Compare both use it. Compare retains its existing `spawnConfetti` export,
which is the same function as the shared export. This removes Notes' import of
Compare's private voting implementation without migrating Compare itself.
## Dependency graph and composition
```mermaid
flowchart TD
Legacy[Legacy Notes URL and window bridge] --> Index[Notes index: one composition]
Index --> UI[Notes UI factories]
Index --> App[Application workflows and state owner]
Index --> Ports[HTTP, persistence and notification adapters]
UI --> Rules[Pure domain rules]
App --> Rules
Index --> Shared[Existing shared UI capabilities and maintained confetti]
```
Application modules receive adapters and feedback operations as arguments. They
do not import browser infrastructure or DOM views. Pure domain modules receive
the clock at the original read sites. UI consumes a read-only state view and
application operations. Composition supplies callbacks between views and workflows;
modules do not import the coordinator back. F1 rejects maintained-source cycles.
There are nine exact legacy-import exceptions, all on the Notes index. They bridge
the existing UI, modal, color-picker, drag, tile, dock, stacking, Escape and tour-hint
capabilities at their existing URLs. Their removal condition is migration of the
corresponding shared responsibility to maintained source with its identity and
lifecycle contract verified. No new feature imports another feature's private internals.
## State and asynchronous ownership
`createNotesState` is called once. It owns the Notes collection, selection IDs,
editing ID, search, filters, archive/view flags, loading and dismissed-reminder time.
Neither the facade nor reminders create a second collection. The public feature API
does not expose this store.
Internal views are read-only live views rather than detached snapshots. This choice
preserves captured note identity: an existing asynchronous rollback still acts on
the same captured note. Array/object writes and selection mutations must use the
state owner's operations. Workflow-local undo queues and serialized draft promises,
and UI-local canvas undo/gesture state, are not competing Notes stores.
Fetches retain the original last-completion behavior. An earlier request that
finishes later can overwrite a later request's result. Tests pin this behavior;
generation guards would be a separate behavioral change. Submission also retains
the original first-temporary-note replacement rule. Multiple simultaneous creations
need a separately reviewed correlation fix, not a silent change during extraction.
Archive, unarchive, pin, item edit/remove/add, delete and undo preserve their original
optimistic ordering and rollback. Animation callbacks remain view-owned. Drawing
save keeps the existing button guard around its asynchronous PNG upload. Draft writes
are serialized per editor; a completed older save cannot clear a newer local draft.
Reminder scans still use the originally loaded Notes collection, not a second
all-active cache. An archived-only collection or a failure-cleared list can defer
active reminders until an active refresh/reopen. The timer remains alive when the
panel closes, but extraction does not redesign this inherited data-availability
limitation. Review such a change separately from scheduler/state ownership.
## Public compatibility
The import URL remains `/static/js/notes.js?v=20260911notesselectioncancel1`.
App, Chat stream updates, Chat note links and Documents continue using their existing
imports. Calendar and Documents retain their window bridge calls. HTML and app
bootstrap code are unchanged. The service worker retains the exact legacy entry.
The legacy namespace still exports `openPanel`, `closePanel`, `togglePanel`,
`isPanelOpen`, `openNote`, `newNote`, `refreshDueBadge`, `dismissFiredReminderDot`,
`openNotes`, `closeNotes`, `isNotesOpen`, and `default`.
The default object's ten methods are unchanged; `dismissFiredReminderDot` remains
named-only. Aliases refer to the same functions, including those on the default
object. `openNote` and `refreshDueBadge` remain async; function arities are pinned.
`window.notesModule` is exactly the default controller. The canonical unversioned
feature index and both tested legacy entry URLs share the same controller and
operations. The default object retains its existing mutability. A different legacy
URL has its own ESM namespace, as native ESM requires, but creates no second store
or scheduler because it forwards to the same canonical index.
The canonical index additionally exports `startNotesBackground` for explicit
activation. The compatibility facade invokes it once through an idempotent start
operation. Existing consumers need no change. Remove the facade only after every
consumer, global bridge, bootstrap and offline reference has migrated together.
## Protected selected Notes/Todos integration
The sender dispatches `odysseus:notes-context-selected` on `window` with
`{ noteIds, accepted: false }`. Dispatch and acknowledgement remain synchronous.
Chat owns acceptance, its session/pending selection, the context indicator and
the `selected_note_ids` send payload. Notes exits selection only after acceptance.
Rejected selections stay selected and display the existing feedback.
No Chat internals were imported or changed. Existing session lifecycle events still
transfer a pending selection on materialization, restore selections on chat switches,
and reset them for a new chat. Backend ID validation and owner filtering remain
authoritative. Unit/backend tests cover these contracts; the real authenticated
browser probe verifies rejection, pending-chat acceptance, the actual indicator and
new-chat reset. Live model execution is not part of that probe.
## Persistence and HTTP contracts
| Key | Preserved format |
| --- | --- |
| `odysseus-notes-view` | `list` or `grid` |
| `odysseus-notes-labels-collapsed` | `1` or `0` |
| `odysseus-notes-first-open-hint-v1` | Existing first-open marker |
| `odysseus-notes-reminder-fired` | JSON array of IDs |
| `odysseus-notes-reminder-glowed` | JSON array of IDs |
| `odysseus-notes-reminder-pending-highlight` | JSON array of IDs |
| `odysseus-notes-reminder-active-highlight` | JSON array of IDs |
| `odysseus-notes-reminder-dismissed-at` | Decimal millisecond timestamp |
| `odysseus-note-draft-<id>` and `odysseus-note-draft-__new__` | Existing JSON draft fields and `_ts` |
CRUD, reorder, upload, reminder firing, default-chat lookup, session creation and
chat-stream requests keep their URLs, methods, credentials, payloads and error
interpretation. Uploads retain `files` and optional `gallery_id` multipart fields.
The agent-solve stream still disables `manage_notes` and drains without switching
the user's chat. No backend implementation changed.
Storage backup failures still warn once; failed network saves keep recoverable
drafts, including intentionally empty text. Draft JSON does not serialize a canvas
bitmap. A drawing remains ephemeral until its explicit PNG upload, as before.
## Lifecycle scopes and explicit corrections
| Scope | Owner and disposal |
| --- | --- |
| Browser page | One reminder startup timeout and interval; page termination ends them. The reminder factory also has explicit disposal. |
| Open panel | Capture/bubble keyboard handlers, reorder listeners and masonry observer; both close paths dispose them. |
| Editor/canvas | Local DOM handlers and draft workflow; drawing's window listener is released when its canvas detaches or the panel closes. |
| Mobile editor/gesture | Overlay and document touch handlers; owner replacement and panel disposal remove handlers and reset registration flags. |
| Menus/hints | Notes portals close with the panel; shared dismiss functions unregister their Escape/tour lifetimes. |
| File picker | Existing completion or 30-second cleanup owns the temporary input. |
Two implementation corrections follow the user's explicit lifecycle requirements.
The original close paths canceled the supposedly background reminder interval.
The new page-owned interval survives closing/minimizing Notes, while retaining the
3-second startup delay, 30-second period and immediate first check. Closing a panel
also releases owned observers/listeners, pending hold timers, active Notes gestures
and portals; late panel fetch callbacks do
not recreate UI on a detached panel. Network completion/state ordering is preserved.
Shared drag/window utilities continue owning their own active-gesture handlers.
They were not moved into Notes. Idle open/close and editor replacement are verified;
every possible interleaving with an active shared window drag was not tested.
## Source and deployment
Every maintained source is registered in `frontend/modules.json` and copied without
rewriting imports. Notes sources map to `static/js/features/notes/` at the same
relative paths. Shared confetti maps to `static/js/shared/ui/confetti.js`.
Repository-owned LF attributes and byte parity cover all 43 maintained modules,
including the unchanged F1 proof. The legacy facade is an authored compatibility
entry, not a second implementation.
All new outputs join exact service-worker precache arrays. A real offline test also
found eight previously missing exact legacy URLs in the Notes graph; those entries
are now included. The eager Notes graph has 57 exact runtime URLs. Cache naming
follows the repository's `odysseus-v659-notes-reference` convention. Root offline
reload remains outside certification because the existing worker scope is `/static/`.
Python startup and Docker serve the committed static graph without maintained
source or frontend tooling. The publisher is a development check/copy operation,
not a runtime compiler or an atomic deployment system.
## Checkpoints, rollback and lessons
[Checkpoint receipts](notes-validation.md#internal-checkpoints) record the actual
checks and local snapshot hashes. These intermediate snapshots are engineering
evidence, not standalone release artifacts. N5 includes bounded editor, cards,
checklist, drawing, mobile, toolbar and window reviews on the same branch.
The N3 mechanical state rewrite initially confused canvas-local and feature undo
bindings. The N5 drawing test found it; the canvas binding was restored and the
same drawing/undo interaction was replayed against the original implementation.
Use scope-aware transformations and test each stateful UI responsibility before
accepting a broad rewrite. Source parsing alone does not prove semantic equivalence.
For rollback, reconstruct the prior complete artifact from its revision/snapshot
in a fresh temporary directory, run parity/graph/behavior checks there, then publish
the matching static graph and service worker as a unit through the authorized
release process. Restore the whole facade/source/output/manifest set, not just
`notes.js`. No schema or storage-key migration needs reversal. Do not reset a dirty
worktree or delete unrelated outputs. The validation report gives a non-destructive
snapshot reproduction recipe.
For the next maintainer:
1. Start with the module inventory and the state/workflow contracts, then reproduce
the focused tests, offline test and isolated native browser probe.
2. Review the explicit timer/lifecycle corrections separately from unchanged
business rules. Inspect both maintained source and byte-identical deployment.
3. Resolve or track the reproduced upstream CI failures and the open PR overlaps
before authorizing publication. Create/link the dedicated issue, then publish
exactly one Notes PR targeting official `dev`.
4. Record the integrated revision and independent recipient reproduction to complete
H. The designated maintainer can then apply this method to future features.
For Calendar, Documents, Email and later monoliths, characterize actual exports,
URL identities, state writers, clock reads, I/O and lifecycle first. Extract pure
rules, then adapters, then a single state owner/workflows. Use explicit callback
ports to break coordinator cycles. Split UI by interactions and resource lifetime,
not by arbitrary line ranges. Transfer source/output ownership and offline URLs
with each boundary, and preserve source-sensitive assertions at their new owner.
Those feature migrations are not part of this implementation.