---
phase: 28-reading-shell-fold-in
plan: 01
type: implement
wave: 1
depends_on: []
files_modified: [web/src/components/search/NavSearchResults.test.tsx, web/src/stores/navSearch.test.ts, web/src/routes/AppShell.test.tsx, web/src/routes/AppShell.tsx, web/src/routes/PageEditor.test.tsx, docs/architecture/overview.md, docs/architecture/graph-subsystem.md, .planning/PROJECT.md, internal/web/dist/.gitkeep]
autonomous: false
requirements: [RS-01, RS-02, RS-03, RS-04]

must_haves:
  truths:
    - "ENFORCED ORDER (RS-01/D-02): the sequence is (a) suites green → (b) backfill NavSearchResults/navSearch/⌘K tests → (c) RS-04 browser smoke → (d) commit. NO code enters HEAD before (a)+(b)+(c) pass. This is the exact 'committed-then-breaks' failure the milestone exists to fix; committing first is a plan violation, not a shortcut."
    - "The refactor lands as EXACTLY these commits, in order, each a coherent independently-revertable unit: (1) reading-shell, (2) inline nav-search, (3) local-graph removal end-to-end (Go+TS in ONE commit), (4) CM6 coordinate fix (theme.ts + LivePreviewEditor.tsx ALONE — its own revertable commit, HARD requirement), then a SEPARATE (5) housekeeping commit (prior-phase .smtc KB notes + 26-VERIFICATION.md) and (6) docs commit (scope-reduction docs). The housekeeping/docs commits stay OUT of the four refactor commits so those stay pure."
    - "At final HEAD after all commits: `git status --porcelain` shows ONLY uncommitted phase planning artifacts (SUMMARY + screenshots); the four refactor commits + housekeeping + docs account for every other working-tree path. `internal/web/dist/.gitkeep` is RESTORED (present, not deleted)."
    - "At final HEAD the FULL suites are green: `go build ./... && go vet ./... && go test ./...`; from web/ `npx tsc -b` clean, `npx vitest run` all pass, `npm run build` clean with the `assertLazyChunks` guard active. This is re-run AT HEAD, not merely trusted from pre-commit."
    - "RS-02: `web/src/components/search/NavSearchResults.test.tsx` (NEW) covers Pages/Headings/Attachments grouping (empty groups omitted), heading navigate-and-scroll (rehype-slug id = anchor with leading '#' stripped) WITH query-clear (setQuery('')), and loading / error / empty states — mocking `useSearch` + react-router `useNavigate`. `web/src/stores/navSearch.test.ts` (NEW) covers query set + clear (tree-returns-when-empty). ⌘K focusing the navrail input is asserted in `AppShell.test.tsx` (the handler lives in AppShell:558, not NavSearchResults)."
    - "RS-01 crit 3: the canonical tag union is CONFIRMED intact after the TagBadgeBar→PropertiesPanel move — `PropertiesPanel` rebuilds `readTags(frontmatter) ∪ inlineTagsFromBody(body)` (ci-deduped), inline-origin tags carry no ✕, only non-inline tags removable via byte-stable setTags. This is VERIFIED (PropertiesPanel.test.tsx already asserts it), not rebuilt."
    - "RS-03: the two scope reductions are documented as decisions-with-a-future-path — in-place tree-filter removal → docs/architecture/overview.md + PROJECT.md Key Decisions; per-page local-graph removal → docs/architecture/graph-subsystem.md + PROJECT.md Key Decisions; each points to its REQUIREMENTS.md Future re-introduction item. Stale refs corrected: AppShell.tsx:752 'top bar' comment and PageEditor.test.tsx:508,536 'TagBadgeBar' descriptions (the TagBadgeBar CSS class names inside PropertiesPanel.tsx/.css are legit — NOT touched)."
    - "RS-04: an agent-driven real-Chromium smoke against a running vite dev + Go backend exercises drag-select + copy across bold/heading/link/code (correct copied text) and caret placement around headings + on wrapped lines (lands where clicked, no drift); screenshots saved under the phase dir; results recorded in the SUMMARY. Any construct headless Chromium cannot faithfully exercise (true human-mouse drag drift) is FLAGGED for user spot-check, never silently claimed green."
    - "Fold, don't re-design: no reading-shell/graph/search file is re-architected, renamed, or 'improved' while committing. The only edits to folded code are RS-03 stale-ref corrections and any minimal fix needed to keep the baseline green (a real defect, not polish)."
---

<objective>
Fold the large uncommitted Obsidian-parity reading-shell refactor into a clean, VERIFIED-BEFORE-COMMITTED
baseline. Close the one material coverage gap (the inline nav-search redesign: NavSearchResults +
navSearch store + ⌘K), document the two intentional scope reductions (tree-filter removal; per-page
local-graph removal) with future-path pointers, correct two stale references, and confirm the
manual-QA-only CM6 selection/coordinate change in a real browser — THEN land the refactor as four
coherent feature commits (CM6 alone) plus a separate housekeeping and a docs commit. After this phase,
subsequent work builds on committed HEAD, not a working-tree blob. RS-01…RS-04. Fully independent phase.
</objective>

<execution_context>
@$HOME/.omp/agent/anvil/workflows/execute-plan.md
</execution_context>

<context>
@.planning/phases/28-reading-shell-fold-in/28-CONTEXT.md
@.planning/REQUIREMENTS.md §RS-01…RS-04
@.planning/ROADMAP.md §"Phase 28: Reading-Shell Fold-In"
@.planning/codebase/MAP-cm-editor-subsystem.md
</context>

<commit_policy override="true">
THIS PLAN OVERRIDES execute-plan's default per-task commit protocol. Reason: the deliverable is a
SPECIFIC slicing of a PRE-EXISTING working-tree blob, done ONLY AFTER verification + smoke pass (RS-01,
D-02). A per-task commit would contaminate the slicing and commit unverified code.

- Tasks 1–9 make NO git commits. They edit/create files in the working tree and run verification only.
- The human-verify checkpoint gates the commit.
- Task 10 is the ONE task that stages + commits, in the exact commit order in must_haves.
- Stage files individually (`git add <path>`), NEVER `git add .` / `git add -A`.
- The final SUMMARY + smoke screenshots are committed by execute-phase's normal SUMMARY step (a
  `docs(28)` commit) AFTER this plan completes — leave them uncommitted at plan end.
</commit_policy>

<grounding_facts>
Verified by reading the code THIS session (SMTC daemon OFFLINE — grep/read confidence, Structural tier; TRUST these):

**Baseline is green NOW (RS-01 crit 1a confirmed this session):** `go build/vet/test ./...` = 18 pkgs ok;
`npx tsc -b` exit 0; `npx vitest run` = 789 passed / 62 files; `npm run build` exit 0 (assertLazyChunks
active). The act(...) lines in vitest stderr are PRE-EXISTING warnings, not failures.

**Nav-search surface (RS-02 targets):**
- `NavSearchResults.tsx` (UNTESTED — no .test.tsx exists): default export, no props. Reads
  `useNavSearch` (`query`, `setQuery`), `useSearch(query)` → `{ data, isLoading, isError }`. Groups by
  `GROUP_ORDER` [page→"Pages", heading→"Headings", attachment→"Attachments"], empty groups filtered out
  (:35-41). `openResult(r)` (:55-69): heading+anchor → strip leading '#' → `navigate('/app/page/'+path+'#'+id)`
  + `requestAnimationFrame(scrollIntoView)`; else `navigate('/app/page/'+path)`; ALWAYS `setQuery("")`
  after. States: isError → role="alert" "Search is unavailable." (:71-77); showLoading&&total===0 →
  aria-live "Searching…" (:79-86); total===0 → "No matches for …" (:88-94); else grouped rows (:96-113).
  Renders `SearchResultRow` (already committed + `highlight.ts` unit-tested — DO NOT add a standalone
  SearchResultRow.test.tsx per D-06).
- `useSearch(rawQuery)` (hooks/useSearch.ts): `useQuery<SearchResult[]>({queryKey:["search",q], queryFn:
  ()=>search(q), enabled: q.trim().length>0, placeholderData:(prev)=>prev})`, 200ms debounce. MOCK this
  module in NavSearchResults tests to control `{data,isLoading,isError}`.
- `SearchResult` (api/client.ts:1207): `{ kind: "page"|"heading"|"attachment", title, path, snippet,
  anchor?, page_title? }`. `SearchResultKind` at :1200.
- `stores/navSearch.ts`: zustand `{ query:"", setQuery:(q)=>set({query:q}) }`. NOT persisted.
- `AppShell.tsx`: ⌘K handler at :557-565 (`(e.metaKey||e.ctrlKey) && key==='k'||'K'` → preventDefault →
  `openNav(true)` → setTimeout 0 → `searchInputRef.current?.focus()+select()`); navrail input at :693-697
  (`ref={searchInputRef}` className "navrail-filter-input" placeholder "Search…"); searching?→NavSearchResults
  vs LeftTree at :716-720. STALE comment at :752 "Search now lives in the top bar." (search is in the navrail).
  `AppShell.test.tsx` EXISTS (7.7KB) but has NO ⌘K/metaKey coverage — add the D-07 test there.

**Tag union (RS-01 crit 3 — VERIFY, do not build):**
- `PropertiesPanel.tsx:55-73` builds `effectiveTags`: frontmatter order (readTags) then inline-only body
  tags, ci-deduped, `inlineOrigin` flag drives no-✕. `PropertiesPanel.test.tsx` ALREADY asserts: non-inline
  gets ✕ + calls onRemoveTag("release"); inline-only chip has no ✕; ci-dedup of a frontmatter tag that is
  also inline → one chip, no ✕. RS-01 crit 3 = confirming these pass, not writing new ones.

**Stale test descriptions (RS-03):**
- `PageEditor.test.tsx:508` (an `it(...)` DESCRIPTION string) + `:536` (an in-body code comment) both name
  "TagBadgeBar" — the component is now `PropertiesPanel`.
  Update the DESCRIPTION strings only. Do NOT rename the `tag-badge`/`TagBadgeBar` CSS class names inside
  `PropertiesPanel.tsx/.css` (deliberate visual-parity reuse).

**CM6 change (RS-04 smoke target):**
- `web/src/lib/cm/theme.ts` (heading `margin`→`padding`, height-oracle comment ~:79) + `LivePreviewEditor.tsx`
  (`drawSelection` + `EditorView.lineWrapping`). WHY: CM6's height oracle measures padding (offsetHeight),
  not margin, so posAtCoords/coordsAtPos stay exact. Not unit-testable → real-browser smoke.

**Docs anchors (RS-03):**
- `docs/architecture/overview.md`: Component Map (:95), Key Architectural Decisions (:113) — fold the
  tree-filter→search note into the nav/reading-shell narrative + a Key-Decisions bullet.
- `docs/architecture/graph-subsystem.md`: `Neighborhood(ctx, pagePath, depth)` local-graph (:137),
  HTTP Endpoints (:173) — record the removal (retained global `/app/graph`; removed `/graph/local` +
  `Neighborhood` + `LocalGraphPanel`/`DepthControl`).
- `.planning/PROJECT.md`: `## Key Decisions` table (:98-115) — one row per removal, each with a Future pointer.
- `.planning/REQUIREMENTS.md` Future section (:264-268) already lists both re-introduction items — link to them.

**Working-tree hygiene (RS-01, D-03/D-04):**
- Non-refactor paths in `git status`: 8 `.smtc/memories/*.md` (untracked) + modified
  `.planning/phases/26-mermaid-diagrams/26-VERIFICATION.md` → SEPARATE housekeeping commit.
- Deleted `internal/web/dist/.gitkeep` → RESTORE (empty file; Go-embed build-output keeper); it is NOT
  part of the refactor.
</grounding_facts>

<tasks>

<task type="auto" tdd="false">
  <name>Task 1: Re-confirm the baseline is green + restore dist/.gitkeep (NO commit)</name>
  <files>internal/web/dist/.gitkeep</files>
  <read_first>
    - .planning/phases/28-reading-shell-fold-in/28-CONTEXT.md — the enforced verify-before-commit order (D-02) + hygiene decisions (D-03/D-04)
  </read_first>
  <action>
    This is the (a) step of the enforced order — establish that the working tree is green BEFORE adding
    any tests. Run, from repo root: `go build ./... && go vet ./... && go test ./...`. Then from `web/`:
    `npx tsc -b`, `npx vitest run`, `npm run build`. All must pass (vitest baseline is 789/62; act(...) lines
    are pre-existing warnings). If ANYTHING is red, STOP and report — a red baseline changes the plan
    (fold-not-redesign override: fixing a genuine defect to make the baseline green is in scope; re-design is not).
    Then RESTORE the accidentally-deleted build-output keeper: `git checkout -- internal/web/dist/.gitkeep`
    (or recreate the empty file and confirm `git status` no longer shows it as deleted). Make NO commit.
  </action>
  <acceptance_criteria>
    - `go build ./... && go vet ./... && go test ./...` exits 0
    - from web/: `npx tsc -b` exits 0; `npx vitest run` reports all files passed; `npm run build` exits 0
    - `test -f internal/web/dist/.gitkeep` succeeds AND `git status --porcelain internal/web/dist/.gitkeep` prints nothing (no longer staged-for-deletion)
    - `git log --oneline -1` is unchanged from plan start (no commit made)
  </acceptance_criteria>
</task>

<task type="auto" tdd="true">
  <name>Task 2: RS-02 — NavSearchResults.test.tsx (grouping, heading nav+scroll+query-clear, states)</name>
  <files>web/src/components/search/NavSearchResults.test.tsx</files>
  <read_first>
    - web/src/components/search/NavSearchResults.tsx — the exact grouping (:35-41), openResult (:55-69), and state branches (:71-113) under test
    - web/src/hooks/useSearch.ts — the hook to mock ({data,isLoading,isError})
    - web/src/api/client.ts:1200-1214 — SearchResult / SearchResultKind shape for fixtures
    - web/src/components/search/SearchResultRow.tsx — how a row renders + onClick=onOpen (rows exercised transitively, D-06)
    - web/src/stores/recent.test.ts + web/src/stores/navSearch.ts — store-reset pattern to seed the query
  </read_first>
  <action>
    Create `NavSearchResults.test.tsx` (vitest + @testing-library/react + user-event). Render inside a
    react-router `MemoryRouter` (NavSearchResults calls useNavigate). Mock `../../hooks/useSearch`
    (`vi.mock`) so each test controls `{ data, isLoading, isError }`; seed a non-empty query via
    `useNavSearch.getState().setQuery("q")` in `beforeEach` (component precondition: parent only mounts it
    when query is non-empty), and reset it to "" in `afterEach`. Assert:
    1. GROUPING: given data with page+heading+attachment results, the three group labels "Pages",
       "Headings", "Attachments" render in that order; a kind with zero results omits its group
       (e.g. no attachments → no "Attachments" label).
    2. HEADING NAVIGATE-AND-SCROLL + QUERY-CLEAR: a heading result with `anchor: "#my-section"` — clicking
       its row calls `navigate` with `/app/page/<path>#my-section` (leading '#' stripped for the id, kept in
       the route hash) AND clears the query (`useNavSearch.getState().query === ""`). Spy `useNavigate`
       (mock react-router-dom's useNavigate to return a `vi.fn()`); assert the navigate arg. (scrollIntoView
       runs in a rAF against a possibly-absent element — do not assert DOM scroll; assert the navigate path + query clear.)
    3. STATES: `isError:true` → an element with role="alert" containing "unavailable"; `data:[]` (total 0,
       not loading) → "No matches" text; `isLoading:true` with no data → after the 150ms delay,
       "Searching…" (advance timers with `vi.useFakeTimers()`/`vi.advanceTimersByTime(150)` or assert the
       aria-live status region appears). Keep it deterministic — prefer fake timers for the loading branch.
    RED first (test file referencing the untested component), then confirm GREEN (the component already
    exists — tests should pass once written correctly; if a test reveals a real behavior gap, that is a
    finding to report, not a component redesign).
  </action>
  <acceptance_criteria>
    - `web/src/components/search/NavSearchResults.test.tsx` exists
    - grep: file contains `Pages`, `Headings`, `Attachments`, `role="alert"` or `alert`, and `setQuery`/`query` clear assertion
    - from web/: `npx vitest run src/components/search/NavSearchResults.test.tsx` — all tests pass, ≥6 test cases (grouping, empty-group-omitted, heading nav+clear, error, empty, loading)
    - the heading test asserts the navigate arg is `/app/page/<path>#<id-without-leading-hash>`
  </acceptance_criteria>
</task>

<task type="auto" tdd="true">
  <name>Task 3: RS-02 — navSearch store test (query set/clear; tree-returns-when-empty)</name>
  <files>web/src/stores/navSearch.test.ts</files>
  <read_first>
    - web/src/stores/navSearch.ts — the store under test ({query, setQuery})
    - web/src/stores/recent.test.ts — the established store-test pattern (getState() + beforeEach reset)
    - web/src/stores/editorMode.test.ts — a second store-test example (if present)
  </read_first>
  <action>
    Create `navSearch.test.ts` following the store-test pattern. `beforeEach`:
    `useNavSearch.getState().setQuery("")`. Assert: initial `query === ""`; `setQuery("hello")` →
    `getState().query === "hello"`; `setQuery("")` clears it back to "" (this is the tree-returns-when-empty
    contract — the empty query is what makes AppShell render LeftTree instead of NavSearchResults, per
    AppShell:716-720). Keep it a pure store test (no React).
  </action>
  <acceptance_criteria>
    - `web/src/stores/navSearch.test.ts` exists
    - from web/: `npx vitest run src/stores/navSearch.test.ts` — all pass, ≥3 cases (initial empty, set, clear)
    - grep: file imports `useNavSearch` from `./navSearch` and uses `.getState()`
  </acceptance_criteria>
</task>

<task type="auto" tdd="true">
  <name>Task 4: RS-02 — ⌘K focuses the navrail search input (in AppShell.test.tsx)</name>
  <files>web/src/routes/AppShell.test.tsx</files>
  <read_first>
    - web/src/routes/AppShell.test.tsx — the existing suite's render harness (providers, mocks) to reuse
    - web/src/routes/AppShell.tsx:557-565 (⌘K handler) + :693-697 (searchInputRef input) — the behavior under test
  </read_first>
  <action>
    Add a test to the EXISTING `AppShell.test.tsx` (D-07: the handler lives in AppShell, not NavSearchResults).
    Using the suite's existing render harness (same providers/mocks the other AppShell tests use), fire a
    ⌘K / Ctrl+K keydown on `document` (e.g. `fireEvent.keyDown(document, { key: "k", metaKey: true })` —
    the handler is registered via `document.addEventListener("keydown", …)` at AppShell.tsx:567, so a
    `window`-dispatched event would NOT reach it in jsdom; target `document`/`document.body`), then
    assert the navrail search input (`placeholder="Search…"` / class `navrail-filter-input`) receives focus.
    The handler focuses via `setTimeout(...,0)` — use `await waitFor(() => expect(input).toHaveFocus())`
    (or advance timers). If the input is behind a collapsed nav, assert `openNav(true)` first opened it (the
    input becomes queryable). Reuse mocks; do not add a new render harness. Keep the existing tests untouched.
  </action>
  <acceptance_criteria>
    - grep: AppShell.test.tsx contains `metaKey` (or `ctrlKey`) and a focus assertion (`toHaveFocus`)
    - from web/: `npx vitest run src/routes/AppShell.test.tsx` — all tests pass (existing + the new ⌘K case)
    - the new test asserts the navrail search input is focused after the ⌘K keydown
  </acceptance_criteria>
</task>

<task type="auto" tdd="false">
  <name>Task 5: RS-01 crit 3 — confirm the tag-union tests pass (verify, don't build)</name>
  <files>web/src/components/PropertiesPanel.test.tsx</files>
  <read_first>
    - web/src/components/PropertiesPanel.tsx:55-73 — the effectiveTags union (readTags ∪ inline; inlineOrigin→no ✕)
    - web/src/components/PropertiesPanel.test.tsx — the existing assertions of that union
  </read_first>
  <action>
    Do NOT write new tests. Run `npx vitest run src/components/PropertiesPanel.test.tsx` from web/ and
    confirm the existing cases pass: (1) a non-inline (frontmatter-only) tag has a ✕ and its click calls
    `onRemoveTag`; (2) an inline-only body tag renders as a chip with NO ✕; (3) a frontmatter tag that is
    ALSO inline de-dupes (case-insensitive) into ONE chip with no ✕. If all pass, RS-01 crit 3 (the canonical
    union survived the TagBadgeBar→PropertiesPanel move) is confirmed — record that in the SUMMARY. If any
    FAIL, that is a real regression in the folded code → report it as a finding (fixing it minimally to make
    the union correct is in scope per the fold-not-redesign override; do not re-architect PropertiesPanel).
  </action>
  <acceptance_criteria>
    - from web/: `npx vitest run src/components/PropertiesPanel.test.tsx` exits 0 (all pass)
    - the SUMMARY records RS-01 crit 3 as CONFIRMED with the three asserted properties (inline no-✕, non-inline removable, ci-dedup)
  </acceptance_criteria>
</task>

<task type="auto" tdd="false">
  <name>Task 6: RS-03 — correct the two stale references (comment + test descriptions)</name>
  <files>web/src/routes/AppShell.tsx, web/src/routes/PageEditor.test.tsx</files>
  <read_first>
    - web/src/routes/AppShell.tsx:752 — the stale "Search now lives in the top bar." comment
    - web/src/routes/PageEditor.test.tsx:508,536 — the two "TagBadgeBar" test descriptions
    - web/src/components/PropertiesPanel.tsx + PropertiesPanel.css — CONFIRM the `tag-badge`/`TagBadgeBar` CSS class names here are legit (visual parity) and MUST NOT be renamed
  </read_first>
  <action>
    (1) AppShell.tsx:752 — replace the stale comment "Search now lives in the top bar." with an accurate
    one, e.g. `{/* Fixed footer — Settings/Admin. Search lives inline in the navrail (⌘K focuses it). */}`.
    (2) PageEditor.test.tsx:508 + :536 — update the DESCRIPTION strings that name "TagBadgeBar" to name
    "PropertiesPanel" (the component that now renders the tag chips), keeping the assertion bodies unchanged
    (e.g. ":508 approving suggested tags lands the tag in frontmatter and renders it in PropertiesPanel
    (non-inline → removable)"; ":536 the PropertiesPanel comment"). Do NOT touch any `tag-badge` /
    `TagBadgeBar` CSS class-name string inside PropertiesPanel.tsx/.css — those are deliberate. Re-run
    `npx vitest run src/routes/PageEditor.test.tsx` to confirm still green.
  </action>
  <acceptance_criteria>
    - grep `-n "Search now lives in the top bar" web/src/routes/AppShell.tsx` → no match (comment corrected)
    - grep `-n "TagBadgeBar" web/src/routes/PageEditor.test.tsx` → no match in the :508/:536 DESCRIPTION strings (now "PropertiesPanel")
    - grep `-n "tag-badge" web/src/components/PropertiesPanel.tsx web/src/components/PropertiesPanel.css` → still present (class names untouched)
    - from web/: `npx vitest run src/routes/PageEditor.test.tsx` exits 0
  </acceptance_criteria>
</task>

<task type="auto" tdd="false">
  <name>Task 7: RS-03 — document the two scope reductions (docs/ + PROJECT.md Key Decisions)</name>
  <files>docs/architecture/overview.md, docs/architecture/graph-subsystem.md, .planning/PROJECT.md</files>
  <read_first>
    - docs/architecture/overview.md:95-119 — Component Map + Key Architectural Decisions (tree-filter→search lands here)
    - docs/architecture/graph-subsystem.md:124-186 — Query Layer (Neighborhood :137) + HTTP Endpoints (:173) (local-graph removal lands here)
    - .planning/PROJECT.md:98-115 — the Key Decisions table (one row per removal)
    - .planning/REQUIREMENTS.md:264-268 — the Future re-introduction items to point at
  </read_first>
  <action>
    Record BOTH removals as intentional, revisitable decisions (D-09/D-10):
    (1) overview.md — in the nav/reading-shell narrative (near Component Map) add a short note: the in-place
    file-tree filter (narrow-the-tree-by-title) is removed; the navrail search now REPLACES the tree with
    full-text results (NavSearchResults) when the query is non-empty, returning to the tree when cleared.
    Add a Key-Architectural-Decisions bullet stating this, with a pointer to REQUIREMENTS.md Future
    ("In-place file-tree filter") as the re-introduction path.
    (2) graph-subsystem.md — under the Query Layer / HTTP Endpoints, record that the PER-PAGE local graph is
    removed end-to-end: the global `/app/graph` (GraphData) is RETAINED; removed are the `/graph/local`
    route + handler, the `Neighborhood(ctx, pagePath, depth)` query method, and the client
    `LocalGraphPanel`/`DepthControl`. Point to REQUIREMENTS.md Future ("Re-introduce a per-page local graph").
    (3) PROJECT.md Key Decisions — add TWO rows to the table (:102 header), e.g.
    `| v1.5: in-place tree-filter removed; navrail search replaces the tree | One find surface; full-text > title-prefix for a growing wiki | Removed in the reading-shell fold-in; Future re-intro tracked (REQUIREMENTS Future) |`
    and `| v1.5: per-page local graph removed; global /app/graph retained | Local neighborhood underused vs its route+handler+panel weight | /graph/local + Neighborhood + LocalGraphPanel/DepthControl removed; Future re-intro tracked |`.
    Prose only; keep byte-changes scoped to these sections; do NOT restructure the docs.
  </action>
  <acceptance_criteria>
    - grep `-in "file-tree filter\|tree filter\|navrail search" docs/architecture/overview.md` → matches the new note
    - grep `-in "local graph\|/graph/local\|Neighborhood\|LocalGraphPanel" docs/architecture/graph-subsystem.md` → the removal is recorded (retained global graph noted)
    - grep `-in "local graph\|tree.filter" .planning/PROJECT.md` → two new Key-Decisions rows present, each referencing the Future re-intro path
    - both docs reference the REQUIREMENTS.md Future re-introduction items
  </acceptance_criteria>
</task>

<task type="auto" tdd="false">
  <name>Task 8: PRE-COMMIT GATE — full suites + build green with all edits in the tree (RS-01 crit 1, step b done)</name>
  <files></files>
  <read_first>
    - .planning/phases/28-reading-shell-fold-in/28-CONTEXT.md — D-02 enforced order (this is the gate before the smoke + commit)
  </read_first>
  <action>
    With Tasks 1–7's new tests + doc/stale-ref edits in the working tree (still UNCOMMITTED), re-run the FULL
    gate — this proves the backfilled tests + edits leave everything green before the smoke and the commit.
    From repo root: `go build ./... && go vet ./... && go test ./...`. From web/: `npx tsc -b`,
    `npx vitest run` (now INCLUDING the new NavSearchResults/navSearch/⌘K tests — count is up from 789),
    `npm run build` (assertLazyChunks active). ALL must pass. If red, fix the offending NEW test/doc edit
    (not the folded code, unless a genuine defect) until green. Record the new vitest pass count in the SUMMARY.
    Make NO commit.
  </action>
  <acceptance_criteria>
    - `go build ./... && go vet ./... && go test ./...` exits 0
    - from web/: `npx tsc -b` exits 0; `npx vitest run` all pass (test count > 789 baseline); `npm run build` exits 0
    - `git log --oneline -1` still unchanged from plan start (no commit yet)
  </acceptance_criteria>
</task>

<task type="auto" tdd="false">
  <name>Task 9: RS-04 — real-Chromium smoke of the CM6 selection/coordinate change (step c)</name>
  <files>.planning/phases/28-reading-shell-fold-in/</files>
  <read_first>
    - web/src/lib/cm/theme.ts (~:79 heading margin→padding + the height-oracle comment) — WHY the change exists
    - web/src/components/LivePreviewEditor.tsx (drawSelection + lineWrapping) — the behavior to smoke
    - .planning/codebase/MAP-cm-editor-subsystem.md — the CM6 subsystem map (coordinate/selection surface)
    - .planning/phases/28-reading-shell-fold-in/28-CONTEXT.md D-05 — the drive-Chromium + flag-infidelity contract
  </read_first>
  <action>
    Agent-driven smoke (D-05). Start the app: a Go backend + `vite dev` (or `npm run dev`) — bring up a dev
    server and log in / open a page with an editor containing bold, a heading, a link, and a code span/block,
    plus at least one long (wrapping) line. Using the browser tool, drive a REAL Chromium tab against it and
    exercise, capturing a screenshot into the phase dir for each:
    1. Drag-select across bold + heading + link + code, copy, and verify the copied text is the correct
       plain text of the selection (read the clipboard or the selection range text).
    2. Click to place the caret immediately around a heading (just before/after it) and on a WRAPPED line —
       confirm the caret lands where clicked, no vertical/horizontal drift (this is exactly what the
       margin→padding height-oracle fix targets).
    Save screenshots as e.g. `28-smoke-<case>.png` under the phase dir. Record PASS/FAIL per case in the
    SUMMARY with the screenshot names. Per D-05: any construct headless Chromium cannot faithfully reproduce
    (notably true human-mouse drag-select drift) — FLAG it explicitly for the user's spot-check in the
    SUMMARY rather than claiming it green. If the browser tool or a dev server is genuinely unavailable in
    this environment, do NOT fake it: STOP and surface it as a blocker (the smoke is a named RS-04 acceptance
    criterion and gates the commit) so the co-driver can run the checklist.
  </action>
  <acceptance_criteria>
    - ≥3 screenshots saved under `.planning/phases/28-reading-shell-fold-in/` (drag-copy across constructs; caret-around-heading; caret-on-wrapped-line)
    - the SUMMARY records, per RS-04 case, PASS/FAIL + the screenshot filename, and explicitly flags any headless-infidelity case for user spot-check
    - copied-text correctness across bold/heading/link/code is asserted (actual copied string recorded), not merely "looks right"
  </acceptance_criteria>
</task>

<task type="checkpoint:human-verify">
  <name>Checkpoint: review green suites + RS-04 smoke evidence BEFORE the baseline lands</name>
  <action>
    Present to the co-driver, and WAIT for confirmation before Task 10 commits anything:
    - the Task 8 gate result (backend green; tsc/vitest/build green; new vitest count) — RS-01 crit 1 (a)+(b);
    - the RS-04 smoke results + screenshot names, with any headless-infidelity cases flagged for spot-check — crit 5;
    - the exact 6-commit slicing plan Task 10 will execute (the four refactor commits + housekeeping + docs).
    This is the trust gate the whole milestone exists to enforce: no unverified 64-file blob enters HEAD.
    Proceed to Task 10 ONLY on explicit approval; if the user flags a smoke failure, route it back as a
    finding (do not commit).
  </action>
</task>

<task type="auto" tdd="false">
  <name>Task 10: Slice the working tree into the 6 ordered commits (the ONLY committing task)</name>
  <files></files>
  <read_first>
    - .planning/phases/28-reading-shell-fold-in/28-CONTEXT.md D-01/D-03/D-04 — the commit slicing + hygiene contract
    - the <commit_policy> block above — stage individually; this is the only task that commits
  </read_first>
  <action>
    Stage each file individually (`git add <path>` — NEVER `git add .`/`-A`) and commit in THIS order. Use
    `git status --porcelain` to confirm the routing accounts for every path; if a working-tree path is NOT
    in a bucket below AND is not a known-uncommitted planning artifact (this plan file
    `28-01-PLAN.md`, the SUMMARY, or the `28-smoke-*.png` screenshots — all left for execute-phase's
    SUMMARY step), STOP and surface it (do not guess it into a commit). Conventional, feature-scoped
    messages `<type>(28): …`.

    COMMIT 1 — reading-shell (the reading column becomes a document):
      NEW: web/src/components/Breadcrumb.{tsx,css,test.tsx};
           web/src/components/DocStatusBar.{tsx,css,test.tsx};
           web/src/components/PropertiesPanel.{tsx,css,test.tsx}
      MOD: web/src/routes/PageEditor.{tsx,css,test.tsx}; web/src/routes/PageView.{tsx,css,test.tsx};
           web/src/components/DescriptionGenerate.tsx; web/src/components/TagSuggest.tsx;
           web/src/lib/frontmatter.{ts,test.ts}; web/src/components/LivePreviewEditor.css
      DEL: web/src/components/TagBadgeBar.{tsx,css,test.tsx}
      (PageView carries the incidental LocalGraphPanel-import removal — kept here per D-01, which scopes the
       local-graph commit to the graph/Go files, not PageView; the panel FILE is deleted in commit 3.)
      msg e.g. `feat(28): reading-shell document view — Breadcrumb + PropertiesPanel + DocStatusBar (retire TagBadgeBar)`

    COMMIT 2 — inline nav-search (⌘K modal → navrail full-text surface):
      NEW: web/src/components/search/NavSearchResults.{tsx,css,test.tsx}; web/src/stores/navSearch.{ts,test.ts}
      MOD: web/src/routes/AppShell.{tsx,css,test.tsx}; web/src/components/LeftTree.tsx
      DEL: web/src/components/search/SearchPalette.{tsx,css,test.tsx}; web/src/store/searchStore.ts;
           web/src/stores/treeFilter.ts
      msg e.g. `feat(28): inline navrail full-text search (NavSearchResults + navSearch) replacing the ⌘K palette`

    COMMIT 3 — local-graph removal, END-TO-END in ONE commit (Go + TS):
      MOD (Go): internal/graph/query.go; internal/graph/query_test.go; internal/server/handlers_graph.go;
                internal/server/handlers_graph_test.go; internal/server/router.go; internal/server/handlers_auth.go
      MOD (TS): web/src/api/client.ts; web/src/components/graph/GraphView.tsx;
                web/src/components/graph/GraphCanvas.tsx; web/src/hooks/useGraph.ts; web/src/hooks/useGraph.test.ts
      DEL: web/src/components/graph/DepthControl.{tsx,css,test.tsx};
           web/src/components/graph/LocalGraphPanel.{tsx,css,test.tsx};
           web/src/stores/localGraphPanel.{ts,test.ts}
      msg e.g. `refactor(28): remove per-page local graph end-to-end (retain global /app/graph)`

    COMMIT 4 — CM6 coordinate fix, ALONE (independently revertable — HARD requirement, D-01):
      MOD: web/src/lib/cm/theme.ts; web/src/components/LivePreviewEditor.tsx
      msg e.g. `fix(28): CM6 selection/coordinate accuracy — heading margin→padding + drawSelection + lineWrapping`

    COMMIT 5 — housekeeping (SEPARATE, out of the refactor commits, D-03):
      ADD: .smtc/memories/*.md (the 8 untracked prior-phase KB notes)
      MOD: .planning/phases/26-mermaid-diagrams/26-VERIFICATION.md
      msg e.g. `chore(28): fold in prior-phase KB notes + phase-26 verification`

    COMMIT 6 — scope-reduction docs (RS-03, new this phase):
      MOD: docs/architecture/overview.md; docs/architecture/graph-subsystem.md; .planning/PROJECT.md
      msg e.g. `docs(28): document tree-filter + per-page-local-graph removals (with future re-intro paths)`

    NOTE: internal/web/dist/.gitkeep was RESTORED in Task 1 → it must NOT appear in any commit. Leave the
    SUMMARY + smoke screenshots UNCOMMITTED (execute-phase's SUMMARY step commits them). After committing,
    re-run the FULL gate AT HEAD and confirm the tree is clean.
  </action>
  <acceptance_criteria>
    - `git log --oneline -6` shows the six commits in the order above with feature-scoped `(28)` messages
    - the CM6 commit (4) touches ONLY web/src/lib/cm/theme.ts + web/src/components/LivePreviewEditor.tsx (`git show --stat` confirms) — independently revertable
    - commit 3 contains BOTH Go and TS local-graph paths (one commit); commits 5+6 contain NO refactor code
    - `git status --porcelain` after all commits lists ONLY .planning phase artifacts (SUMMARY/screenshots); `internal/web/dist/.gitkeep` is NOT deleted and NOT in any commit
    - AT HEAD: `go build ./... && go vet ./... && go test ./...` exits 0; from web/ `npx tsc -b` + `npx vitest run` + `npm run build` all exit 0
  </acceptance_criteria>
</task>

</tasks>

<verification>
- Backend at final HEAD: `go build ./... && go vet ./... && go test ./...` — all green.
- Frontend at final HEAD (from web/): `npx tsc -b` clean; `npx vitest run` all pass (count > 789 baseline, incl. NavSearchResults + navSearch + ⌘K); `npm run build` clean (assertLazyChunks active).
- `git log --oneline -6` = the six ordered commits; `git show --stat` on commit 4 = theme.ts + LivePreviewEditor.tsx only.
- `git status --porcelain` = only uncommitted phase artifacts; `internal/web/dist/.gitkeep` present.
- RS-04 screenshots present under the phase dir; SUMMARY records per-case PASS/FAIL + any spot-check flag.
</verification>

<success_criteria>
1. RS-01: suites confirmed green (a) before and (b) after the backfill; the refactor is committed as the
   four ordered feature commits (CM6 alone, independently revertable) + separate housekeeping + docs
   commits; final HEAD is green; dist/.gitkeep restored; the tag union is confirmed intact (crit 3).
2. RS-02: NavSearchResults + navSearch have new tests (grouping, heading nav+scroll+query-clear,
   loading/error/empty); ⌘K-focuses-navrail is asserted in AppShell.test.tsx; vitest count rises and stays green.
3. RS-03: both scope reductions documented in docs/ + PROJECT.md Key Decisions with Future re-intro pointers;
   the AppShell "top bar" comment and PageEditor.test.tsx "TagBadgeBar" descriptions corrected (CSS class
   names untouched).
4. RS-04: an agent-driven real-Chromium smoke confirms drag-select+copy across bold/heading/link/code and
   caret placement around headings / on wrapped lines (no drift); screenshots + per-case results in the
   SUMMARY; headless-infidelity cases flagged for user spot-check.
5. Fold-not-redesign held: the only edits to folded code are the RS-03 stale-ref fixes (and any minimal
   green-keeping defect fix, reported as a finding) — no re-architecture.
</success_criteria>
