Skip to content

fix(wiki): skip uppercase markdown suffixes so a .MD file cannot break every query - #66

Open
Hotragn wants to merge 2 commits into
AlmanacCode:mainfrom
Hotragn:fix/case-insensitive-md-glob
Open

fix(wiki): skip uppercase markdown suffixes so a .MD file cannot break every query#66
Hotragn wants to merge 2 commits into
AlmanacCode:mainfrom
Hotragn:fix/case-insensitive-md-glob

Conversation

@Hotragn

@Hotragn Hotragn commented Aug 19, 2026

Copy link
Copy Markdown

Summary

  • iter_page_paths globs rglob("*.md"), which is case-insensitive on Windows and on default macOS volumes, so it also matches NOTES.MD and Mixed.Md.
  • page_id_for_path then compares the suffix exactly and raises, and load_page_document calls it unguarded — so one uppercase-suffixed file takes down every command that reindexes.
  • The producer now yields only exact .md suffixes, so all three platforms derive the same page set from the same wiki tree.

Why

This reproduces on macOS, the one platform the project currently supports. APFS is case-insensitive by default, so the glob matches a file the validator will reject.

End-to-end repro — a wiki containing a single NOTES.MD:

$ ls almanac/
NOTES.MD   README.md   topics.yaml

$ codealmanac search notes
codealmanac: wiki page must be markdown: ...\almanac\NOTES.MD

The failure path is load_page_documentpage_id_for_path (raises ValidationFailed) → load_documentsload_index_sources → the implicit reindex that every query command performs. So search, show, health and reindex all fail, not just one command. load_documents has a document is Nonefiles_skipped path, but an exception bypasses it.

Linux never matched these files, which is why CI is green — this is the same Ubuntu-only CI blind spot that hides the launchd behaviour.

Verification

# Windows 11, Python 3.13 (uv-managed), uv 0.12.0
uv run pytest tests/test_wiki_parsing.py tests/test_read_model.py \
              tests/test_validate.py tests/test_topics_health.py -q
# all pass

uv run pytest -q          # 13 failed, 552 passed  (was 13 failed, 550 passed)
uv run ruff check .       # All checks passed!
git diff --check          # clean

The two added tests are net-new coverage; this fix does not flip an existing failing test, which is precisely why the bug survived — nothing exercised a non-.md suffix.

Docs and wiki

  • Not applicable — restores intended behaviour; no user-facing surface change.

Notes for reviewers

  • Fixed at the producer, not the callers. iter_page_paths has four callers (index/sources.py, health/sources.py, wiki/frontmatter_rewrite.py), and all three subsystems inherit the fix. Filtering in each caller would be the same rule written three times.
  • No-op on Linux. rglob("*.md") never matched .MD there, so the page set is unchanged; this only brings Windows and macOS in line with it.
  • test_page_ids_resolve_for_every_iterated_page is the durable guard. It asserts the producer/validator agreement directly rather than hard-coding one bad suffix, so it also catches any future divergence between the two.
  • Deliberately not decided here: whether .MD should be accepted as a page rather than skipped. That changes indexing semantics and is a product call — the existing non-negotiable that "slugs are kebab-case of the filename" suggests you may well want canonicalization instead. I kept the Linux behaviour (ignore) so this stays a pure bug fix; happy to send the canonicalizing version if you prefer it.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…k every query

`iter_page_paths` globs `rglob("*.md")`, which is case-insensitive on Windows and
on default macOS volumes, so it also matches "NOTES.MD" and "Mixed.Md".
`page_id_for_path` then compares the suffix exactly and raises
`ValidationFailed("wiki page must be markdown")`.

`load_page_document` calls `page_id_for_path` unguarded, so the error propagates
through `load_documents` -> `load_index_sources` -> the implicit reindex that
every query command performs. One uppercase-suffixed file anywhere under
`almanac/` therefore takes down `search`, `show`, `health` and `reindex`:

    $ codealmanac search notes
    codealmanac: wiki page must be markdown: ...\almanac\NOTES.MD

This is not Windows-only. macOS is case-insensitive by default, so it reproduces
on the one platform the project currently supports. Linux never matched these
files, which is why CI is green.

The producer now yields only exact ".md" suffixes, so all three platforms derive
the same page set from the same wiki tree. Fixing `iter_page_paths` rather than
each caller covers the index, health and frontmatter-rewrite paths at once.

Whether ".MD" should instead be *accepted* as a page is a product call, not a bug
fix, so this change keeps the Linux behaviour of ignoring it. Happy to follow up
if you would rather canonicalize the suffix the way slugs already are.
The two tests added in the previous commit only reach the new filter on a
case-insensitive filesystem. On Linux `rglob("*.md")` never returns "NOTES.MD",
so both assertions held without the guard ever executing — meaning the fix was
untested on the only platform CI runs.

Replaces the weaker of the two with one that feeds the case-insensitive match in
directly, so it exercises the filter regardless of the host filesystem. Verified
by removing the guard: the new test fails, and it fails for the right reason
rather than because of the platform it happens to run on.
@Hotragn

Hotragn commented Aug 19, 2026

Copy link
Copy Markdown
Author

Pushed 4474b05 to fix a real weakness in my own tests, before anyone spends review time on them.

Both tests I originally added only reach the new filter on a case-insensitive filesystem. On Linux, rglob("*.md") never returns NOTES.MD, so the loop only ever saw wiki.md and both assertions passed without the guard executing at all. Since CI is ubuntu-latest only, that meant the fix was effectively untested on the one platform you can actually run — the tests described the behaviour instead of pinning it.

The replacement feeds the case-insensitive match in directly, so it exercises the filter regardless of the host filesystem:

monkeypatch.setattr(Path, "rglob", lambda self, pattern: iter((page, upper)))

iterated = tuple(iter_page_paths(almanac_path))

assert iterated == (page,)
for path in iterated:
    assert page_id_for_path(almanac_path, path)

Verified it pins rather than describes — with the guard removed:

FAILED tests/test_wiki_parsing.py::test_page_iteration_excludes_uppercase_markdown_suffixes
FAILED tests/test_wiki_parsing.py::test_page_iteration_filters_case_insensitive_glob_matches
2 failed, 9 passed

The second of those is the one that will now also fail on Linux. I kept the filesystem-based test alongside it, since it covers the real Windows/macOS glob path end to end.

The asserted invariant is deliberately "everything iter_page_paths yields has a resolvable page id" rather than ".MD specifically is skipped", so it also catches any future divergence between the producer and page_id_for_path instead of just this one suffix.

Also re-confirmed the end-to-end symptom is gone, on the same wiki that reproduced it (almanac/ containing README.md, topics.yaml, NOTES.MD):

$ codealmanac search notes
README

$ codealmanac health
orphans (0): ok
dead-refs (0): ok
broken-links (0): ok
...

Before the fix that first command exited non-zero with wiki page must be markdown: ...\NOTES.MD.

No change to src/ in this push — the one-line guard is unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant