feat(links): broken-anchor detection in find --broken-links (iter-190, L-21) - #224
Merged
Conversation
…, L-21) Carry link `#fragment` anchors through parsing and resolution and report a distinct broken-anchor category in `find --broken-links` — a link whose target file exists but whose `#heading` does not — never conflated with a broken target and never inflating `links fix` counts. - Add backward-compatible `fragment: Option<String>` to `Link` (serde default); parse side captures the fragment without `#`, rewrite span still stops before `#` so mv / links fix preserve fragments byte-exact. - New `hyalo-core/src/anchor.rs` exact-heading matcher: case-insensitive Obsidian convention, percent-decodes markdown fragments for matching, skips `^block-id` refs. Validates against already-indexed/scanned sections — zero extra file reads (A/B: within noise on both disk and --index paths). - `LinkInfo` gains `fragment` + `broken_anchor` (skipped from JSON when absent); unified text filter distinguishes broken-target `(unresolved)` from broken anchor `(broken anchor)`. - HYALO006 stays target-only (DEC-061); anchors surface only in find for now. - DEC-060 (match convention + shape backward-compat deviation) and DEC-061 recorded; CHANGELOG / README / HYALO006 desc / rule-knowledgebase template updated; iter-188 + review L-21 disposition flipped to resolved. - 9 e2e (anchors.rs): matrix disk+index parity, links-fix count guard, backlinks, single + batch mv fragment preservation, rebuilt-index; plus anchor.rs unit tests and links.rs span/parse round-trip tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… gate Soften the task-4 perf AC to name the zero-file-read code path (index.get(target_path).sections / fragment_matches_headings in find/mod.rs) so ac-fidelity-check.sh can verify it against the diff alongside the hyperfine A/B numbers, which aren't diff-verifiable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lands L-21 as one atomic unit: link
#fragmentanchors are now carried through parsing and resolution, andfind --broken-linksreports a broken anchor — a link whose target file exists but whose#headingdoes not — as a category distinct from a broken target.Link.fragmentadded as a backward-compatibleOption<String>(#[serde(default, skip_serializing_if)]). Parse side captures the fragment without#; the rewrite span still stops before#, somv/links fixpreserve fragments byte-exact. Under this repo'sto_vec_namedframing the field is additive — old.hyalo-indexsnapshots decode withfragment: None(fail-safe, no false anchor reports); rebuild withcreate-indexto pick up anchor data. Recorded as a DEC-060 deviation from the plan's hard-break premise (verified empirically).hyalo-core/src/anchor.rsmatcher: exact, case-insensitive (Obsidian[[Foo#tasks]]→## Tasks), percent-decodes markdown fragments for matching only, skips^block-idrefs. Validates against already-indexed / already-scannedsections— zero extra file reads (A/B: within noise on both disk and--indexpaths).find --broken-links:LinkInfogainsfragment+broken_anchor(skipped from JSON when absent); the two categories are never both set on one link (broken target skips the anchor check). Unified text filter renders(broken anchor)distinctly from(unresolved).links fixunchanged: still target-only, so broken anchors never inflatebroken/fixableor the "Apply N fixes" hint.findbefore any lint/CI gate. DEC-060/DEC-061 recorded; CHANGELOG, README, HYALO006 description, and the rule-knowledgebase template updated; iter-188 + review L-21 dispositions flipped to resolved.Test plan
cargo fmt --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace -qall greenxtask gates: check-ac-fidelity (plan), check-feature-fanout, check-help-drift, check-bundled-skills all exit 0
anchor.rs: 12 unit tests (exact/trim/case/multiple/no-match/heading:None/unicode/percent-encoded/^block)anchors.rs: 9 e2e — matrix disk +--indexparity,links fixcount guard, backlinks preservation, single + batchmvfragment preservation, rebuilt-indexlinks.rs: span/parse fragment round-trip + span-untouched invariant testsA/B perf recorded in the iteration plan (disk 30.3→30.1 ms, index 29.7→30.2 ms — within noise)## Claims vs code
<generated 2026-07-19T18:53:34Z by ralph-loop>
target-only→ ❌ no match in diff1 claim(s) had no matching evidence. Add the code, soften the
commit message, or annotate with
// allow-claim-miss: <symbol>and a reason.