Skip to content

The CLAUDE.md currency guard carried a third spelling of the fragment grammar and it had drifted - #377

Merged
fdaviddpt merged 1 commit into
mainfrom
fix/375
Aug 20, 2026
Merged

The CLAUDE.md currency guard carried a third spelling of the fragment grammar and it had drifted#377
fdaviddpt merged 1 commit into
mainfrom
fix/375

Conversation

@fdaviddpt

@fdaviddpt fdaviddpt commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Closes #375.

What was wrong

tests/test_claude_md_currency.py counts unfolded changelog fragments to decide whether a release is being prepared, and it counted them with its own copy of the fragment name grammar:

FRAGMENT = re.compile(r"\A\d+\.[a-z]+\.md\Z")

That copy predates the optional <slug> segment the grammar grew in #308. A cycle whose pending fragments were all slug-form therefore parsed as an empty directory, and the guard skipped with "no unfolded changelog fragments, so no release is being prepared" -- an absence produced by the parser rather than by the directory, which is this repository's own defect class pointed at its own instrumentation. Harmless so far only because every cycle to date has carried two-segment fragments alongside.

The judgment call: derive, do not add a fourth copy

The issue asks for one spelling fewer rather than one more, so FRAGMENT is now assemble_changelog._NAME_RE -- taken from the module that owns the grammar and that a fragment must already satisfy to reach CHANGELOG.md. There is precedent for exactly this in tests/test_docs_state_slug_grammar_308.py, which reads the grammar out of the assembler at runtime for the same stated reason, so no new module was invented to hold a regex.

The name grammar and not the public parse_fragment_name, which additionally refuses a section outside the six: 1.bogus.md is a file somebody filed as a fragment and the assembler will refuse the release over it, so counting it as absent here would be the same defect one layer down.

The two CI gates were re-derived, not taken on trust

The issue asserts both are already correct. Checked rather than believed, by running their patterns against this range's own fragments:

gate 308.fixed.slug-form-documented.md 228.fixed.md README.md
.github/workflows/changelog.yml:96 matches matches refused
the workflow scripts/scaffold.py:799 writes matches matches refused

Both correct. Both are shell and cannot import; this one could, which is why it is the copy that was removed rather than repaired.

A sweep of scripts/ for the two character classes the grammar is built from finds the remaining spellings: assemble_changelog._NAME_RE (the owner) and release_version.FRAGMENT_NAME (a transcription). The transcription is not an unguarded fourth copy -- tests/test_release_version_fragment_names_297.py::test_the_version_rule_and_the_assembler_agree_on_which_names_are_fragments measures the two against each other behaviourally. No finding there.

The test drives the guard, not the pattern

Asserting only that the regex matches a slug name would pass against a version that still skipped, so the new test points _fragment_dir at an all-slug fixture and reads which arm the guard took. Paired with an empty directory and a README-only directory, both of which must still skip -- without that control the first test would pass against a guard that never skips at all.

pytest.skip.Exception is pinned rather than caught as Exception: Skipped derives from BaseException, so except Exception and pytest.raises(Exception) both sail past it and skip the enclosing test -- a green tick over an assertion that never ran.

Evidence

Red, before the fix:

E   AssertionError: the optional slug is part of the documented grammar -- `<issue>.<section>[.<slug>].md`
E   assert None
E    +  where None = <built-in method match of re.Pattern object at 0x101e47ab0>('878.fixed.second-entry.md')

E   AssertionError: every fragment waiting to be folded is slug-form, so a release IS being prepared -- and the guard reported 'no unfolded changelog fragments' and skipped. That absence was produced by the parser, not by the directory.
E   assert 'skipped' == 'ran'
2 failed, 13 passed in 0.03s

The empty-directory control passed at that point too, so the red was the finding and not the harness.

Green, after: 15 passed in 0.06s for the file, and 2611 passed, 2 skipped in 247.91s for the full suite (python3 -m pytest tests/ -q, coverage 91.69% against an 85% floor). Both pre-existing skips are unrelated and name their own reasons.

Platform

Observed on macOS/darwin, Python 3.13 locally. Reasoned for the other twelve legs: the diff adds no path-separator or suffix logic, builds every path through pathlib, pins encoding="utf-8" on every write, prints nothing, adds no platform branch and no narrow except on a platform-specific type, and introduces no non-ASCII into the test source. pytest.skip.Exception is a pytest fact, not a platform one.

Verified by the maintainer

The red re-run does not have its usual shape here and that is worth recording rather than leaving
absent: the defect and its test are the same file, so checking the branch's test file out onto
main brings the fix with it and the run comes back green. Reconstructed instead — branch test file
on origin/main in a detached worktree, then FRAGMENT alone reverted to the pre-fix pattern:

2 failed, 13 passed in 0.10s
AssertionError: every fragment waiting to be folded is slug-form, so a release IS being prepared
 -- and the guard reported 'no unfolded changelog fragments' and skipped.
assert 'skipped' == 'ran'

The empty-directory and README-only controls passed in that same run, so the red is the defect and
not a broken harness. That is the whole of my independent check; everything else above is the
author's and stands on its own evidence.

… grammar and it had drifted (#375)

`tests/test_claude_md_currency.py` counted pending fragments with
`\A\d+\.[a-z]+\.md\Z`, written before the grammar grew its optional
`<slug>` segment in #308. A cycle whose pending fragments were all
slug-form therefore parsed as an empty directory, and the guard skipped
with "no unfolded changelog fragments, so no release is being prepared" --
an absence produced by the parser rather than by the directory, which is
this repository own defect class pointed at its own instrumentation.

The fix is one spelling fewer, not one more: `FRAGMENT` is now
`assemble_changelog._NAME_RE`, taken from the module that owns the
grammar. The name grammar and not `parse_fragment_name`, which also
refuses a section outside the six -- `1.bogus.md` is a file somebody
filed as a fragment and the assembler will refuse the release over it, so
counting it as absent here would be the same defect one layer down.

The two CI gates were re-derived rather than trusted from the issue and
are correct: the pattern in `.github/workflows/changelog.yml:96` and the
same shape in the workflow `scripts/scaffold.py:799` writes both accept
the slug form and both refuse `README.md`. Neither can import; this one
could.

The test drives the guard rather than the pattern -- an all-slug fixture
must make it run, because asserting only that the regex matches a slug
name would pass against a version that still skipped. Paired with an
empty directory and a README-only directory, both of which must still
skip. `pytest.skip.Exception` is pinned rather than caught as `Exception`:
`Skipped` derives from `BaseException`, so the enclosing test would have
skipped instead of failing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HuQoqn4iLmW2ULpvhKnwj
@fdaviddpt
fdaviddpt merged commit daddc05 into main Aug 20, 2026
14 checks passed
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.

The CLAUDE.md currency guard's fragment pattern predates the slug grammar, so an all-slug cycle makes it skip with 'no fragments'

1 participant