Skip to content

feat(tooling): changelog fragments - conflict-free parallel PRs - #806

Merged
igerber merged 6 commits into
mainfrom
feat/changelog-fragments
Aug 30, 2026
Merged

feat(tooling): changelog fragments - conflict-free parallel PRs#806
igerber merged 6 commits into
mainfrom
feat/changelog-fragments

Conversation

@igerber

@igerber igerber commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • Changelog fragments: release notes are now per-PR files under changelog.d/ (one file per PR, ### <Category> blocks; format spec in changelog.d/README.md), so concurrent PRs from separate worktrees no longer conflict on CHANGELOG.md. The ## [Unreleased] section is pointer-only between releases, CI-enforced by the new tests/test_changelog_fragments.py in the docs-tests lane.
  • Compiler .claude/scripts/changelog_compile.py (stdlib-only, py3.9-compatible, argv-safe): check validates the fragment store + pointer invariant; compile --version --date merges fragments into the release section (category order pinned, oldest-first within category, byte-preserving contiguous joins), writes the v-prefixed comparison link, deletes the fragments, and refuses everything it can't certify - downgrades, dirty/uncommitted fragments, empty sections, non-canonical dates, wrongly-sourced or missing links; exit 4 = idempotent already-compiled re-run for /bump-version.
  • Migration: current Unreleased content (fix(survey): unit-level survey_metadata recompute reports raw weight scale #801/feat(provenance): per-row event-study df threading (M-092 completion) + ContinuousDiD unit-level metadata #803/chore(validation): shared n_bootstrap type guards + honest_did inference-df consolidation #804/test(dml): replicate Chang (2020) §4.2.2 RCS simulation DGP (DML PR-B2) #805) moved verbatim into four fragments - a scratch-copy compile reproduces the pre-migration section byte-identically (modulo the header line).
  • CI wiring: fragment/compiler/CHANGELOG paths trigger docs-tests only (new step, self-pinned filters); the py39 smoke job runs the compiler check (floor coverage); rust-test.yml excludes the guard test (per the test_doc_snippets precedent) so guard-only edits skip the heavy matrix.
  • Instruction surfaces: CONTRIBUTING (new convention section + rollback note), CLAUDE.md, bump-version (compiler flow + exit-code contract), docs-impact, pre-merge-check (checkbox + check invocation), submit-pr (embedded template + always-run warning), push-pr-update (both blocks), PR template, the CI reviewer prompt (P2 missing-fragment / P3 direct-Unreleased clauses, comment-only carve-out keeping the frozen negative controls clean), docs/v4-design.md (fragment now carries the flipped-row-id obligation).
  • Deferred (DEFERRED.md, Parked): hard fragment-required gate; fragment-izing TODO/DEFERRED/ledger collisions.

Intended behavior changes, stated: compiled release sections list entries oldest-first within a category; releases now require fragments (the old git-log changelog generation is removed; fragment-free cycles use a committed ### Internal stub).

Rebasing an in-flight branch that edited Unreleased: move the bullet into a fragment (documented in CONTRIBUTING + changelog.d/README).

Methodology references (required if estimator / math changes)

  • Method name(s): N/A - no methodology changes (release tooling only; changelog text moved verbatim)
  • Paper / source link(s): N/A
  • Any intentional deviations from the source (and why): None

Validation

  • Tests added/updated: tests/test_changelog_fragments.py (51 tests: live-repo invariant, README↔compiler parity, check negatives/positives, compile behavior incl. exit-4 certification edge cases and the committed-stub recovery flow, workflow-filter + step pins, byte-stability). tests/test_openai_review.py (264) still green after the workflow edits.
  • Backtest / simulation / notebook evidence (if applicable): scratch-copy dry-run compile reproduced the pre-migration Unreleased body byte-identically; re-run exited 4 with the same date.

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

Changelog

  • changelog.d/ fragment added (or N/A - no user-visible change): Yes (changelog.d/20260830-changelog-fragments.md)

Release notes move from CHANGELOG.md's ## [Unreleased] section (where every
concurrent PR prepended to the same lines and conflicted) to per-PR fragment
files under changelog.d/, compiled into the version section at release time
by the new stdlib-only .claude/scripts/changelog_compile.py (check +
compile subcommands: pointer-only Unreleased invariant, fragment grammar,
canonical semver/date validation, downgrade + dirty-fragment + empty-section
guards, exit-4 idempotent re-run that certifies only a state the compiler
could have produced - canonical date, predecessor-sourced comparison link,
non-empty section - and v-prefixed comparison links).

- Migrate the current Unreleased content (from #801/#803/#804/#805) into
  four fragments, byte-identity verified via a scratch-copy compile
  round-trip; Unreleased is now pointer-only.
- Guard test tests/test_changelog_fragments.py (51 tests): live-repo
  invariant, README<->compiler category parity, check/compile behavior
  incl. the fragment-free committed-stub recovery flow, and self-pinning
  docs-tests.yml path filters + step.
- CI: fragment/compiler/CHANGELOG paths trigger the docs-tests lane only
  (new doc-snippets step; the py39 smoke job also runs the compiler check
  so the 3.9 floor is continuously exercised); rust-test.yml excludes the
  guard test from its filters (test_doc_snippets precedent) so guard-only
  edits skip the heavy matrix.
- Instruction surfaces updated: CONTRIBUTING (new CI-enforced convention
  section + rollback note), CLAUDE.md source-of-truth row, bump-version
  (compiler flow, exit-code contract, commit-the-stub recovery, five stale
  statements), docs-impact, pre-merge-check (checkbox + real `check`
  invocation), submit-pr (embedded template + always-run warning),
  push-pr-update (both blocks), the PR template, the CI reviewer prompt
  (P2 missing-fragment / P3 direct-Unreleased clauses with a comment-only
  carve-out), and docs/v4-design.md's two forward obligations.
- DEFERRED.md row: hard fragment gate + fragment-izing TODO/DEFERRED/
  ledger collisions parked pending demand.

Intended behavior change: compiled release sections list entries
oldest-first within a category (ascending fragment-filename order);
releases now require fragments (git-log changelog generation removed).
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 6e471badfbefc1ef4f718ee1637b7515d571f595


Overall assessment

⚠️ Needs changes — one unmitigated P1 release-integrity issue.

Executive summary

  • No estimator, mathematical, weighting, variance, inference, or default behavior changes.
  • The dirty-fragment guard can silently compile and delete an ignored, uncommitted fragment.
  • Two minor validator edge cases remain.
  • CI path wiring and changelog_compile.py check pass.
  • The deferred hard fragment-presence gate is properly tracked.

Methodology

No findings. Method-related changelog text is relocated existing content; estimator code and docs/methodology/REGISTRY.md are unchanged.

Code Quality

1. Ignored fragments bypass the committed-content guard

Severity: P1
Location: .claude/scripts/changelog_compile.py:L335-L350, .claude/scripts/changelog_compile.py:L363-L396

Impact: git status --porcelain omits ignored untracked files. A fragment ignored through .git/info/exclude, global gitignore, or repository rules is therefore compiled into CHANGELOG.md and deleted while the command reports success—contradicting the guarantee that uncommitted content is rejected.

Concrete fix: Require every discovered fragment to exist as a regular file in HEAD with worktree bytes matching the committed blob. Add a regression test using .git/info/exclude and verify compilation rejects and preserves the ignored fragment.

2. EOF-only Unreleased headers raise a traceback

Severity: P2
Location: .claude/scripts/changelog_compile.py:L125-L134

Impact: CHANGELOG.md ending with ## [Unreleased] causes .index("\n") to raise ValueError instead of returning a validation finding.

Concrete fix: Parse the header with horizontal whitespace only and handle a missing terminal newline explicitly; add an EOF-header test.

Performance

No findings.

Maintainability

No findings.

Tech Debt

Tracked fragment-presence gate

Severity: P3 informational
Impact: CI validates existing fragments but does not require one for every user-visible change.

Concrete fix: No action required; tracked in DEFERRED.md:L83.

Security

No secrets or security vulnerabilities found.

Documentation/Tests

Empty bullets pass validation

Severity: P2
Location: .claude/scripts/changelog_compile.py:L99-L109

Impact: ### Fixed\n- \n is accepted and compiled as an effectively empty section.

Concrete fix: Require at least one top-level bullet with non-whitespace content and add a negative test.

Validation note: the live repository check passed. The pytest suite could not run locally because pytest is unavailable.

Path to Approval

  1. Strengthen the dirty guard to reject any fragment not committed unchanged in HEAD, including ignored files, and add the .git/info/exclude regression test.

…t); EOF-only Unreleased header is a finding not a traceback; empty bullets rejected (PR #806 review P1/P2s)
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: c1a6432a79f3c3ca29d5569b2f82b1fbdab08d13


Overall assessment

⚠️ Needs changes — two unmitigated P1 release-tooling issues remain.

Executive summary

  • No estimator, weighting, variance, inference, or default behavior changed.
  • Migrated methodology prose agrees with REGISTRY.md and tracked limitations.
  • The previous dirty-fragment finding is only partially fixed: Git configuration and symlinks can still bypass it.
  • EOF handling was fixed for Unreleased but not the existing-release parser.
  • The repository check passes; pytest was unavailable locally.

Methodology

No findings. The affected estimator names appear only in relocated changelog prose. Claims checked against docs/methodology/REGISTRY.md:L1189, L3132-L3144, L4639 and TODO.md:L81-L89; no undocumented methodology deviation was introduced.

Code Quality

1. Dirty guard still does not prove fragments match HEAD

Severity: P1 — previous finding only partially resolved
Location: .claude/scripts/changelog_compile.py:L342-L369

Impact: git status is not a committed-content check. For example, status.showUntrackedFiles=no suppresses an untracked fragment even with the current arguments, allowing it to be compiled and deleted. A committed symlink fragment similarly remains status-clean while read_text() follows mutable target content. This still permits unreviewed release text to be silently consumed.

Concrete fix: For every discovered fragment, reject symlinks/non-regular files, require a regular blob at the same path in HEAD, and compare worktree bytes directly with that blob. Add regressions for status.showUntrackedFiles=no and a committed symlink fragment.

2. [Newly identified] Existing release header at EOF still raises

Severity: P1
Location: .claude/scripts/changelog_compile.py:L205-L213

Impact: An existing target header at EOF without a trailing newline reaches .index("\n") and raises ValueError. The EOF pattern was fixed only in _unreleased_slice, leaving the same validator anti-pattern here.

Concrete fix: Treat a missing newline as an empty section and return the normal validation error. Add an exit-4-path test with the target header ending at EOF.

Performance

No findings.

Maintainability

No findings.

Tech Debt

Fragment-presence enforcement

Severity: P3 informational
Impact: CI does not require a fragment for every user-visible change.

Concrete fix: No action required; tracked in DEFERRED.md:L83.

Security

No secrets or additional security vulnerabilities found.

Documentation/Tests

Ignored-fragment regression can pass for the wrong reason

Severity: P2
Location: tests/test_changelog_fragments.py:L376-L389

Impact: The test leaves README.md untracked, so ordinary status is already nonempty; it does not prove the ignored fragment caused rejection.

Concrete fix: Commit the baseline repository first, then create and ignore the sole fragment before invoking the compiler.

Path to Approval

  1. Validate each fragment as a regular HEAD blob with byte-identical worktree content, with configuration and symlink regressions.
  2. Make _section_nonempty() handle EOF safely and add the target-header-at-EOF test.

… match their HEAD blob (regular file, byte-identical; symlinks and status-hidden configs refused); _section_nonempty EOF-safe (PR #806 review round-2 P1s/P2)
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 71541ad72cb4a63aa5d3cbcbf8975ac86b217a09


Overall assessment

⚠️ Needs changes — two unmitigated P1 release-tooling issues remain.

Executive summary

  • No estimator, inference, weighting, variance, or default behavior changed.
  • Relocated methodology claims agree with the registry and tracked limitations.
  • The previous EOF failure is fixed.
  • Symlinks are rejected only after being read, leaving CI/release failure paths.
  • Duplicate Unreleased headers bypass the pointer invariant.

Methodology

No findings. The changelog prose is relocation-only and agrees with docs/methodology/REGISTRY.md:L1226-L1237, L3132-L3144, L4639 and TODO.md:L81-L89.

Code Quality

1. Symlinks and special files are read before validation

Severity: P1 — previous finding only partially resolved
Location: .claude/scripts/changelog_compile.py:L144-L170, L346-L389

Impact: run_check() calls read_text() before the compile-only regular-file guard. Consequently, CI accepts a valid symlink targeting ordinary content, while dangling or device/FIFO targets can crash or hang both check and compile. The test at tests/test_changelog_fragments.py:L443-L452 exercises only the later compile guard.

Concrete fix: In run_check(), use lstat() and reject symlinks/non-regular files before reading. Convert read failures into findings. Add check-level regressions for committed normal-target and dangling symlinks.

2. Duplicate Unreleased headers bypass validation

Severity: P1 — [Newly identified]
Location: .claude/scripts/changelog_compile.py:L128-L141, L172-L185

Impact: _unreleased_slice() validates only the first matching header. A second ## [Unreleased] can contain direct bullets while check passes; compilation then inserts a release above that second section and deletes the fragments, producing a malformed changelog.

Concrete fix: Require exactly one exact Unreleased header before validating its body. Add check and compile regressions containing two Unreleased sections, with a bullet under the second.

Performance

No additional findings.

Maintainability

No additional findings.

Tech Debt

Fragment-presence enforcement

Severity: P3 informational
Impact: CI does not require a fragment for every user-visible change.

Concrete fix: No action required; tracked in DEFERRED.md:L83.

Security

No secrets or privilege-related vulnerabilities found.

Documentation/Tests

No additional findings. The standalone compiler check passes. The focused pytest suite could not run because pytest is unavailable in the review environment.

Path to Approval

  1. Reject symlinks and non-regular fragment paths in run_check() before reading them, with check-level regressions.
  2. Reject duplicate Unreleased headers and test both checking and compilation.

…ad; exactly one Unreleased header enforced (PR #806 review round-3 P1s)
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: dc3b57f09bec0317ad14c243e7fc8d1f067cc1eb


Overall assessment

⚠️ Needs changes — one newly identified P1 release-tooling edge case remains.

Executive summary

  • No estimator, inference, weighting, variance, or identification logic changed.
  • Relocated methodology claims agree with the Methodology Registry and tracked limitations.
  • Both previous P1 findings are resolved with regressions.
  • Duplicate release-version headers can still be certified as an already-completed release.
  • The standalone changelog check passes; pytest is unavailable in this environment.

Methodology

Relocated methodology prose

Severity: P3 informational
Impact: None. The DML §4.2.2, event-study df, and survey-metadata descriptions are relocation-only and agree with docs/methodology/REGISTRY.md:L1189, L1565, L3132-L3144, and L4639.
Concrete fix: No action required.

Code Quality

1. Duplicate release headers pass exit-4 certification

Severity: P1 — [Newly identified]
Location: .claude/scripts/changelog_compile.py:L286-L356

Impact: _existing_headers() may return multiple headers for the target version, but run_compile() validates only the first matching section. If that section is nonempty and a matching comparison link exists, the compiler exits 4 even when another ## [X.Y.Z] section remains. /bump-version then treats the malformed changelog as successfully compiled and proceeds with the release.

Concrete fix: Before the existing-header loop, reject duplicate semantic-version headers—preferably any duplicated release version—with exit 1. Add a regression containing two target-version sections, no fragments, and an otherwise valid comparison link.

The previous symlink/special-file and duplicate-Unreleased findings are resolved at .claude/scripts/changelog_compile.py:L182-L221, with corresponding tests.

Performance

No findings.

Maintainability

No additional findings.

Tech Debt

Fragment-presence enforcement

Severity: P3 informational
Impact: A fragment is not yet mandatory for every user-visible change.
Concrete fix: No action required; tracked in DEFERRED.md:L83.

Security

No secrets or security vulnerabilities found.

Documentation/Tests

No independent findings. changelog_compile.py check passes and both new Python files parse successfully. The focused pytest suite could not run because pytest is unavailable.

Path to Approval

  1. Reject duplicate release-version headers before returning exit 4, and add the duplicate-target-header regression described above.

@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 8ced971851dc304b43cfb3afd6b095bc317c7c20


Overall assessment

Looks good — all prior P1 findings are resolved; no unmitigated P0/P1 issues remain.

Executive summary

  • No estimator, weighting, variance, inference, or identification code changed.
  • Relocated methodology claims agree with the Methodology Registry and cited Chang review.
  • Duplicate release headers are now rejected before exit-4 certification, with regression coverage.
  • The changelog guard and syntax checks pass; pytest is unavailable locally.

Methodology

Relocated methodology prose

Severity: P3 informational
Impact: None. The DML §4.2.2, event-study df, and survey-metadata claims match docs/methodology/REGISTRY.md:L1189, L1565-L1566, L3132-L3144, and L4639, plus docs/methodology/papers/chang-2020-review.md:L263-L266.
Concrete fix: No action required.

Code Quality

Prior duplicate-header finding resolved

Severity: P3 informational
Impact: Duplicate release versions can no longer be certified as already compiled. The check occurs before the exit-4 path at .claude/scripts/changelog_compile.py:L286-L300, with regression coverage at tests/test_changelog_fragments.py:L396-L407. Earlier symlink and duplicate-Unreleased findings also remain resolved.
Concrete fix: No action required.

Performance

No findings.

Maintainability

No findings.

Tech Debt

Fragment-presence enforcement

Severity: P3 informational
Impact: A fragment is not yet mandatory for every user-visible change.
Concrete fix: No action required; tracked in DEFERRED.md:L83.

Security

No secrets or security vulnerabilities found.

Documentation/Tests

No findings. changelog_compile.py check, syntax validation, and git diff --check pass. The focused pytest suite could not run because pytest is not installed in this environment; CI invokes it from .github/workflows/docs-tests.yml:L126-L130.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Aug 30, 2026
@igerber
igerber merged commit b613098 into main Aug 30, 2026
35 of 36 checks passed
@igerber
igerber deleted the feat/changelog-fragments branch August 30, 2026 12:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-ci Triggers CI test workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant