Files
bbs-md/.planning/phases/03-navigation-and-links/03-01-SUMMARY.md
T
ruohki 8ea4545c9b 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
2026-02-28 23:06:30 +01:00

7.3 KiB

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
03-navigation-and-links 01 renderer-vault
links
wiki-links
pulldown-cmark
path-traversal
navigation
requires provides affects
02-vault-core-and-rendering (renderer.rs RenderState, vault.rs load_document)
LinkRecord struct with line_index/col_offset/span_len/dest/is_wiki
render_markdown returns (Vec<Line>, Vec<LinkRecord>)
resolve_wiki_link with case-insensitive multi-strategy matching
resolve_standard_link for inline link resolution
is_within_vault path traversal guard
03-02 (consumes LinkRecord and resolution functions for Tab-cycling navigation)
added patterns
Options::ENABLE_WIKILINKS (pulldown-cmark 0.13.1, already locked)
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)
modified
path role
src/renderer.rs Added LinkRecord, PendingLink, PendingLinkRecord; updated render_markdown to return (Vec<Line>, Vec<LinkRecord>); ENABLE_WIKILINKS; link event handling with broken wiki-link detection
path role
src/vault.rs Added is_within_vault, resolve_wiki_link, resolve_standard_link
path role
src/app.rs Updated handle_resize to destructure (lines, _link_records) from render_markdown
path role
src/main.rs Updated initial render call to destructure (lines, _link_records) from render_markdown
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)
duration_seconds completed_date tasks_completed tasks_total files_modified
209 2026-02-28 2 2 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

Extended render_markdown() to return (Vec<Line<'static>>, Vec<LinkRecord>) instead of the plain Vec<Line<'static>> from Phase 2.

LinkRecord struct (new, in src/renderer.rs):

pub struct LinkRecord {
    pub line_index: usize,   // index into Vec<Line>
    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

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<PathBuf>:

  • 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<PathBuf>:

  • 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