ADR-0024: Resource wikilink form — resources/ prefix + heading-text anchors

Status: Accepted Date: 2026-05-25

Context

The original citation grammar (CONTEXT.md, pre-ADR-0024) used [[<Class>/<file>#slug-anchor|<slug-anchor>]] — e.g. [[Believers-Responsibility/01-church-growth-begins-with-god#from-street-to-society|from-street-to-society]]. The path dropped the /resources/ segment for brevity, and the anchor was a hand-slugified form of the target heading. The pre-commit hook bridged the path gap by treating any wikilink containing / as resources/<that path>.md.

This convention failed in both renderers the vault publishes through:

  • Quartz (markdownLinkResolution: "shortest") treats slash-containing wikilinks as absolute paths from the content root. With no /Believers-Responsibility/ at the root, Quartz emitted hrefs like /Believers-Responsibility/01-church-growth-begins-with-god#from-street-to-society — 404 in production (theology.menning.cloud).
  • Obsidian matches #fragment against literal heading text (case-insensitive), not against slugified heading IDs. So #from-street-to-society never matched ## From Street to Society; clicks landed on the file but ignored the anchor.

Both bugs were silent — wikilinks looked correct in source, and the pre-commit hook validated only that target files existed, not that anchors resolved.

Decision

Resource wikilinks use the form:

[[resources/<Class>/<path>/<file>#heading-text|Heading Text]]
  • Path includes the literal /resources/ segment that matches on-disk layout. Quartz emits the correct href; Obsidian resolves the same way it always did.
  • Anchor is the exact, verbatim text of the target H1/H2/H3 heading — spaces, capitalization, punctuation, apostrophes preserved. Do not pre-slugify. Obsidian matches heading text; Quartz auto-slugifies the fragment to the heading’s id.

Validation: scripts/pre-commit-hook.py now rejects (a) wikilinks of the old class-prefix-without-resources/ form, and (b) anchored wikilinks whose fragment does not resolve to a heading in the target file (via the shared slugify in scripts/wikilink_utils.py).

Atomization: scripts/process_batch.py per-class citation_form_hint and the in-prompt example atomic both use the new form, so newly-generated atomics conform.

Alternatives considered

  • B. Keep the short convention; add a Quartz transformer plugin that prepends resources/ when the wikilink’s first path segment is a known class. Cheaper migration (only ~626 anchors needed rewriting instead of ~770 path+anchor pairs) and aligned with the pre-commit hook’s existing bridge. Rejected because the bridge is invisible to anyone reading the wikilink — a future contributor or agent must learn the rule from a plugin source file. Self-documenting paths fit the vault’s explicit, AI-navigable philosophy; the one-time migration cost is paid against indefinite onboarding cost. The anchor rewrite was unavoidable in either option.
  • C. Filename-only wikilinks [[01-church-growth-begins-with-god#|…]]. Rejected — loses class disambiguation when two resources share a filename slug.
  • D. Rewrite resource heading IDs to match the old anchor form. Rejected — would destroy heading readability and only fix Quartz, not Obsidian.

Consequences

  • (+) Both renderers work without runtime bridges or special config beyond what Quartz ships out of the box.
  • (+) Wikilinks read top-to-bottom as a path: a contributor can locate the target file from the wikilink alone.
  • (+) The pre-commit hook now catches a class of bug it previously could not see (anchor drift, headings renamed without updating callers).
  • (−) One-time vault migration: 752 wikilinks rewritten across 358 files (see scripts/migrate_wikilink_convention.py). Surfaced 4 pre-existing broken anchors that need source-file fixes (CSG section 4.2 doesn’t exist; BR chapter 04 has a footnote marker leaked into a heading; two atomics cite a nonexistent #intro heading in BR chapter 08).
  • (−) Wikilink strings are longer (one extra path segment). Acceptable cost.
  • (−) Bible-class wikilinks (68 across the vault) still point at an unpopulated resources/Bible/ folder. Pre-existing — surfaced by but not caused by this ADR.

Migration provenance

Implemented and migrated on 2026-05-25. Migration script (scripts/migrate_wikilink_convention.py) is idempotent — re-running on the post-migration vault is a no-op. Kept in-tree as a regression aid and as the canonical example of how to do future bulk wikilink rewrites.