[0.20.0] - 2026-07-19
Added
-
Broken-anchor detection in
find --broken-links(iter-190, L-21): links
now carry their#fragment(heading anchor) through parsing and resolution.
find --broken-linksreports a broken anchor — a link whose target file
exists but whose#headingdoes not — as a category distinct from a broken
target. In JSON, an anchored link gainsfragmentand (when the heading is
missing)broken_anchor: true; text output renders"Foo#Real" → "Foo.md"
and marks a missing heading as(broken anchor). The two categories are
never both reported on one link (a broken target skips the anchor check), and
broken anchors do not inflatelinks fix'sbroken/fixablecounts or
its "Apply N fixes" hint —links fixstays target-only, letting anchor
semantics soak one release behindfindbefore any lint/CI gate consumes
them. Anchor matching is exact and case-insensitive (Obsidian convention:
[[Foo#tasks]]matches## Tasks), decodes percent-encoded markdown
fragments (foo.md#my%20heading) for comparison only (the written form is
preserved), and skips^block-idrefs (block ids are not indexed). Validation
reads headings from the already-materialized index/scan sections — zero
extra file reads on the--indexpath, and no per-file re-read on disk scan.
mvandlinks fixpreserve fragments byte-exact (the rewrite span stops
before#).The
Linkwire-shape gained an additivefragment: Option<String>field
(#[serde(default)]), serialized into.hyalo-indexentries and the
persisted link graph. The field is backward compatible: existing
.hyalo-indexsnapshots load unchanged (fragments read asNone, so no false
anchor reports from stale entries). To pick up fragment data for anchor
validation on the--indexpath, rebuild the index withhyalo create-indexafter upgrading. -
HYALO006/broken-linklint rule (iter-188):hyalo lintnow flags
wikilinks and markdown links that point at a vault file which does not exist.
Enabled andwarnby default;hyalo lint --strictpromotes it to an error
so CI can gate broken links. The vault-wide resolution context (case/stem
index) is built once per invocation — from the.hyalo-indexsnapshot
when--indexis active, else a single vault walk — and shared across
workers, so the rule adds no per-file graph rebuild. Respects
[lint.rules.HYALO006]enable/severity overrides,--rule HYALO006/
--rule-prefix HYALO, and--files-from(resolution stays vault-wide even
when the linted set is scoped, so a scoped file linking to an
unscoped-but-existing file does not false-positive). -
Honest partial-failure envelopes for link write paths (iter-187): when a
file write fails mid-batch,hyalo links fix --apply,hyalo links auto --apply, and batchhyalo mv --applynow emit a complete JSON envelope
rather than aborting with a bare error.links fix --applygainsfailed/
failed_fixesbuckets (each with the per-file error string);links auto --applygainsfiles_applied/files_skipped/files_failedcounts plus
a per-fileapply_outcomeslist (applied/skipped/failed with reason, so skips
that previously only went to stderr are now in the envelope). Any partial
failure yields a non-zero exit code. Files written before the failure are
reported as applied, never silently kept and unreported.
Fixed
find --orphan/--dead-end/--fields backlinkscount
case-insensitive inbound links (L-6 tail):[[foo]]pointing atFoo.md
now counts as inbound infind, matching thebacklinkscommand and
summary(all three route through the same case-insensitive graph lookup).
Previouslyfind --orphancould list a file thatsummaryandbacklinks
agreed had inbound links.- Percent-encoded markdown link destinations now resolve (iter-188, L-23):
[x](my%20dest.md)previously never resolved (the%20was compared
literally against the on-disk filenamemy dest.md), sofind --broken-links
false-positived andbacklinks "my dest.md"missed the linker. The path
portion is now percent-decoded during resolution and in the link graph, so
encoded and angle-bracket ([x](<my dest.md>)) forms resolve to the same
file. Malformed (%2,%zz) or non-UTF-8 (%FF) escapes keep the literal
text — a filename with a stray%still resolves as written. Rewrite keeps
the destination as-authored (the%20form is preserved onmv). - Batch
mv --applyno longer leaves dangling links after a rolled-back
rename (PR #221 review): when a mid-batch write failure rolled back file
renames, a "self-rewrite" plan — one whose rewritten content was written to
a file's own new (renamed) location, e.g. a moved file's outbound link
rewrite — was previously left in place even though its rename was undone,
stranding the file at its old path with content referencing the (now
reverted) new layout. Such plans are now identified bypathcoinciding
with one of the batch's own rename destinations, and their pre-batch
content is restored alongside the rename rollback. Plans on files outside
the rename set (pure external linker files) still keep the original
DEC-056 behavior of being kept and honestly reported. hyalo links fix --applyno longer aborts the whole batch on a per-file
I/O error (PR #221 review): astat/read failure for one source file
(e.g. deleted between detection and apply) now lands that file's fixes in
thefailed/failed_fixesenvelope and the remaining files in the batch
still get their fixes applied, instead of propagating the error and losing
all progress.hyalo summaryorphan/dead-end counts are now case-insensitive (iter-189,
L-6): inbound-link membership for orphan/dead-end classification went through
a case-sensitive target-set check, so on a case-insensitively-written vault a
fileFoo.mdlinked only as[[foo]]was miscounted as an orphan even though
hyalo backlinks Foo.mdfound the linker. Inbound membership now uses the
lower_index-backedbacklinks_cilookup (the same onebacklinksuses), so
such a file is correctly reported as a dead-end and orphan counts agree with
the backlink view. Outbound membership is unchanged (on-disk paths compared
against on-disk paths — no case divergence). Note:find --orphan/
find --dead-endstill compute inbound via the case-sensitivebacklinks
path; aligning them is a documented follow-up (see iter-189) so this release
ships exactly one observable orphan/dead-end change.
Changed
-
hyalo links fixdry-run validates plans against on-disk text (iter-187):
dry-run now runs the identical plan-building phase as--apply, so its
unapplied/unapplied_fixesfields report exactly the fixes--apply
would refuse (stale index / concurrent edit) instead of always being empty.
The "Apply N fixes" hint count now discounts would-be-stale fixes so it
matches what--applyactually writes. -
Classify-side link resolution collapsed onto the shared resolver (iter-189,
refactor only): thelinks fixverdict logic (resolve_and_classify_link,
classify_link,classify_short_form_wikilink, plus theLinkResolution/
StemIndextypes) moved out oflink_fix.rsintodiscovery.rsas
classify_link_from_source— the Classify-mode sibling of the Exists-mode
resolve_link_from_source. Both now route their kind-dependent normalization
through one privatenormalize_link_targethelper, so Exists ("does this link
resolve?") and Classify ("full fix-policy verdict") can no longer drift on the
wikilink/markdown/site-absolute/bare-basename branching. The test-only
detect_broken_links(&[FileLinks])twin was deleted and its five unit tests
ported ontodetect_broken_links_from_index. No user-visible behavior change
(locked by e2e capturing thebroken/case_mismatches/ambiguousbuckets). -
Shared link-existence resolver entry point (iter-188, task 0): the
"does this link exist?" resolution thatfind --broken-links/
find --orphan/find --dead-endand the new HYALO006 rule both need is now
a singlediscovery::resolve_link_from_sourcefunction. It owns the
kind-dependent normalization (wikilink vault-relative, markdown
site-absolute / path-qualified / bare-basename) and the final
resolve_targetcall, sofind/mod.rsno longer inlines that branching and
the lint rule does not reimplement it. -
Unified link write path (iter-187):
auto_linknow builds
RewritePlans and writes through the sharedexecute_plans_partial
machinery instead of a hand-rolled line splitter (removed
split_lines_preserving_endings), keeping its stronger full-content TOCTOU
guard. Batchmvreports which link rewrites were durably applied before a
mid-batch abort (DEC-056: completed content writes on untouched linker files
are not rolled back; the renames are, along with the content of any
self-rewrite plan whose path coincided with a rename destination).
What's Changed
Other Changes
- feat(links): honest partial-failure envelopes + dry-run parity + unified auto write path (iter-187) by @ractive in #221
- iter-188: HYALO006 broken-link rule + L-19/L-23 link semantics & review close-out by @ractive in #222
- refactor(links): collapse Classify-side resolution onto shared discovery entry point (iter-189) by @ractive in #223
- feat(links): broken-anchor detection in find --broken-links (iter-190, L-21) by @ractive in #224
- fix(output): broken-anchor marker missing from find text output by @ractive in #225
- fix(find): case-insensitive inbound for --orphan/--dead-end (L-6 tail) by @ractive in #226
Full Changelog: v0.19.0...v0.20.0