5 added, 0 removed. Audit A to A.
<!-- hyalo:start -->
Use `hyalo` CLI (not Read/Grep/Glob) for all markdown knowledgebase operations.
Examples: `hyalo find --property status=planned`, `hyalo find "search text"`, `hyalo lint` (add `--strict` to fail on missing-type / undeclared-property warnings), `hyalo types list`.
Run `hyalo --help` for usage. Output format auto-detects (text on terminals, json when piped); pass `--format text`/`--format json` to override.
Use `hyalo config` to inspect the effective configuration (effective dir, config path, hints, format, site_prefix) — useful when debugging `.hyalo.toml` resolution. Add `--raw` to also print the file's text. When a config exists but does not parse, `results.malformed` is true and `results.parse_error` carries the diagnostic; every other value shown is a built-in default. `.hyalo.toml` is found in the working directory or in the nearest ancestor whose configured vault contains it, so running from inside the vault keeps the config in effect. `--dir` selects a vault, not a config: naming the configured vault keeps `.hyalo.toml` in effect, naming another tree switches to that tree's config (announced on stderr). Its JSON is the standard `results`/`hints` envelope, so `hyalo config --jq '.results.dir'` works.
`[scan] exclude = ["Templates/**"]` hides matching files from *every* command (reported as `results.scan.exclude`); naming an excluded file explicitly is refused with the glob. A config that does not parse now exits 1 for `lint`, `find --strict` and `views run` — their exit code is a gate — while other reads keep answering with a `-q`-proof warning.
Every link in `--fields links` carries a `kind`: `wikilink` | `embed` (`![[…]]`) | `markdown` | `frontmatter` (a `[[wikilink]]` in a YAML frontmatter value) | `external` (any `scheme:` URI — `https:`, `obsidian://`, `mailto:`, `file://`) | `attachment` (resolved to a non-`.md` vault file: an image, a PDF, an Obsidian `.base`). `external` and `attachment` links are never broken and are not graph edges, so list the genuinely broken ones with `hyalo find --broken-links --jq '.results[] as $f | $f.links[] | select(((.kind == "external" or .kind == "attachment") | not) and ((.path == null and (.out_of_vault | not)) or .broken_anchor)) | "\($f.file):\(.line) \(.target)#\(.fragment // "")"'`. Resolution folds case on every platform (DEC-267), and a broken `#anchor` that prefixes exactly one heading — the whole heading included — carries `suggested_fragment` with the full heading text (DEC-268).
Anchor **resolution** folds `-`, `_` and a space into one word separator (DEC-309), so `#Browser_compatibility` resolves to `## Browser compatibility` (MDN's broken anchors: 10 929 → 529); a nested heading path `[[note#H1#H2]]` resolves by walking the outline, so the second segment must sit under the first (DEC-311). A wikilink target is trimmed and `.` segments are dropped before resolution (DEC-310): `[[ a ]]`, `[[a #H]]` and `[[./a]]` all report `path: "a.md"` while `target` keeps what was written.
A bare `[[alias]]` naming a note's frontmatter `aliases:` is **broken** by default (DEC-308, amends DEC-296) — Obsidian does not resolve one; its suggester writes `[[Note|alias]]`. `[links] aliases` defaults to `false`; the link still carries `via: "alias"`, and `links fix` plans that exact rewrite in an `alias_fixes` bucket (strategy `Alias`, confidence 1.0, plain `--apply`, never fuzzy). `[links] aliases = true` restores iteration 272's resolution. In either mode an ambiguous stem is never tie-broken by an alias, and an alias claimed by two notes is ambiguous — `links fix` lists the `candidates` and HYALO006 says "ambiguous wikilink … matches 2 candidates".
`mv` lists a link it cannot attribute rather than guessing: a bare `[[stem]]` two files share lands in `skipped_ambiguous` with its `candidates`, for a frontmatter value (with `property`) as for body prose, in every directory layout, and for the moved file's own body self-links (`self: true`); `--allow-ambiguous` rewrites them all. Destinations resolve like sources — an absolute in-vault path is accepted, and `--to .` / `--to ./` / `--to <vault-dir>/` all mean the vault root. A batch dry run lists `collisions: [{source, destination}]` and plans the rest; `--apply` refuses.
A `[[wikilink]]` in **any** frontmatter value is a graph edge (DEC-269) — `categories: ["[[Books]]"]`, `type: "[[Author]]"`, a nested map — counting for `backlinks`, `--orphan`/`--dead-end`/`--broken-links`, `summary.links` and HYALO006, reported with `kind: "frontmatter"`, the `property` it came from and its frontmatter line, and rewritten in place by `mv`. Set `[links] frontmatter = false` in `.hyalo.toml` to narrow the scan back to `related`/`depends-on`/`supersedes`/`superseded-by` (`hyalo config --jq '.results.links'` reports the effective value).
`set K=<scalar>` on a property holding a list replaces it and says so on stderr, listing the files under `list_collapsed` (DEC-270) — use `hyalo append` to keep the list.
`title` is promoted in three steps — a scalar frontmatter `title`, else the first H1, else the **filename stem** (DEC-283) — and `title_source` (`property` | `h1` | `filename`) says which answered. `--title`, `--property 'title~=…'` and `--sort title` all read the promoted value; test the raw key with `--property title` / `--property '!title'`.
A path named with `--file`, positionally, or via `--files-from` is linted even when `[lint] ignore` matches it (DEC-284), so `git diff --name-only | hyalo lint --files-from -` checks what you touched; select paths with `--glob` when you want the ignore list applied.
`hyalo new --dry-run` prints the scaffold without writing. Its placeholders are deliberately un-fillable: a required `string` gets `TBD`, a required `number`/`date`/`datetime`/`boolean` with no schema default is written **empty** so `hyalo lint` reports it (DEC-285).
`links auto` holds back candidate titles that are common words, generic doc filenames or run-dominating, reporting them as `results.default_excluded_titles` (DEC-286); `[links.auto] exclude_titles` replaces that list and `--no-warn-common-titles` switches it off.
+ A bulk write phase — `lint --fix`, `mv`, `links fix --apply`, `set`/`append`/`remove --glob`, `properties rename`, `tags rename` — still replaces every file atomically but pays the durability fsync once per touched directory instead of once per file (DEC-317); the Obsidian Hub's `lint --fix` went from 48.1 s to 2.35 s. Past a few hundred files it reports progress on stderr; `-q` silences it. A phase of 8 files or fewer, and every single-file mutation, keep the full per-file guarantee.
+ `summary.orphans`/`summary.dead_ends` and `find --orphan`/`find --dead-end` share one edge predicate (DEC-318): an external URI, a same-file anchor and any target with an explicit non-`.md` extension are not note-graph edges — a *broken* `![[missing.png]]` included, which is still a broken link and still reported by `find --broken-links`.
+ `links fix` damps a contested fuzzy winner below the apply floor rather than writing it (DEC-319), reports `broken_anchors` as the count `find --broken-links` computes (never a hard-coded 0), carries `emitted_target` on every `fuzzy_fixes` entry including below-floor ones, and says "derived from the directory name" when the prefix that stripped nothing was never configured. MDN's `_colon_`/`_star_` slug encoding stays unresolved (DEC-320) and there is no basename fallback into `**/x/index.md` (DEC-321).
+ Hints thread `--site-prefix` when it came from the CLI, exactly as `--dir`, `--format` and `--index-file` are; `find --broken-links --format text` prints only each file's broken links (JSON keeps the full inventory); `<https://…>` / `<obsidian://…>` autolinks are inventoried as `external`; `K!=V` tests a value, so a file lacking K does not match — use `!K`; and `summary --index` reports the same `skipped` count as a disk scan (snapshot format v2 — rebuild an older index).
+ Link-kind histogram: `hyalo find --fields links --jq '[.results[].links[].kind] | group_by(.) | map({kind: .[0], n: length})'`. Missing images: `hyalo find --fields links --jq '.results[] as $f | $f.links[] | select(.kind == "embed" and .path == null) | "\($f.file):\($f.line) \(.target)"'`.
Frontmatter closes only on a **column-0** `---` (trailing whitespace allowed): an indented ` ---` inside a block scalar is content, and a block that never closes at column 0 is reported as unclosed / `HYALO005` instead of being silently truncated (DEC-293). `set`/`append` also never emit a block scalar containing a `---`/`...` line — such a value is written double-quoted and round-trips. `properties rename --from ''`/`--to ''` exits 1.
`lint` does not fire a prose rule inside a fenced or indented code block or an HTML comment; MD010, MD031, MD040, MD046, MD047 and MD048 keep checking them on purpose, and MD031 stays quiet at the opener of an unterminated fence. Silence a region with markdownlint's own comments — `<!-- markdownlint-disable no-hard-tabs -->` … `<!-- markdownlint-enable … -->`, plus `-disable-line`, `-disable-next-line`, `-disable-file`, `-enable-file` — taking rule ids or aliases (DEC-294).
`links fix` produces no case-mismatch plan for a site-absolute link carrying the configured `site_prefix` (DEC-295) and never appends `/index` or `.md` to a link form that lacked it; `mv` applies its ambiguity guard to frontmatter links too, reporting each skip with the `property` it came from.
A path you *name* is a promise (DEC-301): `find --file`/positional on a note whose frontmatter will not parse exits 1 with the diagnostic (a `--files-from` list still counts it and exits 0); `find --index --file` reads a note the snapshot has never seen from disk; `--file`/`--glob` keep `broken_anchor` and `suggested_fragment`, so all four ways of selecting one file return identical link JSON; and `lint --rule X` reports rule X only — a parse error is HYALO005's finding, otherwise a counted skip.
The stale-index warning now names the file that drifted (DEC-302): when the directory-mtime probe is clean, each indexed file's mtime is compared against disk, so an in-place overwrite is no longer served silently (~0.03 s over MDN's 14,375 files; same-second edits remain invisible). A snapshot also records what `[scan] exclude` dropped when it was built, so `summary --index` and `summary` agree on `excluded` (DEC-303).
`mv` resolves the destination exactly like the source (DEC-304): with `dir = "kb"`, `hyalo mv kb/a.md kb/sub/a.md` from the project root lands at `kb/sub/a.md`, never `kb/kb/sub/a.md`, in all four destination forms. `--on-conflict` is `error` | `skip` and nothing else (DEC-305), honoured in single-file mode too; batch `mv` sweeps for split frontmatter links once per batch and reports `moves[].frontmatter_links_skipped` (DEC-306).
Exit codes are 0 / 1 / 2 (DEC-307): 0 the command answered (a dry run and a zero-result query both answered — drift lives in the payload, e.g. `okf index`'s `results.changed`, not in the exit code); 1 every hyalo-own user error, always rendered through the JSON error envelope (bad `--sort`, unparseable `--glob`, unreadable `--files-from`, unknown `init --profile`, `create-index --output` into a missing directory, `find a b`, `deinit --dir <nonexistent>`); 2 clap usage errors and internal errors.
`--property` rejects an empty operand (`K=`, `K>=`, `K>`), a second `=` (`a=b=c`) and an empty name (`=b`) with exit 1 instead of matching nothing — use `K`, `K=null`, `!K`, or `K~=/a=b/` for a literal `=`. A zero-result `find` that filtered on a key the vault *does* carry now says so and lists the values with counts, rather than claiming no file has the property.
`hyalo lint --rule SCHEMA` / `--rule-prefix SCHEMA` runs the frontmatter/schema pass alone, and `lint-rules list`/`show` carry a non-configurable `SCHEMA` row; `lint --fix` JSON reports `rules_fixed: {rule: n}`. An empty required typed placeholder reports one error, not two. An empty `--files-from` list warns (`-q`-proof) because "no input" must not look like "no findings".
`--sort title` collates: case-folded, leading punctuation skipped, and an H1-derived title has its `<!-- … -->` comments stripped. Hints on an indexed run carry `--index` / `--index-file` wherever the hinted command accepts it. `hyalo config` reports `links.case_insensitive`; `false` disables hyalo's case-folding index only — the literal path probe is still the filesystem's, so exact-match resolution is guaranteed only on a case-sensitive volume.
`markdownlint-disable-next-line` protects the line *after* the comment and never its own — a trailing directive no longer makes a heading invisible to prose rules — and no autofix may insert a blank line between the directive and the line it guards; fenced blocks indented inside list items (including under `1.` and inside a blockquote) are code, so no prose rule fires there; an unknown rule id in a suppression comment warns (`-q`-proof); `--max-per-rule 0` means unlimited. MD010's `code_blocks` stays unexposed — use `<!-- markdownlint-disable no-hard-tabs -->` (DEC-316).
`check-jq-recipes` **fails** on a shipped mutating recipe without `--dry-run`, so every mutating example in `skill-hyalo.md`, `rule-knowledgebase.md`, `pi-package/` and this file is safe to paste. `--jq` computes **no** hints, so `.hints` under a filter is always `[]` (DEC-313) — read hints from plain `--format json`.
An unknown key under `[schema]`, `[schema.types.<t>]`, or the wrong nesting `[schema.<t>]`, is refused with the same "unknown field" diagnostic `[scan]` gives; `hyalo config` reports `malformed: true` plus `schema_error`, and every validating command refuses per DEC-290. `required = ["title"]` means present and non-empty, **not** `string` (DEC-312): `title: 2024` passes — declare `type = "string"` to require one. `set` has no YAML-null form (DEC-314); `set --help` carries the full coercion table.
The snapshot header carries a format version: an index written by an older binary is refused with a warning naming both versions and the run falls back to disk. `hyalo config` reports `snapshot_format_version` (what this binary writes), `summary --index` reports the snapshot's `index_format_version`. `--index-file <unreadable>` exits 1 with an envelope — a named path is a promise — while a missing in-vault `.hyalo-index` under bare `--index` still falls back, with a `-q`-proof warning. The stale-index blind spot is up to ~2 s (whole-second mtimes plus a one-second tolerance).
A bare path is always vault-relative; when the CWD is inside the vault and a file of that name exists in both places, hyalo warns (`-q`-proof) naming both candidates and the one it used. `mv --to ../deep/` says "path contains `..`" like the source check. `[links] case_insensitive = "false"` means exact bytes and reports the link's own spelling, not the canonical one (DEC-315) — `hyalo config` says so on a case-folding filesystem.
`--- ` (trailing whitespace) opens frontmatter, matching the closer and YAML (DEC-293 amended), and both fences round-trip byte-identically through `set`/`append`/`remove`. A JSON-mode write against an unparsable file is one envelope carrying the YAML diagnostic in `cause`. `tags rename` keeps a flow-style `tags: [a, b]` flow. `1. [ ]`, `2) [x]` and `- [ ]` are tasks. Bulk `set`/`append`/`remove` carry `skipped_detail` — `{file, reason}` with `unchanged` or `unparsable` — so a same-value write is distinguishable from a refusal.
<!-- hyalo:end -->