prism branch T-1864/bugfix-sidebar-resolved-document-notes commits 1 files 4 touched (2 production, 1 test, 1 report) lines +243 / -3 (production: +18 / -3)

Pre-push review: T-1864/bugfix-sidebar-resolved-document-notes

PR #406 — Fix T-1864: Resolved document notes disappear from the regular sidebar. One commit over origin/main (2b10c6dc). Read-only review: nothing was edited; every finding below is a recommendation for the author.

At a glance

  • Root cause: one dictionary (buildCombinedBlockNotes, sentinel stripped) fed two consumers with different needs — structural grouping (must exclude the sentinel) and the flat Resolved list (must not). A resolved document-level note therefore had no home in the regular sidebar.
  • Fix shape: extract the predicate into NoteGrouping.excludingDocumentSentinel(_:); build the full dictionary once per render, filter a copy for groupByStructure, pass the full set to resolvedNotes. buildCombinedBlockNotes now delegates to the helper and keeps its one remaining caller (NotesPanel.activeNotesSection) unchanged.
  • Behaviour parity: Req 2.8 (resolved document notes in the Resolved section) now holds on iPad/macOS as it already did on iPhone; Req 2.9 (active-only Document Notes section) is untouched — documentLevelNotes still filters .active.
  • Verification: make lint clean; verify-test-isolation OK (83 static checks); targeted macOS run of SidebarNotesViewTests, NoteGroupingTests, NotesPanelDocumentNotesTests, NotesManagerDocumentLevelTests, ImportedDocumentLevelNotesTests, ListBlockLevelNotesTests: 79/79 passed. The full suite was not run (other xcodebuild jobs were active on the machine; the report is honest about the same limitation).
  • Recommendations (none blocking): make the regression test call production code rather than mirror it; add the [Unreleased] › Fixed CHANGELOG entry; refresh two stale comments (SidebarNotesView.swift:62-63, NoteGrouping.swift:112/153); consider a follow-up ticket so NotesPanel.resolvedNotesSection and the sidebar share one Resolved computation (they still differ on ordering and on resolved orphans).

Verdict

Ready to push

The fix is correct and minimal: SidebarNotesView.body now feeds the sentinel-inclusive dictionary into NoteGrouping.resolvedNotes while structural grouping keeps the sentinel-free one, which is exactly what global-notes Req 2.8 asks for and what the compact NotesPanel already did. Edge cases were traced and hold: a document whose only note is a resolved document-level note still renders the list (anchoredNotes keeps the sentinel bucket regardless of status, so hasDisplayableNotes is true), imported comment-block document notes flow through the same path, and the sidebar's resolved rows attach no navigation so nothing scrolls to __document__. SwiftLint is clean and the six notes-related suites pass 79/79 on macOS, including the two new tests. Nothing blocks the push. Two things are worth doing before or shortly after merge: the regression test re-implements body's three lines instead of exercising production code, so reverting the view would leave it green; and the branch has no CHANGELOG.md entry where every recent Fix T-… commit added one.

Review findings

8 raised · 0 fixed · 8 skipped

Jump to findings →

Tests

Pass rate: 100% (79 of 79)

New tests: 2

Diff coverage: n/a

Jump to tests →

Commits

Three-level explanation

What changed

Prism lets you attach notes to a document. Most notes point at a specific paragraph or heading, but a document-level note is about the file as a whole and points at nothing in particular. Internally that kind of note is filed under a placeholder address, __document__, instead of a real block.

On iPad and Mac the notes list lives in a sidebar. It has two kinds of area: sections that follow the document's headings ("notes under Introduction, notes under Setup…") and a flat, collapsed Resolved list for notes you have ticked off. The code that built the sidebar prepared one list of notes with the placeholder-address notes removed — sensible for the heading sections, because a note with no position cannot be filed under a heading — and then reused that same trimmed list for the Resolved area too. So when you resolved a document-level note, it vanished: not active any more, so gone from "Document Notes"; already removed from the list the Resolved area was built from, so not there either.

The fix builds the full list once, trims a copy for the heading sections, and gives the untrimmed list to the Resolved area.

Why it matters

A resolved note is supposed to be recoverable — you can reopen it or delete it from the Resolved area. Losing it from view on iPad/Mac (but not iPhone, which had its own correct code) meant users could not see or manage those notes from that screen. Nothing was deleted from storage; it just could not be reached.

Key concepts

  • Sentinel value: a special stand-in value (__document__) used where a real value (a block id) is expected, to mean "none in particular".
  • Structural vs flat grouping: structural means arranged by the document's headings; flat means a simple list. The sentinel only causes trouble for the structural kind.
  • Regression test: a test added with a bug fix so the same bug cannot quietly come back.

Changes overview

  • prism/Models/NoteGrouping.swift: new static func excludingDocumentSentinel(_:) that filters the BlockNote.documentSentinelId key out of a [String: [BlockNote]]. buildCombinedBlockNotes(from:) now delegates to it instead of inlining the same predicate.
  • prism/Views/SidebarNotesView.swift: body computes allCombinedNotes = buildCombinedNotes(from:), derives combinedNotes = excludingDocumentSentinel(allCombinedNotes) for groupByStructure, and passes allCombinedNotes to resolvedNotes(from:).
  • prismTests/SidebarNotesViewTests.swift: two tests — one loads a resolved document-level note through a real NotesManager and asserts it appears in the resolved set while being absent from the structural set; one pins the helper's contract.
  • specs/bugfixes/sidebar-resolved-document-notes/report.md: the fix-bug report (root cause, alternatives, verification).

Implementation approach

The sentinel bucket is populated regardless of status (NotesManager.swift:433 stores every document-level note under the sentinel; rebuildAnchoredNotes re-buckets by blockId with no status filter), so hasDisplayableNotes stays true and the list renders even when the only note is a resolved document note. Imported comment-block document notes anchor to the same sentinel (CommentBlockExtractor) and are merged by allNotes(for:), so they follow the same path. The sidebar's resolved NoteRow has no onTapGesture, so no navigation is attempted against __document__.

The change follows the compact NotesPanel.resolvedNotesSection, which never filtered the sentinel and therefore never had the bug.

Trade-offs

  • Filter a copy vs. rely on groupByStructure's own guard: groupByStructure already skips the sentinel in its unplaced sweep (NoteGrouping.swift:153), so passing the full dictionary would be behaviourally identical and avoid a dictionary copy. The author kept the explicit filter, which keeps the guard "defensive" rather than load-bearing. The copy is sized by blocks-that-have-notes and copies array references only — negligible.
  • Special-casing inside resolvedNotes (rejected in the report): would make a structure-agnostic filter sentinel-aware for every caller.
  • Separate resolvedDocumentLevelNotes accessor merged in the view (rejected): a second place to keep in sync.

Technical deep dive

The defect class is over-filtering a shared input: buildCombinedBlockNotes was written for groupByStructure and reused for resolvedNotes under the assumption that "combined notes for the sidebar" is one dictionary. The extraction of excludingDocumentSentinel makes the two consumers' requirements explicit at the call site instead of hiding them behind a builder name. Ordering is unaffected: resolvedNotes re-sorts by createdAt across all buckets, and groupByStructure uses its own thread-aware sort.

Edge cases traced during review: (1) resolved-only document note → anchoredNotes[sentinel] non-empty → hasDisplayableNotes true → list and Resolved disclosure render; (2) imported resolved document note → importedNotes[sentinel] → merged by allNotes(for:); (3) Req 2.9 unchanged — documentLevelNotes still filters .active, covered by NotesPanelDocumentNotesTests.

Architecture impact

Minimal. buildCombinedBlockNotes survives with one production caller (NotesPanel.swift:341) and its existing tests. The two surfaces still compute Resolved differently, and this PR aligns only the sentinel dimension: the panel builds from documentNotes?.notes + importedNotes (pre-relocation, unsorted, includes resolved orphans, navigates on tap with the stored block id — including __document__, a documented no-op), whereas the sidebar builds from anchoredNotes + importedNotes (post-relocation, sorted by createdAt, orphans shown in their own section). That divergence is pre-existing and out of scope here, but it is the same class of drift that produced T-1864; pointing NotesPanel.resolvedNotesSection at NoteGrouping.resolvedNotes(from: buildCombinedNotes(...)) would close it.

Potential issues

  • The regression test mirrors the implementation. resolvedSectionIncludesDocumentLevelNotes re-implements body's three lines rather than calling production code, so reverting SidebarNotesView.body to buildCombinedBlockNotes leaves it green. It pins that the helpers compose correctly, not that the view uses them. Extracting the pair into a testable NoteGrouping function (e.g. returning (grouped, resolved)) that body calls would make the test bite. SidebarNotesView has no SwiftUI-level test harness, so this is the cheapest honest option.
  • Stale comments: SidebarNotesView.swift:62-63 still references groupedNotes/resolvedAnchoredNotes (neither exists); NoteGrouping.swift:153 says the sentinel "should already be filtered by buildCombinedBlockNotes" and :112 says the parameter comes "from buildCombinedNotes" — both are now half-true.
  • No CHANGELOG entry where every recent Fix T-… commit added one under [Unreleased] › Fixed.

Completeness assessment

Fully implemented: the Req 2.8 fix for the regular sidebar; helper extraction; report. Partially implemented: regression coverage (present, but not wired to the view's actual code path). Missing: CHANGELOG entry; the global-notes design (§4c) still describes only NotesPanel's Resolved computation, so the rule the sidebar now follows is not written down anywhere except the code comment.

Important changes — detailed

SidebarNotesView.body: feed the full dictionary to resolvedNotes

prism/Views/SidebarNotesView.swift

Why it matters. This is the user-visible fix. The Resolved disclosure is built from allCombinedNotes (sentinel included) while groupByStructure keeps the filtered copy, so a resolved document-level note reappears on iPad/macOS.

What to look at. prism/Views/SidebarNotesView.swift:61-72

Takeaway. When one derived value feeds two consumers, check whether each consumer's filter justification still applies. A filter that is right for structural grouping (no block to attach to) is wrong for a flat list.
Rationale. Matches NotesPanel's existing behaviour and global-notes Req 2.8; keeps the sentinel out of structural grouping where it has no position. Stated in the commit body and the report's 'Approach rationale'.

NoteGrouping.excludingDocumentSentinel: extract the predicate

prism/Models/NoteGrouping.swift

Why it matters. Makes the sentinel exclusion an explicit, reusable step so a caller can choose where to apply it, instead of only getting it bundled into buildCombinedBlockNotes.

What to look at. prism/Models/NoteGrouping.swift:84-95

Takeaway. Filtering on the dictionary key (documentSentinelId) is the right level here; BlockNote.isDocumentLevel is a per-note property and would need an element peek.
Rationale. The report rejects special-casing inside resolvedNotes (would make a structure-agnostic filter lie for other callers) and a separate accessor merged in the view (second thing to keep in sync).

SidebarNotesViewTests: regression coverage for T-1864

prismTests/SidebarNotesViewTests.swift

Why it matters. Two tests: one drives a real NotesManager with a resolved document note and asserts it is in the resolved set and absent from the structural set; one pins the helper's contract. They pass, but the first mirrors body's computation rather than calling it.

What to look at. prismTests/SidebarNotesViewTests.swift:144-206

Takeaway. A test that re-implements the production lines it protects cannot fail when those lines regress. Extract the computation so the test and the view share it.
Open question. Rationale not stated by the author and not inferable from the diff.

Bugfix report: root cause, alternatives, honest verification

specs/bugfixes/sidebar-resolved-document-notes/report.md

Why it matters. Documents the over-filtering root cause, two rejected alternatives, and a Verification section that explicitly leaves the full suite unchecked with a reason. Format matches sibling reports.

What to look at. specs/bugfixes/sidebar-resolved-document-notes/report.md

Takeaway. The Prevention section's rule — assert sentinel exclusion in both directions (excluded where structural, retained where flat) — is a good addition to any future sentinel test.
Rationale. Standard fix-bug workflow output; verification limited to targeted suites because make test-quick was unreliable under machine contention at the time (this review hit the same contention and did the same).

Key decisions

Extract a helper rather than special-case resolvedNotes.

Keeps resolvedNotes a structure-agnostic filter over whatever dictionary it is given; sentinel awareness lives where the structural constraint actually is. From the report's 'Alternatives considered'.

Reuse the already-built dictionary instead of a parallel resolvedDocumentLevelNotes accessor.

Avoids a second source of truth to keep in sync with NotesManager+DocumentLevel. From the report.

Keep the explicit filtered copy for groupByStructure.

groupByStructure already skips the sentinel defensively in its unplaced sweep, so passing the full dictionary would be equivalent. The author kept the explicit filter so that guard stays defensive rather than load-bearing. The copy is O(blocks-with-notes) and copies array references only.

(inferred — not stated by the author.)
Keep buildCombinedBlockNotes as a named wrapper.

It now has one production caller (NotesPanel.swift:341) and its own tests; leaving it avoids touching the compact panel in a fix scoped to the sidebar.

(inferred — not stated by the author.)

Review findings

SeverityAreaFindingResolution
minorprismTests/SidebarNotesViewTests.swift:154-192 — regression test mirrors implementationresolvedSectionIncludesDocumentLevelNotes re-implements body's three lines (buildCombinedNotes → excludingDocumentSentinel → resolvedNotes) and asserts on its own copy. Reverting SidebarNotesView.body to buildCombinedBlockNotes would leave it green, so it does not guard the regression it is named for. Raised independently by two review agents.Not applied (read-only review). Recommend extracting the (grouped, resolved) computation into a NoteGrouping function that body calls, and asserting against that. Also add #expect(manager.hasDisplayableNotes) and an imported-document-note variant.
minorCHANGELOG.md — no [Unreleased] › Fixed entryAll six most recent 'Fix T-…' commits on origin/main added a user-facing CHANGELOG entry; this branch does not.Not applied (read-only review). Recommend adding a Fixed entry for T-1864 before pushing.
nitprism/Views/SidebarNotesView.swift:62-63 — stale commentThe retained head of the body comment still says 'to avoid redundant recomputation in groupedNotes and resolvedAnchoredNotes'; neither symbol exists (locals are grouped / resolved).Not applied. Rewrite as one paragraph naming grouped/resolved.
nitprism/Models/NoteGrouping.swift:112 and :153 — comments now half-trueLine 153 says the sentinel 'should already be filtered by buildCombinedBlockNotes'; the sidebar now filters via excludingDocumentSentinel. Line 112's parameter doc says the dictionary comes 'from buildCombinedNotes', which is the one caller that must NOT pass it unfiltered.Not applied. Name excludingDocumentSentinel in both comments.
nitprismTests/SidebarNotesViewTests.swift:159-168, 193-206 — test hygieneThe new test hand-rolls a BlockNote although the suite's makeNote(blockId:status:) helper covers it. excludingDocumentSentinelRemovesOnlySentinel is a pure NoteGrouping unit test and sits apart from the existing sentinel suite in NoteGroupingTests.swift:285-345.Not applied. Use makeNote; consider moving the helper test next to its siblings.
minorprism/Views/NotesPanel.swift:391-416 — pre-existing surface divergence (out of scope)NotesPanel builds Resolved from documentNotes?.notes + importedNotes (unsorted, includes resolved orphans, navigates on tap with the stored blockId — including '__document__'); the sidebar builds from anchoredNotes + importedNotes sorted by createdAt with orphans in their own section. This PR aligns only the sentinel dimension.Not applied; not this PR's scope. Suggest a follow-up ticket to point resolvedNotesSection at NoteGrouping.resolvedNotes(from: buildCombinedNotes(...)) so both surfaces share one computation.
nitspecs/global-notes/design.md §4c / §5 — design gapThe design describes only NotesPanel's Resolved computation and tells the sidebar to filter the sentinel from groupByStructure, saying nothing about the sidebar's Resolved list — the exact gap that produced this bug.Not applied. One line in §4c stating the sidebar derives Resolved from the unfiltered dictionary would close it.
noneCorrectness of the fixTraced: resolved-only document note keeps anchoredNotes[sentinel] non-empty so hasDisplayableNotes is true and the list renders; imported resolved document notes flow via importedNotes[sentinel] and allNotes(for:); sidebar resolved rows attach no navigation; Req 2.9 (active-only Document Notes) unchanged and covered by NotesPanelDocumentNotesTests. No duplicate of excludingDocumentSentinel exists elsewhere; other buildCombinedBlockNotes callers are unaffected.Nothing to do.

Tests

Source: local run at 2026-09-06T01:03:28+10:00 · snapshot bebf8a491dcdcbd4804be6c87257c649c39abb78

Baseline: none

Execution: passed (partial results) · JUnit: 1 file · Coverage: none · Baseline: absent

Coverage scope: as the project configures it

Totals: 79 passed · 0 failed · 0 skipped · 0 errored · 0 flaky

New and removed tests

Derived by declaration name, from the diff (no baseline run).

Blast radius

Files that import a changed file on the left, changed files in the centre, files a changed file imports on the right. Snapshot working-tree against base 2b10c6dc4944d8e50c99e6b60e962591a230b39f.

Dependents none found Changed Dependencies edges at package granularity prism/Models prism/Views prismTests …debar-resolved-document-notes prism prism/Models/NoteGrouping.swift…ism/Models/NoteGrouping.swift prism/Views/SidebarNotesView.swift…/Views/SidebarNotesView.swift prismTests/SidebarNotesViewTests.swift…s/SidebarNotesViewTests.swift specs/bugfixes/sidebar-resolved-document-notes/report.md…lved-document-notes/report.md prism/prismApp.swiftprism/prismApp.swift
addedmodifieddeletedrenamedunchangedcollapsed package group or +N more⚑N test files with an edge to the node

Skipped files

Per-file diffs

Click to expand.

prism/Models/NoteGrouping.swift Modified +10 / -1
diff --git a/prism/Models/NoteGrouping.swift b/prism/Models/NoteGrouping.swiftindex c49526f0..4248c3a0 100644--- a/prism/Models/NoteGrouping.swift+++ b/prism/Models/NoteGrouping.swift@@ -82,7 +82,16 @@ enum NoteGrouping {      /// Combines user and imported block notes (excludes document-level sentinel).     static func buildCombinedBlockNotes(from notesManager: NotesManager) -> [String: [BlockNote]] {-        buildCombinedNotes(from: notesManager).filter { $0.key != BlockNote.documentSentinelId }+        excludingDocumentSentinel(buildCombinedNotes(from: notesManager))+    }++    /// Removes the document-level sentinel entry from a combined notes dictionary.+    ///+    /// Structural grouping (`groupByStructure`) has no block to attach the sentinel+    /// to, so it must be excluded there. Flat collections like the Resolved section+    /// are not structural and should keep it — see `resolvedNotes`.+    static func excludingDocumentSentinel(_ combinedNotes: [String: [BlockNote]]) -> [String: [BlockNote]] {+        combinedNotes.filter { $0.key != BlockNote.documentSentinelId }     }      /// Filters resolved notes from the combined dictionary, sorted by creation date.
prism/Views/SidebarNotesView.swift Modified +8 / -2
diff --git a/prism/Views/SidebarNotesView.swift b/prism/Views/SidebarNotesView.swiftindex addcd53f..de941248 100644--- a/prism/Views/SidebarNotesView.swift+++ b/prism/Views/SidebarNotesView.swift@@ -61,9 +61,15 @@ struct SidebarNotesView: View {     var body: some View {         // Compute combined notes once per render to avoid redundant recomputation         // in groupedNotes and resolvedAnchoredNotes.-        let combinedNotes = NoteGrouping.buildCombinedBlockNotes(from: notesManager)+        //+        // Structural grouping excludes the document-level sentinel (it has no+        // block to attach to), but the Resolved section is a flat list, not a+        // structural one, so it must see the full set — otherwise a resolved+        // document-level note has nowhere in the sidebar to live (T-1864).+        let allCombinedNotes = NoteGrouping.buildCombinedNotes(from: notesManager)+        let combinedNotes = NoteGrouping.excludingDocumentSentinel(allCombinedNotes)         let grouped = NoteGrouping.groupByStructure(combinedNotes, structure: structure)-        let resolved = NoteGrouping.resolvedNotes(from: combinedNotes)+        let resolved = NoteGrouping.resolvedNotes(from: allCombinedNotes)          VStack(spacing: 0) {             // Always show header so the + button is accessible
prismTests/SidebarNotesViewTests.swift Modified +64 / -0
diff --git a/prismTests/SidebarNotesViewTests.swift b/prismTests/SidebarNotesViewTests.swiftindex 6f71290c..20a5ea4a 100644--- a/prismTests/SidebarNotesViewTests.swift+++ b/prismTests/SidebarNotesViewTests.swift@@ -141,6 +141,70 @@ struct SidebarNotesViewTests {         #expect(result.isEmpty)     } +    // MARK: - T-1864: resolved document-level notes in the sidebar++    /// `SidebarNotesView.body` feeds the FULL combined-notes set (including the+    /// document-level sentinel) into `resolvedNotes`, not the block-only set+    /// `excludingDocumentSentinel` produces for structural grouping. Before the+    /// fix, the sidebar reused the block-only dictionary for both, so a resolved+    /// document-level note had nowhere to appear — it was excluded from+    /// structural grouping (correctly, since it has no block) AND excluded from+    /// the Resolved section (incorrectly), effectively vanishing.+    @Test("Sidebar resolved section includes resolved document-level notes")+    func resolvedSectionIncludesDocumentLevelNotes() async {+        let store = MockNotesStore()+        let manager = NotesManager.makeForTesting(store: store)++        let block = MarkdownBlock.paragraph(markdown: "Block content")+        let docNote = BlockNote(+            id: UUID(),+            blockId: BlockNote.documentSentinelId,+            contextQuote: "",+            content: "Resolved doc note",+            status: .resolved,+            createdAt: Date(),+            modifiedAt: Date()+        )+        let docNotes = DocumentNotes(+            identifier: DocumentIdentifier(path: "test.md"),+            displayName: "test.md",+            notes: [docNote]+        )+        await store.preload(docNotes)+        await manager.loadNotes(+            source: .file(url: URL(fileURLWithPath: "/test.md")),+            sessionID: UUID(),+            blocks: [block]+        )++        // Mirrors SidebarNotesView.body's computation.+        let allCombinedNotes = NoteGrouping.buildCombinedNotes(from: manager)+        let combinedNotes = NoteGrouping.excludingDocumentSentinel(allCombinedNotes)+        let resolved = NoteGrouping.resolvedNotes(from: allCombinedNotes)++        #expect(+            resolved.contains { $0.id == docNote.id },+            "Resolved document-level notes must appear in the sidebar's Resolved section"+        )+        // The sentinel must still stay out of structural grouping.+        #expect(!combinedNotes.keys.contains(BlockNote.documentSentinelId))+    }++    @Test("excludingDocumentSentinel removes only the sentinel key")+    func excludingDocumentSentinelRemovesOnlySentinel() {+        let docNote = makeNote(blockId: BlockNote.documentSentinelId)+        let blockNote = makeNote(blockId: "b1")+        let combined: [String: [BlockNote]] = [+            BlockNote.documentSentinelId: [docNote],+            "b1": [blockNote],+        ]++        let result = NoteGrouping.excludingDocumentSentinel(combined)++        #expect(!result.keys.contains(BlockNote.documentSentinelId))+        #expect(result.keys.contains("b1"))+    }+     // MARK: - groupByStructure      @Test("groupByStructure returns empty for empty input")
specs/bugfixes/sidebar-resolved-document-notes/report.md Added +161 / -0
diff --git a/specs/bugfixes/sidebar-resolved-document-notes/report.md b/specs/bugfixes/sidebar-resolved-document-notes/report.mdnew file mode 100644index 00000000..520d3c0a--- /dev/null+++ b/specs/bugfixes/sidebar-resolved-document-notes/report.md@@ -0,0 +1,161 @@+# Bugfix Report: Resolved Document Notes Disappear From The Regular Sidebar++**Date:** 2026-09-06+**Status:** Fixed++## Description of the Issue++In the regular (iPad/macOS) sidebar, resolving a document-level note (a note+not anchored to any specific block) made it disappear entirely from+`SidebarNotesView`. It was not in the active "Document Notes" section (which+only shows active notes) and it was not in the collapsed "Resolved" section+either, so there was no way to see or restore it from that layout. The+compact `NotesPanel` did not have this problem.++**Reproduction steps:**+1. Open a document in the regular iPad/macOS layout.+2. Add a document-level note (tap the "+" in the sidebar header).+3. Mark the note resolved.+4. Observe: the note vanishes from the sidebar entirely.++**Impact:** Any resolved document-level note becomes invisible and+unreachable from the regular sidebar (iPad/macOS). The note is not lost from+storage, but the user has no way to view, reopen, or manage it from that+surface.++## Investigation Summary++- **Symptoms examined:** Compared `SidebarNotesView`'s active Document Notes+  section (`NotesManager+DocumentLevel.documentLevelNotes`, active-only) with+  its Resolved section, and with the compact `NotesPanel`'s resolved-notes+  computation.+- **Code inspected:** `prism/Views/SidebarNotesView.swift`,+  `prism/Models/NoteGrouping.swift`, `prism/Services/NotesManager+DocumentLevel.swift`,+  `prism/Views/NotesPanel.swift`.+- **Hypotheses tested:** Confirmed `NotesPanel.resolvedNotesSection` builds its+  resolved list from `documentNotes?.notes` + `importedNotes.values.flatMap`,+  which is not filtered by block ID and therefore includes document-level+  notes. `SidebarNotesView.body`, by contrast, derives its `resolved` value+  from `NoteGrouping.buildCombinedBlockNotes(from:)`, which explicitly strips+  the document-level sentinel key (`BlockNote.documentSentinelId`) before+  `resolvedNotes` ever sees it.++## Discovered Root Cause++`SidebarNotesView.body` computed one combined-notes dictionary+(`buildCombinedBlockNotes`, which excludes the document-level sentinel) and+fed it into *both* `groupByStructure` (structural grouping, correctly+excluding the sentinel since it has no block to attach to) and `resolvedNotes`+(a flat, non-structural collection that has no such restriction). Because the+sentinel-keyed note was removed before `resolvedNotes` ran, a resolved+document-level note could never appear in the Resolved section.++**Defect type:** Logic error — over-filtering shared input for two consumers+with different requirements.++**Why it occurred:** `buildCombinedBlockNotes` was designed for structural+grouping, where the document sentinel legitimately doesn't belong. It was+reused for the Resolved section under the assumption that "combined notes for+the sidebar" is one dictionary, without noticing the Resolved section is not+structural and should see the full set.++**Contributing factors:** No prior test exercised a resolved document-level+note against the sidebar's resolved computation, since `NoteGroupingTests`'s+sentinel-exclusion tests only assert exclusion from `buildCombinedBlockNotes`+and `groupByStructure` — both correct behaviours in isolation.++## Resolution for the Issue++**Changes made:**+- `prism/Models/NoteGrouping.swift` - Extracted the sentinel-filter predicate+  out of `buildCombinedBlockNotes` into a new public helper,+  `excludingDocumentSentinel(_:)`, so callers can apply the same filter+  selectively instead of only via the block-only builder.+- `prism/Views/SidebarNotesView.swift` - `body` now computes the full combined+  notes dictionary once (`NoteGrouping.buildCombinedNotes`), derives the+  block-only dictionary from it via `excludingDocumentSentinel` for+  `groupByStructure`, and feeds the *full* dictionary (including the+  document-level sentinel) into `resolvedNotes`.++**Approach rationale:** The fix is minimal and keeps the sentinel out of+structural grouping (where it doesn't belong, since it has no document+position) while restoring it to the flat Resolved collection, matching+`NotesPanel`'s existing (correct) behaviour and satisfying global-notes+requirement 2.8.++**Alternatives considered:**+- Special-casing the document sentinel inside `resolvedNotes` itself -+  Rejected: `resolvedNotes` is a generic, structure-agnostic filter over+  whatever dictionary it's given; baking in sentinel awareness there would+  make it lie about its own exclusion for other callers who explicitly want+  the block-only set (e.g. `groupByStructure`'s callers).+- Adding a separate `resolvedDocumentLevelNotes` accessor mirroring+  `NotesManager+DocumentLevel.documentLevelNotes` and merging it manually in+  the view - Rejected: more surface area and a second place to keep in sync,+  versus reusing the dictionary already being built once per render.++## Regression Test++**Test file:** `prismTests/SidebarNotesViewTests.swift`+**Test name:** `resolvedSectionIncludesDocumentLevelNotes`,+`excludingDocumentSentinelRemovesOnlySentinel`++**What it verifies:** `resolvedSectionIncludesDocumentLevelNotes` loads a+resolved document-level note through a real `NotesManager` and mirrors+`SidebarNotesView.body`'s exact computation (build full combined notes once,+derive the block-only set via `excludingDocumentSentinel`, feed the full set+into `resolvedNotes`), asserting the resolved document-level note appears in+the Resolved collection while still being absent from the block-only+dictionary used for structural grouping.+`excludingDocumentSentinelRemovesOnlySentinel` pins the new helper's contract+directly.++**Run command:**+```+xcodebuild test -project prism.xcodeproj -scheme prism \+  -destination 'platform=macOS' -testPlan prism \+  -only-testing:prismTests/SidebarNotesViewTests test+```++## Affected Files++| File | Change |+|------|--------|+| `prism/Models/NoteGrouping.swift` | Added `excludingDocumentSentinel(_:)`; `buildCombinedBlockNotes` now delegates to it |+| `prism/Views/SidebarNotesView.swift` | `body` now feeds the full (sentinel-inclusive) combined notes into `resolvedNotes`, while structural grouping still uses the sentinel-excluded set |+| `prismTests/SidebarNotesViewTests.swift` | Added regression coverage for resolved document-level notes and the new helper |++## Verification++**Automated:**+- [x] Regression test passes+- [ ] Full test suite passes (not run — `make test-quick` is unreliable under+      contention with other agents on this machine; see Manual verification)+- [x] Linters/validators pass (`make lint`)++**Manual verification:**+- `make build-macos` succeeds with zero errors/warnings.+- Targeted xcodebuild run against `prismTests/SidebarNotesViewTests`,+  `prismTests/NoteGroupingTests`, `prismTests/NotesManagerDocumentLevelTests`,+  and `prismTests/ImportedDocumentLevelNotesTests` (macOS destination, single+  worker), confirmed via `Tools/check-test-results.sh`: 66/66 passed, 0+  failed, 0 skipped.++## Prevention++**Recommendations to avoid similar bugs:**+- When a helper filters a shared dictionary for one specific consumer+  (structural grouping), don't reuse that filtered result for a second+  consumer (a flat, non-structural collection) without checking whether the+  filter's justification still applies.+- Any future test of sentinel-exclusion behaviour should assert both+  directions: excluded where required (structural grouping) and retained+  where required (flat Resolved collection).++## Related++- Transit ticket T-1864+- Global-notes requirement 2.8 (resolved document-level notes appear in the+  Resolved section)+- `prism/Views/NotesPanel.swift` (`resolvedNotesSection`) — the compact+  layout's already-correct reference implementation

Things to double-check

Resolved-only document note.

Create one document-level note, resolve it, and confirm the sidebar still shows the list with a Resolved (1) disclosure rather than the empty state. Traced in code (anchoredNotes keeps the sentinel bucket regardless of status) but not exercised by a test.

Full suite.

Only six targeted suites (79 tests) were run here because other xcodebuild jobs were active on the machine. Run make test-quick on a quiet machine before merging if you want the whole macOS unit suite on record.

Untracked review artifact.

The HTML for this review is written under specs/bugfixes/sidebar-resolved-document-notes/ and then moved out by pulsar publish; confirm git status is clean before pushing.