| name | mystmd-case-sensitive-linking |
| description | The load-bearing mystmd v1.10.1 facts and design invariants that keep this project's API cross-references case-sensitive (re.match ≠ re.Match). READ THIS BEFORE modifying anything under src/ (the plugin, inventory writer, {py} role, {apidoc} directive), python/ (extractor), or any objects.inv / anchor logic. Warns about the $-anchor lowercasing foot-gun and the other collapse traps that silently break case-sensitivity. |
mystmd case-sensitive API linking — invariants & foot-guns
The entire project exists to link to both re.match (function) and re.Match
(class) as distinct targets. mystmd normalizes (lowercases) identifiers in
several places; the design threads around all of them. Full evidence:
FINDINGS.md. Do not "simplify" any of the invariants below without re-running
./run_all.sh — each one is load-bearing.
The one rule
API objects are addressed through a case-sensitive, EXPLICIT-anchor inventory
path — never mystmd's lowercased local labels, and never the Sphinx $
shorthand.
Foot-guns (all verified on mystmd v1.10.1)
$-anchor lowercasing (the big one). A Sphinx inventory uri using the $
shorthand (library/re.html#$) is expanded by mystmd with .toLowerCase(),
collapsing re.Match→re.match. Always emit EXPLICIT exact-case anchors
in src/inventory/objectsInv.ts (never #$). Repro:
.venv/bin/python scripts/repro_case_sensitivity.py.
references: is http(s)-only. A local path, absolute path, and file://
all fail to load. To consume a local inventory it must be served over
localhost http (the test harness does this).
- Plugins must be
.mjs. mystmd rejects .js. Keep
outExtension: () => ({ js: '.mjs' }) in tsup.config.ts.
- Local-label anchors get lowercased.
heading identifiers → auto sec-N
(F1); mystTarget/(label)= → lowercased (F2). The ONLY node form that
preserves an exact-case anchor is a div with html_id: 'Exact.Case' —
that is why renderApi.ts wraps each object in one.
- Role links: non-fragment only. A role emitting
{type:'link', url:'#frag'}
is turned into a crossReference and lowercased (collision). Emit a
non-fragment url (page.html#Exact.Case) — mystmd leaves it case-intact.
That is the {py} role.
- No plugin inventory-write API. A transform can't register inventory
entries. We generate
objects.inv ourselves (Option D) and use the {py}
role for intra-project refs (Option B). Options A/C are dead ends
(documented in ISSUE.md).
Where each invariant lives
| Concern | File |
|---|
| explicit-anchor inventory writer | src/inventory/objectsInv.ts |
div html_id anchors + rendering | src/render/renderApi.ts |
{py} role (non-fragment links), {apidoc} directive, inventory transform | src/index.ts |
IR (ApiItem/ApiIndex/Extractor) | src/ir.ts |
| extractors (stub double + griffe + warm sidecar) | src/extractor/, python/ |
Vendored inventory
fixtures/inv/python.objects.inv is regenerated by
scripts/make_python_inv.py — it stores explicit anchors (not the real
CPython $ form) precisely so mystmd resolves it case-sensitively.