33 — Notes & Annotations (envisioning)
Envisioning doc — needs confirmation. Governed by
00-ROADMAP.md. Created 2026-06-13 (owner-raised). The owner wants the Track Notes field to become real: many notes, each with who wrote it and when, surfaced in other parts of the hierarchy (by tag), and — maybe — a status, "if it makes sense with our approach." Key finding: the rich entity the owner is describing already exists — it'sComment. This doc grounds Notes as node-anchored Comments, not a new entity. Backend v1 BUILT 2026-06-20 (see §Status). Decisions at the end.
Status — backend v1 shipped 2026-06-20 (Claude Code;
nest buildclean,jest14/14, boot-verified). Notes are now NOTE-kindComments, purely additive (no client breakage):
CommentType.NOTE+Comment.artistSpaceanchor (notes can live at any level: artist / project / track / version) +Comment.tags[]/shareTags(owner-private, opt-in — privacy enforced for free by the genericStripPrivateTagsInterceptor, which already duck-types{owner, tags, shareTags}).POST /comment/note(CreateNoteDto: content + one anchor id + tags + shareTags) — track-tolerant (doesn't assume a track like the timeline-comment path).GET /comment/notes?artistId|projectId|trackId|fileIdlists a node's notes (the Portal's "Anchored here" band).- Confirmed decision 1 (explicit NOTE kind ✅) and decision 3 (per-resource, owner-private, opt-in tags ✅ — same flag as node tags). ⏳ Remaining: the iOS Portal Notes section (list/create, tag chips, resolve toggle); the eagle-level roll-up (decision 6 — a subtree comment-aggregate read, the heaviest piece); decision 2 (rename
Track.notes→Track.description, deferred — touches clients, needs coordination); and note notifications + live-emit to the anchor room (doc-35), opt-in approval elevation (decision 4), and pinning (decision 5) — all flagged follow-ups.
What triggered this
"We have a notes section. Would be nice that these notes can be selected to appear, probably by a tag or something, in other parts of the hierarchy, and have many notes related, by the person who added the note, date/time, and not probably a status if it makes sense with our approach and envision."
Today Track.notes is a single free-text string (one box, no author, no time, no thread). The owner wants the layer every collaboration tool grows: authored, timestamped, threaded annotations that can be filed and found across the catalog.
The finding: this is Comment, minus the timeline
Verified against the schema. Comment is already polymorphic and already most of what's being asked for:
CommentType = TIMESTAMP | SECTION | VERSION | PROJECT ← already non-timeline-only
owner (User) · content · createdAt/updatedAt (timestamps) ← author + date/time, already there
isResolved (bool) · likes[] · responses[] (threaded) ← resolve + threading, already there
refs: file · track · project · section ← already anchors to multiple node types
timeStart / timeEnd (optional) ← only set for audio-anchored feedbackSo a "Note" is a Comment without a timeline anchor — type ∈ {PROJECT, VERSION, … and a new NOTE}, no timeStart. The owner's whole list — many notes · author · date/time · threaded · resolvable — is satisfied by the existing entity. Track.notes (the single string) is the only un-modeled piece, and it's a degenerate special case of this.
This is the grounded, no-rebuild answer (consistent with the "don't rebuild; one graph, many lenses" discipline of docs 26/28/32): we don't add a Note collection — we generalize Comment into the universal annotation surface and treat audio feedback as the timeline-anchored flavor of it.
The model
A Note is a Comment anchored to a node instead of a moment. Same entity, same author/time/thread/resolve; the anchor is
(nodeType, nodeId)with notimeStart. The timeline comment (the review loop) is just the flavor that also carriestimeStart/section.
What we add to Comment to fully serve the ask:
- A
NOTEkind (or reusePROJECT/VERSION+ "no timeStart" — decision below). Lets the UI separate editorial notes from time-synced feedback without splitting the data. - An
ArtistSpaceanchor.Commentanchors to file/track/project/section today; addartistSpaceso notes can live at the top of the hierarchy (an artist-level note: "always credit co-writer X"). tags: string[]— the cross-hierarchy surfacing the owner asked for. A note tagged#mixingor#legalappears wherever that tag's lens is active (doc 26 Layer 2 #Tags) — surfaced, never copied. This is the "selected to appear in other parts of the hierarchy" mechanism, and it's the same tag facet the whole nav model already uses. No new "make this note appear elsewhere" machinery.- Status — optional, and not the default (the owner's instinct is right). Two honest levels:
- Resolve (already exists:
isResolved) — open / done. Enough for most notes; zero new model. - Approval dimension (doc 32) — only when a note is genuinely a sign-off ("mastering notes — approved by the ME"). Then it carries the same
{ status, by, at }primitive from32-APPROVAL-MODEL.md, and the note is the approvable artifact for that decision. Recommendation: notes are informational by default (resolve only); elevation to an approval is opt-in, not a field every note shows. Conflating "a note" with "a verdict" would make the approval model lie — the doc-29/32 invariant.
- Resolve (already exists:
How "appears in other parts of the hierarchy" works (three honest mechanisms)
| Mechanism | What it does | Cost |
|---|---|---|
| Anchor + cascade | a Project note is visible when browsing that Project (and its children, read-down) | already implied by the hierarchy |
| Tags + lens (primary) | #tag a note → it surfaces in any tag-lens across the catalog, regardless of where it's anchored | tags[] on Comment + the doc-26 tag index |
| Pin / elevate (optional) | explicitly surface a child note on a parent node ("pin to Project") | a small pinnedTo[] — only if tags prove too implicit |
Recommendation: tags are the mechanism (it's exactly what the owner said — "by a tag or something"), with anchor-cascade for free. Pinning is a fallback we add only if users want a deterministic "show this note up there" without a tag.
Eagle-level view — notes & comments roll up the hierarchy (owner-raised 2026-06-13)
The owner expected to see comments in Portals and they're missing — and the deeper ask: the relevant ones should be visible at every level — artist, project, track, version, and roster — as an "eagle-level" view, not buried on a single track's timeline. This is the natural consequence of "Notes are Comments": once every annotation is one entity anchored to a node, a Portal can show two bands:
- Anchored here — notes/comments whose anchor is this node (the Portal's own thread).
- Rolled up from below — a read-down aggregate of child annotations, filtered to what matters at this altitude (default: unresolved + tagged/flagged), so the higher you fly the more it summarizes:
- Version (File) → its own time-synced comments. (Owner: "with the comments we already have is enough" — no roll-up needed here; this is the leaf.)
- Track → its versions' comments + track-level notes.
- Project → notable notes/comments across its tracks (e.g. unresolved,
#-flagged, or status-bearing). - Artist (ArtistSpace) → a digest across the artist's projects.
- Roster → cross-artist digest — the manager's true eagle view ("what's open across my Pop roster").
Mechanism = the same anchor + tags + lens (no new surfacing engine): the roll-up is a read that gathers Comments whose anchor is in the subtree, then the Status / #Tags / Activity lenses (doc 26 Layer 2) decide which surface at this level. Higher levels show less, louder — a digest, not a firehose (brand pillar: professional calm, doc 31). The owner's "selected ones / the ones that apply" = exactly this filter (unresolved · tagged · status-bearing), tunable per level.
Why comments "disappeared" from Portals: the Portal today renders metadata/share/team but no annotation band — comments still live only in the Wave timeline. The fix is this section: give the Portal an Anchored-here + Rolled-up notes/comments band. (Backend: a subtree comment-aggregate read; see the map. iOS: a Portal Notes/Comments section. Both ride the Portals-everywhere phase.)
Surfacing UX — badge + band + the actor lens (owner-raised 2026-06-20)
The owner asked how the rolled-up signal should appear ("notification / bubble / label … same place or not?"). Grounded in the actors (12), use-cases, and professional-calm brand (31), the answer is two complementary surfaces, not one:
- Ambient count badge on cards/rows (the scan signal). A small count/dot on the Artist tile · Project card · Track row · Roster header = "N items below need attention." This is the owner's "bubble / label" — low-chrome, readable in a grid at a glance, the manager's "where is something open across my catalog?" without opening anything. Tapping drills to the node.
- The roll-up band inside the Portal (the detail). On opening the node, a Rolled-up band lists the filtered child items, each linking to its anchor (tap a feedback item → jump to that track's Wave at that timecode; tap a note → its node). This is the Anchored-here band's sibling.
Recommend BOTH: the badge scans in lists (grid altitude), the band reads on open (node altitude). Same data, two altitudes — consistent with "one graph, many lenses."
One unified roll-up, visually distinguished — not two separate feeds. (Answers "same place or not?") Notes and comments are one entity (this doc), so they roll up into one band / one badge count, with each row carrying a kind chip (📝 note vs 💬 feedback) + status (unresolved / approved) + tag chips. Splitting them into two parallel roll-ups would fracture the one-graph model and double the chrome. Same place; tell them apart by kind, not by feed.
The badge counts the digest, never the firehose. Default filter = unresolved + tagged + status-bearing (open feedback · #-flagged notes · sign-off-pending), and the higher you fly the tighter it gets (Roster ≈ status-bearing + #-flagged only). A raw count of every annotation at the artist level is noise — the opposite of professional calm.
The filter is the actor's lens (doc 26 Layer 2), so one surface serves every actor: Manager / A&R → cross-roster unresolved + status; Artist → awaiting my approval; Legal → #legal; Producer → needs- revision on what I delivered. No per-actor screens — the same badge/band, a different default lens.
Roll-up ≠ notification (keep them distinct, paired). A notification (push / SSE, the notification module + doc-35 live events) fires on the event — "X left a comment." The badge/band shows the standing state — "N open." One is a moment, the other is a status; they share the annotation store but are different surfaces. Don't conflate a count bubble with a push.
Backend shape — make the subtree query cheap (sharpens decision 6). A subtree aggregate read, e.g. GET /comment/rollup?{projectId|artistId|rosterId}=… → { counts: {total, unresolved, tagged, statusBearing}, items: [capped, priority-ordered] }. To keep it a single indexed find (not an N+1 over children), denormalize ancestor refs on every Comment/note at create time — store project + artistSpace alongside the existing track/file. Then: project roll-up = find({project}), artist roll-up = find({artistSpace}), roster roll-up = find({artistSpace: {$in: rosterArtistIds}}), each + filter. This is correct across not-yet-loaded children (the roster view spans many artists the client never loaded) — so a backend aggregate beats a client gather. Needs: set the denorm on create + a one-off backfill for existing comments (derive project/artistSpace from their track/file).
Portal annotations — ONE consolidated feed (owner-raised 2026-06-21)
The first cut shipped two separate bands (a "NOTES" band of anchored-here items + a roll-up band below). The owner: centralize them into one area, with this node's own annotations tagged and on top, and a re-orderable order with a sensible default. Confirmed model — one "Activity" section per Portal:
- Header =
ACTIVITY(not "Notes" — it holds notes and feedback, one entity) + the compose+. - Compose on top (creating a note here always anchors to this node).
- One feed, two zones, visually distinct:
- Here — annotations whose anchor is this node, each with a small
Herechip. Pinned on top. - From below — the rolled-up digest, each with its provenance breadcrumb (roster › artist › project › track › V#). Underneath. So every row answers "is this about this thing, or something inside it?" by chip-vs-breadcrumb — the same kind-chip language (📝/💬) rides alongside.
- Here — annotations whose anchor is this node, each with a small
- Default order (the owner's "you tell"): Here-zone first, then From-below; within each zone, unresolved first, then newest. Rationale: your own node's items lead (relevance), and needs-attention (unresolved) surfaces above settled chatter — professional calm, the digest leads with what's open.
- Re-orderable (follow-on): a small sort control —
Here-first(default) ·Newest·Unresolved— so power users (a manager scanning open items) can flip to a pure status or chronological view. Persist the choice per user (like the doc-26 saved-view lens). Ship the default first; the control is a cheap follow-up.
No backend change —
/comment/notes(Here) and/comment/rollup(below) already return the two zones with identical shapes. This is an iOS IA/layout slice: merge the two bands under oneACTIVITYheader, add theHerechip, apply the default sort. (The sort control is the only net-new bit, deferrable.)
Notes vs Comments — keep them one entity, two flavors
| Note (editorial) | Comment (feedback) | |
|---|---|---|
| Anchor | a node (artist/project/track/version) | a moment (timeStart/section on a version) |
| Lives in | the Portal (doc 26 Layer 4) | the Wave timeline |
| Typical author | manager, A&R, legal, the owner | reviewer listening to a take |
| Surfacing | tags + lenses across hierarchy | the track's comment thread |
| Same underlying entity? | yes — Comment, type=NOTE, no timeStart | Comment, type=TIMESTAMP/SECTION |
One entity means one threading model, one notification path, one resolve mechanic, one tags index — and the Portal's "Notes" section and the Wave's "Comments" are just two lenses over the same store. (Same philosophy as doc 26: one graph, many lenses.)
Backend map (reuse vs net-new)
| Capability | Today | Net-new |
|---|---|---|
| Authored, timestamped, threaded annotation | ✅ Comment (owner, timestamps, responses) | none |
| Anchor to project/version | ✅ CommentType.PROJECT/VERSION + refs | none |
| Anchor to ArtistSpace | ❌ | artistSpace ref on Comment |
| Note vs feedback distinction | 🟡 inferable (no timeStart) | a NOTE kind (or a kind discriminator) |
| Cross-hierarchy by tag | 🟡 node tags ✅ shipped 2026-06-18 (tags[] on Artist/Project/Track + GET /search/tags + read-privacy interceptor, doc 26) | still net-new: tags: string[] on Comment so notes surface in the tag lens |
| Resolve / done | ✅ isResolved | none |
| Optional approval on a note | ❌ | reuse doc-32 {status,by,at} — opt-in, only for sign-off notes |
Rename Track.notes → Track.description | 🟡 one freeform field | rename prop + DTO/clients + additive copy migration (it's a distinct concept from NOTE-kind notes — confirmed) |
| Tag sharing (opt-in) | ✅ shipped 2026-06-18 (node level) — shareTags flag per resource; tags[] owner-private by default, exposed to recipients only when opted in | extend the same flag to Comment tags when notes land |
| Read endpoints per node | 🟡 comment reads are track/file-centric | GET …/:node/notes (filter Comment by anchor + NOTE kind) |
| Eagle-level roll-up (Portal sees child annotations) | ❌ | a subtree aggregate read — gather Comments anchored anywhere in a node's subtree, filtered (unresolved · tagged · status); levels: track / project / artist / roster |
Cheap early taste (optional, like doc 28's variantType): add tags[] to Comment now — it's a single field that simultaneously serves notes and the doc-26 tag lens, with no UI commitment. Flagged, not pulled.
How this sequences
Like docs 26/28/32: envision now, build later, after the doc-26 Shared waves, interleaved by feedback. Notes pair naturally with Portals-everywhere (doc 26 Layer 4) — the Portal is where a node's notes list lives — so the build rides the Portal phase rather than standing alone. Backend-contract-first when built (the Comment extensions — Claude Code) then iOS (Portal Notes section, tag chips, optional resolve).
Decisions
Confirmed (owner 2026-06-14)
- Explicit
NOTEkind. Add aNOTEvalue toCommentType(not "infer from notimeStart") — clearer queries and UI separation between editorial notes and timeline feedback. - Rename
Track.notes→Track.description(a plain free-text field; pickdescriptionor a close synonym). It is a different concept from the new NOTE-kind annotations, so it gets a distinct name to avoid confusion. The threaded, authored notes are NOTE-kindComments — not the description string. Backend: rename theTrackprop (+ any DTO/clients); a tiny additive migration (copynotes→description). - Tags are per-resource + sharing is opt-in. Tags (
tags[]) are authored on the node/note and are the owner's own organization — private by default. They travel to a recipient only when explicitly shared: a "share tags" check at share time (and/or they ride along when a whole workspace is shared), else none. (This is the owner-authoredtags[]layer — distinct from the already-built doc-26 recipient-sidemy-tag, which is always private to the recipient.) Resolves the doc-26 "tags scope" question for both docs: per-resource, owner-private, opt-in propagation — not a global vocabulary.
Still open (decide at build time)
- Status on notes — resolve-only (recommend) vs allow opt-in doc-32 approval elevation for sign-off notes. Never a default status field on every note.
- Pin/elevate — ship tags only (recommend), or also a
pinnedTo[]for deterministic parent surfacing? - Eagle-level roll-up — ✅ backend SHIPPED 2026-06-20 (denorm
track/project/artistSpaceon every Comment +GET /comment/rollup+ batch/comment/rollup/counts+ backfill; boot-verified). iOS badge+band is queued. Confirmed model below (expanded after the owner's 2026-06-20 surfacing ask — see §Surfacing UX):- 6a · Surfacing form — recommend both an ambient count badge on cards/rows (grid scan) and a roll-up band in the Portal (detail on open). (vs band-only / badge-only.)
- 6b · Filter default — recommend unresolved + tagged + status-bearing, "less but louder" the higher you fly, and = the viewer's actor lens (doc-26 Layer 2) so one surface serves every actor.
- 6c · Backend — recommend a subtree aggregate read + denormalize
project/artistSpaceon every Comment (create-time set + one-off backfill) so the query is a single indexed find that spans not-yet-loaded children. (Backend aggregate over client gather.) - 6d · Notes + comments — recommend one unified roll-up, distinguished by a kind chip (note vs feedback), not two separate feeds.
Until built, Track.notes/description stays the single field; nothing here blocks the doc-26 waves or doc-32.
Cross-references
- The entity this reuses:
Comment(comment.schema.ts—CommentType,owner,responses,isResolved) - Tags / lenses / Portals (the surfacing mechanism):
26-IDENTITY-SHARING-NAVIGATION.md(Layers 2 & 4) - Optional status on a note = the approval primitive:
32-APPROVAL-MODEL.md - Actors who write notes (manager / A&R / legal):
12-ACTORS-WORKFLOWS.md - Metadata/notes backlog origin:
24-UX-BACKLOG.md