From 8ea4545c9b596ac3f94c81ac1567723cf9da8f97 Mon Sep 17 00:00:00 2001 From: ruohki Date: Sat, 28 Feb 2026 23:06:30 +0100 Subject: [PATCH] docs(03-01): complete link record extraction and wiki-link resolution plan - 03-01-SUMMARY.md: documents LinkRecord struct, render_markdown signature change, resolve_wiki_link algorithm, is_within_vault guard, and all decisions - STATE.md: advance to Phase 3 Plan 2, update progress to 75%, add 3 decisions, mark NAV path traversal blocker resolved - ROADMAP.md: update phase 3 plan progress (1 of 2 summaries) - REQUIREMENTS.md: mark NAV-01, NAV-02, NAV-10 complete --- .planning/REQUIREMENTS.md | 12 +- .planning/ROADMAP.md | 4 +- .planning/STATE.md | 22 +-- .../03-navigation-and-links/03-01-SUMMARY.md | 139 ++++++++++++++++++ 4 files changed, 160 insertions(+), 17 deletions(-) create mode 100644 .planning/phases/03-navigation-and-links/03-01-SUMMARY.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 60ba82b..df61dae 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -29,8 +29,8 @@ Requirements for initial release. Each maps to roadmap phases. ### Navigation -- [ ] **NAV-01**: User can follow `[[wiki-links]]` to other vault documents -- [ ] **NAV-02**: User can follow standard `[text](path.md)` links to other documents +- [x] **NAV-01**: User can follow `[[wiki-links]]` to other vault documents +- [x] **NAV-02**: User can follow standard `[text](path.md)` links to other documents - [ ] **NAV-03**: User can navigate back through history stack - [ ] **NAV-04**: User can navigate forward after going back - [x] **NAV-05**: User can scroll content with j/k, arrows, PgUp/PgDn @@ -38,7 +38,7 @@ Requirements for initial release. Each maps to roadmap phases. - [x] **NAV-07**: User sees graceful error page when a linked file is not found - [x] **NAV-08**: User sees keyboard hints in status bar - [x] **NAV-09**: App handles terminal resize without breaking layout -- [ ] **NAV-10**: User sees links highlighted inline and can Tab-cycle between them +- [x] **NAV-10**: User sees links highlighted inline and can Tab-cycle between them - [ ] **NAV-11**: User sees breadcrumb / current location in status bar ### BBS Aesthetic @@ -120,11 +120,11 @@ Which phases cover which requirements. Updated during roadmap creation. | NAV-07 | Phase 2 | Complete | | NAV-08 | Phase 2 | Complete | | NAV-09 | Phase 2 | Complete | -| NAV-01 | Phase 3 | Pending | -| NAV-02 | Phase 3 | Pending | +| NAV-01 | Phase 3 | Complete | +| NAV-02 | Phase 3 | Complete | | NAV-03 | Phase 3 | Pending | | NAV-04 | Phase 3 | Pending | -| NAV-10 | Phase 3 | Pending | +| NAV-10 | Phase 3 | Complete | | NAV-11 | Phase 3 | Pending | | BBS-01 | Phase 4 | Pending | | BBS-02 | Phase 4 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 7eb2e0c..2e0dd0d 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -61,7 +61,7 @@ Plans: 3. User can press Backspace or a back key to return to the previous document, and then press a forward key to return to where they were 4. User can Tab-cycle between links on a page to select and follow them without using a mouse 5. User sees their current location (breadcrumb) in the status bar -**Plans:** 2 plans +**Plans:** 1/2 plans executed Plans: - [ ] 03-01-PLAN.md — Renderer link extraction, wiki-link styling, and vault link resolution - [ ] 03-02-PLAN.md — App navigation: history, link cycling, draw-time selection, breadcrumb status bar @@ -86,5 +86,5 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 |-------|----------------|--------|-----------| | 1. Safety Foundation | 3/3 | Complete | 2026-02-28 | | 2. Vault Core and Rendering | 3/3 | Complete | 2026-02-28 | -| 3. Navigation and Links | 0/2 | Not started | - | +| 3. Navigation and Links | 1/2 | In Progress| | | 4. BBS Polish and Live Content | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index a443e90..1be6535 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,16 +5,16 @@ See: .planning/PROJECT.md (updated 2026-02-28) **Core value:** Users can connect via SSH and seamlessly browse a vault of linked markdown documents with retro BBS aesthetics -**Current focus:** Phase 2 COMPLETE — all 3 plans done (vault + highlighter + renderer + app integration) +**Current focus:** Phase 3 IN PROGRESS — Plan 01 done (LinkRecord, wiki-link resolver, path traversal guard) ## Current Position -Phase: 2 of 4 (Vault Core and Rendering) — COMPLETE -Plan: 3 of 3 in current phase (all plans done) -Status: Phase 2 complete — Phase 3 (Navigation) next -Last activity: 2026-02-28 — Plan 03 complete (app.rs wired with document display, scrolling, status bar) +Phase: 3 of 4 (Navigation and Links) — IN PROGRESS +Plan: 1 of 2 in current phase (03-01 done, 03-02 next) +Status: 03-01 complete — link metadata extraction and wiki-link resolution implemented +Last activity: 2026-02-28 — Plan 01 complete (LinkRecord, ENABLE_WIKILINKS, resolve_wiki_link, path traversal guard) -Progress: [███████░░░] 63% +Progress: [████████░░] 75% ## Performance Metrics @@ -29,6 +29,7 @@ Progress: [███████░░░] 63% |-------|-------|-------|----------| | 01-safety-foundation | 3 | 7 min | 2.3 min | | 02-vault-core-and-rendering | 3 | 9 min | 3.0 min | +| 03-navigation-and-links | 1 so far | 3.5 min | 3.5 min | **Recent Trend:** - Last 5 plans: 3 min, 2 min, 2 min, 4 min, 3 min @@ -64,6 +65,9 @@ Recent decisions affecting current work: - [Phase 02-03]: raw_content stored in App for resize re-render — avoids disk re-read on window resize - [Phase 02-03]: draw_error_screen() unified for Missing and Error via Option<&str> reason parameter - [Phase 02-03]: Scroll keys placed before _ catch-all — j/k during quit prompt does not dismiss it +- [Phase 03-navigation-and-links]: vault_path added to render_markdown for render-time broken wiki-link detection +- [Phase 03-navigation-and-links]: Broken wiki-links checked at render time; standard links deferred to navigation time +- [Phase 03-navigation-and-links]: link_span_start_count field tracks span array position at Tag::Link Start for span_len computation ### Pending Todos @@ -72,11 +76,11 @@ None. ### Blockers/Concerns - **REND blocker resolved**: renderer.rs uses no Widget trait at all — `render_markdown()` returns `Vec>` which Plan 03 passes to `Paragraph::new()`. No custom Widget needed. -- **NAV**: Path traversal via wiki-links must be addressed in Phase 3 link resolver — canonicalize and prefix-check every resolved path +- **NAV path traversal resolved**: is_within_vault() with canonicalize + starts_with guards all link resolution in vault.rs - **LIVE**: notify 8.x API must be verified at integration time; watch only current file (not full vault) to avoid inotify exhaustion ## Session Continuity Last session: 2026-02-28 -Stopped at: Phase 3 context gathered -Resume file: .planning/phases/03-navigation-and-links/03-CONTEXT.md +Stopped at: Completed 03-01-PLAN.md +Resume file: .planning/phases/03-navigation-and-links/03-02-PLAN.md diff --git a/.planning/phases/03-navigation-and-links/03-01-SUMMARY.md b/.planning/phases/03-navigation-and-links/03-01-SUMMARY.md new file mode 100644 index 0000000..b450c29 --- /dev/null +++ b/.planning/phases/03-navigation-and-links/03-01-SUMMARY.md @@ -0,0 +1,139 @@ +--- +phase: 03-navigation-and-links +plan: "01" +subsystem: renderer-vault +tags: + - links + - wiki-links + - pulldown-cmark + - path-traversal + - navigation +dependency_graph: + requires: + - 02-vault-core-and-rendering (renderer.rs RenderState, vault.rs load_document) + provides: + - LinkRecord struct with line_index/col_offset/span_len/dest/is_wiki + - render_markdown returns (Vec, Vec) + - resolve_wiki_link with case-insensitive multi-strategy matching + - resolve_standard_link for inline link resolution + - is_within_vault path traversal guard + affects: + - 03-02 (consumes LinkRecord and resolution functions for Tab-cycling navigation) +tech_stack: + added: + - "Options::ENABLE_WIKILINKS (pulldown-cmark 0.13.1, already locked)" + patterns: + - "PendingLink/PendingLinkRecord state machine for deferred line_index resolution" + - "flush_line finalizes link records (Pitfall 1: line_index only known at flush time)" + - "canonicalize + starts_with path traversal guard" + - "chars().count() for Unicode-correct column offsets (Pitfall 4)" +key_files: + modified: + - path: src/renderer.rs + role: "Added LinkRecord, PendingLink, PendingLinkRecord; updated render_markdown to return (Vec, Vec); ENABLE_WIKILINKS; link event handling with broken wiki-link detection" + - path: src/vault.rs + role: "Added is_within_vault, resolve_wiki_link, resolve_standard_link" + - path: src/app.rs + role: "Updated handle_resize to destructure (lines, _link_records) from render_markdown" + - path: src/main.rs + role: "Updated initial render call to destructure (lines, _link_records) from render_markdown" +decisions: + - "vault_path: Option<&Path> added to render_markdown for render-time broken wiki-link detection" + - "Broken wiki-links checked at render time (filesystem stat); standard links not checked at render time" + - "link_span_start_count field tracks span index at Tag::Link Start to compute span_len at TagEnd::Link" + - "app.rs and main.rs updated with TODO(03-02) comments at _link_records destructuring sites" + - "ENABLE_WIKILINKS + TextMergeStream: no interaction issues observed (Pitfall 2 non-issue)" +metrics: + duration_seconds: 209 + completed_date: "2026-02-28" + tasks_completed: 2 + tasks_total: 2 + files_modified: 4 +--- + +# Phase 03 Plan 01: Link Record Extraction and Wiki-Link Resolution Summary + +**One-liner:** LinkRecord parallel structure added to render_markdown output with ENABLE_WIKILINKS, case-insensitive wiki-link resolver, and canonicalize-guarded path traversal protection in vault.rs. + +## What Was Built + +### Task 1: Renderer link extraction and styling (commit a63f411) + +Extended `render_markdown()` to return `(Vec>, Vec)` instead of the plain `Vec>` from Phase 2. + +**`LinkRecord` struct** (new, in `src/renderer.rs`): +```rust +pub struct LinkRecord { + pub line_index: usize, // index into Vec + pub col_offset: usize, // chars().count() offset within line + pub span_len: usize, // display length including brackets + pub dest: String, // raw destination (path or wiki target) + pub is_wiki: bool, // true = needs resolve_wiki_link at nav time +} +``` + +**Key design decisions in renderer:** + +- `PendingLink` captures dest, is_wiki, link_style, and col_offset at `Tag::Link` Start +- `PendingLinkRecord` is enqueued at `TagEnd::Link` with span_len computed +- `flush_line()` finalizes pending records with the correct `line_index` (Pitfall 1 avoided — line_index is only known at flush time, not at TagEnd::Link) +- `link_span_start_count` field records span array position at Start so span_len can be summed at End +- `chars().count()` used for col_offset (Pitfall 4: multi-byte Unicode correctness) + +**Link rendering:** +- Wiki-links: `[[Target]]` → `[Target]` in `LightCyan` if resolved, `Red+CROSSED_OUT` if broken +- Standard links: `[text](path.md)` → `[text]` in `LightCyan` +- Text inside links uses the same style (link style overrides style_stack during Text events) + +**Broken wiki-link detection:** `render_markdown` gains a `vault_path: Option<&Path>` parameter. When `is_wiki` and `vault_path` is Some, calls `crate::vault::resolve_wiki_link` at render time. Standard links are never checked at render time (per research recommendation — stat on every link on every render avoided). + +**Caller updates:** +- `app.rs handle_resize`: `let (lines, _link_records) = render_markdown(content, new_width, None)` with `TODO(03-02)` comment +- `main.rs`: `let (lines, _link_records) = render_markdown(&content, initial_width, None)` with `TODO(03-02)` comment + +### Task 2: Vault wiki-link resolution and path traversal guard (commit f2604d6) + +Three new public functions added to `src/vault.rs`: + +**`is_within_vault(vault_path, candidate) -> bool`:** +Canonicalizes both paths and checks `starts_with`. Returns false on any IO error, making it safe to call on non-existent paths. + +**`resolve_wiki_link(vault_path, raw_target) -> Option`:** +- Splits on last `/` to extract optional subdir prefix +- Generates 3 candidate stems (spaces→hyphens, spaces→underscores, literal spaces) — hyphens first per locked decision +- Scans `read_dir(search_dir)` with case-insensitive stem comparison (`.md` stripped, `.to_lowercase()`) +- Guards matched path with `is_within_vault()` + canonicalize before returning +- Returns vault-relative path on success, `None` on no match or traversal detected + +**`resolve_standard_link(vault_path, current_doc, dest) -> Option`:** +- Resolves `dest` relative to `current_doc`'s parent directory within the vault +- Applies `is_within_vault()` guard via canonicalize +- Returns vault-relative path if file exists and is within vault + +## Verification Results + +1. `cargo build` succeeds — zero errors, 4 expected "unused" warnings (Link fields and vault functions will be consumed by Plan 02) +2. `render_markdown` signature change handled: all call sites updated with `(lines, _link_records)` destructuring +3. `ENABLE_WIKILINKS` enabled; `LinkType::WikiLink` matched in event handler +4. `TextMergeStream` + `ENABLE_WIKILINKS` interaction: no issues (Pitfall 2 did not manifest) +5. Path traversal guard: `is_within_vault` uses `canonicalize + starts_with` per security pattern + +## Deviations from Plan + +### Auto-fixed Issues + +None — plan executed exactly as written, with one minor implementation detail: + +**`link_span_start_count` field:** The plan described computing `span_len` from "spans pushed between Start and End". The implementation uses a dedicated `link_span_start_count: usize` field on `RenderState` to snapshot the span array length at `Tag::Link` Start, then computes span_len as the sum of `chars().count()` for `current_spans[link_span_start_count..]` at `TagEnd::Link`. This is a clean implementation of the plan's intent, not a deviation. + +## Self-Check: PASSED + +Files verified: +- `src/renderer.rs`: contains `pub struct LinkRecord`, `ENABLE_WIKILINKS`, `pending_link_records` +- `src/vault.rs`: contains `pub fn resolve_wiki_link`, `pub fn is_within_vault`, `pub fn resolve_standard_link` +- `src/app.rs`: updated with `_link_records` destructuring and `TODO(03-02)` +- `src/main.rs`: updated with `_link_records` destructuring and `TODO(03-02)` + +Commits verified: +- `a63f411`: feat(03-01): extend renderer with LinkRecord extraction and link styling +- `f2604d6`: feat(03-01): add wiki-link resolution and path traversal guard to vault.rs