`[[Page#Heading text]]` parsed the anchor as the literal string after
`#`, but the index stored each heading's anchor as `slugify(title)`.
The two never compared equal, so every TOC-style anchor link
(`[[#1. Topologia atual]]`, etc.) was reported as a broken anchor and
goto-definition couldn't jump to the right heading either.
Slugify the request before comparing — `slugify` is idempotent on
valid-slug input, so the same code path now accepts both
`[[Page#some-heading]]` (slug form) and `[[Page#Some Heading]]`
(raw text). Applied to diagnostics, navigation, and hover preview so
all three agree on what "an anchor matches a heading" means.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`[[page.wiki]]` should resolve identically to `[[page]]`. The index
keyed by stem, so links with the explicit extension fell through to
the broken-link diagnostic and to-page navigation failed. Strip the
configured extension before every page lookup (resolve, definition,
backlinks, classify_link, classify_outgoing) and stop appending the
extension in synthesise_page_uri when it's already present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New diagnostics module hosts the `nuwiki.link` source: walks every
`WikiLinkNode` and emits a diagnostic per broken target. Severity is
gated by `cfg.link_severity` (off | hint | warn | error). Wired into
`collect_diagnostics` so didOpen/didChange publish link diagnostics
alongside parse errors.
Classification (`BrokenLinkKind`):
- `Wiki` target missing from the workspace index → MissingPage.
- `Wiki`/`AnchorOnly` target with an anchor that matches neither a
heading nor a `:tag:` on the resolved page → MissingAnchor.
- `file:` / `local:` target whose resolved path isn't on disk →
MissingFile. Absolute paths are used as-is; relative paths resolve
against the source URI's parent directory, falling back to the wiki
root.
- Interwiki/Diary/Raw/external — never diagnosed.
`classify_link` works off a `&WikiLinkNode`; `classify_outgoing` does
the same job from a cached `IndexedPage.outgoing` entry so the
workspace-wide checker doesn't need to re-parse every page.
New commands:
- `nuwiki.toc.generate` — generate `= Contents =` heading + nested
list of `[[#anchor|Title]]` entries from current headings. Replaces
any existing h1 "Contents" + its immediate following list
case-insensitively; inserts at line 0 otherwise. Skips the TOC's
own heading so re-generation is idempotent.
- `nuwiki.links.generate` — equivalent of `:VimwikiGenerateLinks`.
Flat alphabetical list under `= Generated Links =`, excludes the
current page.
- `nuwiki.workspace.checkLinks` — returns `Vec<BrokenLinkEntry>` with
`{ uri, range, kind, message }` per broken link across the wiki.
Open documents use live text for range conversion; closed pages
fall back to the stored span coords. Accepts an optional `uri` to
pick a specific wiki; defaults to the first registered one.
- `nuwiki.workspace.findOrphans` — returns `Vec<{ uri, name }>` for
every indexed page with no incoming links. Sorted alphabetically.
Pure ops (`commands::ops`):
- `build_toc_text(items, heading_name)` — formats the TOC body with
2-space indent per level, anchors via `index::slugify`.
- `build_links_text(pages, heading_name, exclude)` — formats the flat
list.
- `find_section_range(ast, heading_name)` — locates an existing h1
section (heading + immediate following list) by case-insensitive
title match.
- `toc_edit` / `links_edit` — full edit producers.
- `collect_workspace_broken_links` / `find_orphans`.
Plumbing:
- `collect_diagnostics` gains a `uri: Option<&Url>` parameter so
link-health can resolve `file:` / `local:` relatives. `ast_diagnostics`
back-compat wrapper preserved.
- `Backend::default_wiki` is no longer `#[allow(dead_code)]`.
Tests: 31 new in `phase15_link_health.rs` covering severity mapping,
classification on every kind/anchor case, TOC nesting, links exclusion,
section-range case-insensitivity, idempotent re-generation, orphan
detection, and the COMMANDS list. The Phase 11 stub test was rewritten
into a real "no index = no diagnostics" assertion. All 274 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>