Release Audit
Generates a markdown audit of a Newton release for keep/defer decisions (or, in retrospective mode, for skill calibration). Three modes, auto-detected in Phase 1:
Read CODING_GUIDELINES.rst and REVIEW_GUIDELINES.rst from the
repository root before auditing. They define the canonical coding, API, and
review policies. This skill performs the final cross-release reconciliation;
it does not replace pull-request review, and a prior review pass does not make
any audit phase optional. In retrospective mode, using the current guides is
intentional: calibrate the current audit policy against evidence pinned to the
target tag. Do not claim historical noncompliance unless the requirement also
existed at that tag.
- Pre-release: spot-check while work is still landing on main. No release branch cut. Version string is
X.Y.Z.devN. - Release-candidate: readiness review after the release branch is cut. Version string is
X.Y.ZrcNor head isrelease-X.Y. - Retrospective: audit an already-shipped release (e.g.,
v1.1.0) against its predecessor, with a Calibration Notes section (Phase 7) that checks Claude's flags against what subsequent patch/minor releases actually did. Triggered by passing a bare released-version argument that matches an existing git tag.
Output: a single markdown report, filed according to the destination chosen in Phase 1:
- Secret gist (default when
ghis available and authenticated): stable filenamenewton-<version-string>-<prerelease|rc|retrospective>-report.md, stable descriptionNewton <version-string> <Pre-Release|Release Candidate|Retrospective> Report. Later runs against the same version revise the same gist in place; prior versions are preserved in the gist's git history. - Local markdown file (fallback when
ghunavailable, or opt-in whenghavailable): dated path at$(git rev-parse --show-toplevel)/newton-<version-string>-<prerelease|rc|retrospective>-report-<YYYY-MM-DD>.md. Not auto-committed; user moves, shares, or deletes as desired.
Inputs inferred from repo state:
- Pre-release / RC: target version from
pyproject.toml(version = "...").newton/_version.pyreads from installed metadata at runtime and is not a static source. - Retrospective: target version from the user's argument.
pyproject.tomlis ignored. - Base = latest tag matching previous minor's line (
vX.Y-prev.*, latest patch). - Head (pre-release / RC) =
upstream/release-<target>if it exists, elseupstream/main. Head (retrospective) = thevX.Y.Ztag itself.
Reference documents to load on demand (via Read):
references/report-template.md— fill this in during Phase 7b (the{{HEADLINE_SUMMARY}}placeholder is drafted in Phase 7a).references/render-rules.md— rendering conventions and output style hard constraints for Phase 7b.references/classification-rules.md— path/symbol rules used in Phases 3-5.references/language-review-examples.md— Phase 5 language-review calibration.
Phase 1 — Align on scope
Determine the mode. Look at the user's argument (
$1), if any:- Argument present AND matches an existing tag (
git rev-parse --verify v<arg>orgit rev-parse --verify <arg>succeeds, AND the resolved name looks likevX.Y.Z/vX.Y.ZrcN): Retrospective mode. The argument is the already-shipped target version. Skippyproject.tomlentirely; the tag is authoritative. Record the raw version string (e.g.,1.1.0) for the report header and filenames. Do not run the pre-release / RC reconciliation in step 4. - Argument present but does NOT match any tag: treat as a version override for the upcoming release. Use it as if it came from
pyproject.toml, then fall through to the pre-release / RC detection below. - No argument: read the version string from
pyproject.toml— the top-level[project]table'sversion = "..."line.
For the non-retrospective path, parse the version string to extract the target minor (e.g.,
1.2.0.dev0→ target1.2) and pre-classify mode:- If the version string contains
"rc"(e.g.,1.2.0rc1) → RC mode candidate: this is a release-candidate readiness report. - If the version string contains
"dev"(e.g.,1.2.0.dev0) → Pre-release mode candidate: this is an early-stage audit of unreleased work. - Otherwise → pre-release mode (default), but record the raw version string so the header can show it as-is.
- Argument present AND matches an existing tag (
Enumerate previous-minor tags (applies to all modes):
git tag --list 'v<prev-major>.<prev-minor>.*' --sort=-v:refnamewhere
<prev-major>.<prev-minor>istarget - 0.1(e.g., for target1.2, previous minor is1.1). Take the first result as the base candidate. Ignore pre-1.0beta-*tags when a stablevX.Y.Zline exists.Major-boundary fallback. When
target.minor == 0(e.g.,2.0.0), thetarget - 0.1computation yields a minor line that never existed (1.9), and the tag list comes back empty. In that case, enumerate the highest minor line of the previous major instead:git tag --list 'v<target-major - 1>.*' --sort=-v:refnameTake the first result (the last-patch of the last-minor of the previous major) as the base candidate. If both the primary and fallback searches return empty (which should only happen on a never-released line), surface that to the user in step 6 rather than silently proceeding.
For retrospective mode with target
X.Y.Z: the base is the lastvX.Y-prev.*tag strictly beforevX.Y.Z. Also check whetherX.Y.Zis itself a patch release (.Z > 0): if so, the "base" could be either the previous patch on the same minor (vX.Y.<Z-1>) OR the previous minor's latest. Present both in step 6 and let the user pick — a patch retrospective usually wants patch-on-patch; a minor-release retrospective wants previous-minor's last patch.Probe for the head:
- Retrospective mode: head is the
vX.Y.Ztag directly. Resolve withgit rev-parse --verify v<target>. - Pre-release / RC:
If this succeeds, head =git rev-parse --verify upstream/release-<target>upstream/release-<target>and this is also a strong signal for RC mode (the branch-cut has happened). Otherwise tryorigin/release-<target>; otherwise head =upstream/main(falling back toorigin/main, thenmain) which is pre-release mode. Record whichever fallback was used for the report header.
- Retrospective mode: head is the
Reconcile mode from version string and head (skipped for retrospective mode; its mode is already fixed):
- Version says RC AND head is a release branch → RC report (strong match).
- Version says RC but head is main (branch not cut yet) → RC report (version is authoritative; note the mismatch in the header).
- Version says dev AND head is a release branch → RC report (branch cut implies we're past the dev window).
- Version says dev AND head is main → Pre-release report.
Probe
ghavailability and look up any existing matching gist.First run:
gh --version && gh auth statusIf either fails →
ghunavailable; destination will be a local markdown file only; skip the rest of this step.Both succeeded → compute the stable gist title for this report:
Newton <version-string> <Pre-Release|Release Candidate|Retrospective> Report(e.g.,
Newton 1.2.0rc1 Release Candidate Report,Newton 1.2.0.dev0 Pre-Release Report,Newton 1.1.0 Retrospective Report). No date. The gist filename and description are stable so later runs can find the same gist and revise it; gist git history preserves prior versions automatically.Run
gh gist list --limit 1000and filter rows whose description exactly matches that stable title. Capture the matching gist IDs; display URLs arehttps://gist.github.com/<id>. Record the match count (0, 1, or N ≥ 2) for step 6.Present proposal in chat and wait for explicit user confirmation of refs AND output destination. Mandatory pause.
Lead with the mode-specific intro line:
- Pre-release:
Generating **pre-release report** for Newton **<version>**. Base **<base-ref>** → Head **<head-ref>**. **<N>** commits in range. - RC:
Generating **release-candidate report** for Newton **<version>**. Base **<base-ref>** → Head **<head-ref>** (release branch cut). **<N>** commits in range. - Retrospective:
Generating **retrospective report** for Newton **<version>** (already shipped). Base **<base-ref>** → Head **v<version>**. **<N>** commits in range. Calibration pass will cross-reference Claude's flags against subsequent patch / minor releases.
For retrospective mode with a patch-target (when
.Z > 0), also present the base choice explicitly:Base candidates for retrospective of
vX.Y.Z:vX.Y.<Z-1>(patch-on-patch: what changed since the previous patch on the same minor)v<prev-major>.<prev-minor>.<latest-patch>(minor-boundary: what changed since the previous minor)
Pick (1) or (2), or specify a different base.
Append the output block for the current
gh/ match state:ghunavailable:Output: markdown file at repo root (
ghnot available). Confirm, or specify different refs?ghavailable, 0 matches:Output: (a) new secret gist [default], (b) local markdown file at repo root. Confirm refs + pick.
ghavailable, 1 match:Output: (a) revise existing gist
<url>[default], (b) new secret gist, (c) local markdown file at repo root. Confirm refs + pick.ghavailable, N matches (N ≥ 2):Multiple existing gists share the stable title:
<url-1>— updated<time-1><url-2>— updated<time-2>...
Output: (a) revise gist by number, (b) new secret gist, (c) local markdown file at repo root. Confirm refs + pick.
Do not run any further phase until the user confirms refs and (when
ghis available) chooses destination. Translate the user's reply into exactly one of the destination tokenslocal,new-gist,revise-gist:<id>and record it for Phase 7c. Letters(a),(b),(c)are positional within the current branch's prompt, not global: resolve them against the option list you just showed the user. For the N-match branch, the user picks a gist by the number you listed (e.g., "revise 2" →revise-gist:<id-of-listed-row-2>), or says new / local.- Pre-release:
Phase 2 — Gather ground truth
Run the commit-list tool:
uv run --no-project python ${CLAUDE_SKILL_DIR}/scripts/list_commits.py \ --base <base-ref> \ --head <head-ref> \ --report-date "$(date +%F)" \ --main-ref <resolved-main-ref>from the repo root (
$(git rev-parse --show-toplevel)). Capture stdout as thecommit_list_json.Run the dependency and license audit helper:
uv run --no-project python ${CLAUDE_SKILL_DIR}/scripts/license_audit.py \ --base <base-ref> \ --head <head-ref>from the repo root. The helper requires Python 3.11+ for stdlib TOML parsing and exits with a clear preflight error on older Python versions. Capture stdout as
dependency_license_audit_md. The helper compares:- Direct requirements in
pyproject.toml, grouped by runtime and optional extra. - Resolved package names, duplicate variants, and version sets in
uv.lockwhen present. project.licenseandproject.license-filesmetadata inpyproject.toml.- In-tree notice files matched by the
project.license-filespathspecs declared at the base or head ref. - Version-specific PyPI license metadata for newly introduced package names and changed locked package versions when network access is available. If metadata lookup fails, keep the helper's "not checked" text and surface that uncertainty rather than filling in a guessed license. If the helper was run with
--skip-pypi, treat package-index metadata as deliberately deferred, not as per-package review failures.
Use one helper invocation as the source of the entire dependency/license section. Do not splice prose from a live metadata check onto tables produced by
--skip-pypi. If a sandboxed or deferred run used--skip-pypiand a later host-side run can reach package indexes, replace the complete section with the default host-side output. Use--skip-pypionly as a fallback when a live metadata pass cannot be completed.In Existing Resolved Package Version-Set Changes, the helper keeps standard license expressions and review statuses inline. Legacy or verbose nonstandard license text is rendered as a compact package-metadata link rather than copied into the report. The helper wraps this potentially long table in a closed
<details>block; keep it collapsed by default so it does not dominate report scrolling.The helper is intentionally stdlib-only. Do not replace it with a dependency inventory tool during the audit run: Newton avoids new release-only dependencies, and this script needs deterministic comparisons across arbitrary git refs without installing the target environment. External tools such as
pip-licenses,cyclonedx-py,pip-audit, orsyftcan supplement a deeper investigation, but they do not replace this git-ref diff overpyproject.toml,uv.lock, declared license files, and version-specific PyPI metadata.Interpretation rules for
dependency_license_audit_md:- A newly introduced external direct dependency or new resolved package name is license-relevant even when it lives behind an optional extra. Do not dismiss optional dependencies; state the extra or install path that pulls them in.
- A package that first appears in the lockfile only beneath a direct dependency already declared at the base is resolved-set churn, not a new dependency. The helper renders it in Existing Resolved Package Version-Set Changes with
(not resolved)as the base and attributes the existing direct root. Reserve New Resolved Packages for packages introduced by a new direct dependency root or packages whose root cannot be established. - A new optional extra whose dependencies are all already present is a support/install-surface change, but not a new package-license change.
- Existing package version bumps are not new licenses by themselves. The helper separates direct requirement moves from transitive-only churn; only elevate version bumps to release highlights or Behavioral & Support Changes when the package pin or compatibility constraint is user-visible.
- In-tree notice-file additions, removals, or modifications under the declared
project.license-filespathspecs always appear in the dependency/license section. If a notice file is missing for a new bundled asset or vendored component, flag it in CHANGELOG Review Notes. - If the helper reports license metadata as "not checked" due to lookup failure or "not declared" for a new package, keep that wording and mark it as needing release-manager review. Do not infer a license from package authorship or project name.
- If the helper reports "not evaluated (--skip-pypi)", say that package-index metadata was deferred and should be checked before final release sign-off. Do not turn that into a per-package review list.
- If a new package is proprietary, copyleft, commercial, unknown, not declared, or not checked due to lookup failure, mention that in the release highlights only when users can install it through a published extra or documented workflow.
- Direct requirements in
Load the pending or released changelog source by mode:
- Pre-release / RC: user-facing release scope is the rolling
## [<target-version>]section inCHANGELOG.md, when already assembled, plus any later.mdfragments on the resolved head. Before the first Towncrier build, use the fragments plus legacy[Unreleased]entries. Enumeratechangelog/*.mdwithgit ls-tree -r --name-only <head> -- changelog, excludeREADME.md, and read each file withgit show <head>:<path>. Inventory.skipfiles separately. ReadCHANGELOG.mdwithgit show <head>:CHANGELOG.md. If the target's dated section exists, collect it through the next dated heading and treat it as the assembled baseline. Otherwise, legacy entries are between<!-- towncrier release notes start -->and the next dated##heading. If the audited head is the checked-out clean working tree, also run:
Use the draft output as the canonical preview for fragments not yet folded into the rolling section; it does not modifyuvx --from towncrier==25.8.0 towncrier build --draft \ --version X.Y.Z --date YYYY-MM-DDCHANGELOG.mdor delete fragments. Audit the assembled baseline and draft additions together. - Retrospective: read the target tag's changelog with
git show v<target>:CHANGELOG.md. Locate the## [<target-version>]header and collect content up to the next dated##heading.
- Pre-release / RC: user-facing release scope is the rolling
Parse entries. Infer a fragment's section from its filename type (
added,changed,deprecated,removed, orfixed); the file content is one entry without a leading bullet. Parse legacy or dated Markdown under the corresponding five###headings. For every entry, extract:- Raw text (FULL — never truncate): the full bullet content (may span multiple lines).
- Section: one of the five names above.
- Source path: the fragment path or
CHANGELOG.md. - Fragment identifier: the numeric issue or leading-
+orphan prefix from the filename. Same-category counters do not change the identifier. - GH refs: regex
GH-(\d+)over the bullet text. Dedup. (Newton commits and CHANGELOG entries sometimes also reference#NNNNas a bare PR number; capture these too.) - Migration hint: does the prose contain a migration phrase (
use <new>,in favor of,renamed to,replaced by,switch to,migrate to)? Record a boolean.
Newton does NOT use a
**Breaking:**literal marker in CHANGELOG (unlike Warp). Do not rely on its presence. Breaking-change detection in Newton comes from: (a) the### Removedsection (implicitly breaking), (b) signature-diff AST analysis (Phase 4e), and (c) reading### Changedprose for rename / parameter-reorder / signature-shift language (Phase 4d).
Phase 3 — Cross-reference
Build the commit ↔ CHANGELOG join on GH-ref overlap:
- For each CHANGELOG entry with at least one GH ref, find commits (from
commit_list_json) whosegh_refsintersect. - For each pending fragment, find the commit that added its exact path:
Numeric identifiers link to issues, not necessarily to the implementing pull request, so the fragment-addition commit is the authoritative join.git log --reverse --diff-filter=A --format='%H|%s|%cs' \ <base-ref>..<head-ref> -- changelog/<fragment-filename> - For legacy entries and any fragment still lacking a backing commit, put
the distinctive substring in
entry_textwithout interpolating it into shell source, then search exact text across both storage forms:
The first commit that is not a Towncrier build, synchronization, or another changelog-only edit usually is the code change associated with the entry. Record that as the backing commit.git log --reverse -S"$entry_text" \ --format='%H|%s|%cs' <base-ref>..<head-ref> -- CHANGELOG.md changelog - After these two passes, any CHANGELOG entry still with no matching commit is a genuine orphan case (deferred/dropped). Keep full entry text; do not truncate in the report.
- For each CHANGELOG entry with at least one GH ref, find commits (from
Do NOT build an audit trace of unmatched commits. The old "commits without CHANGELOG entries" appendix adds noise without value. Commits that don't map to a CHANGELOG entry are not surfaced in the report.
Phase 4 — Analyze API surface
4a — Determine what is genuinely NEW API
For each CHANGELOG Added entry, extract the named symbol(s) — text in backticks matching newton.*, bare ClassName, bare snake_case_name(), or ClassName.method() patterns.
For EACH named symbol, check if it existed at base. Newton exposes user-facing symbols through multiple public re-export modules (not a single __init__.py). See references/classification-rules.md → "Public API surface" for the discovery rules. docs/generate_api.py::api_modules() discovers top-level public modules from module-valued names in newton.__all__; solver_submodule_pages() adds public solver module trees. There is no fixed MODULES constant. Inspect this discovery code plus newton/__init__.py and newton/solvers.py at both refs because the exported module set may grow or shrink between releases.
To check presence at base, run git show <base>:<path> for each relevant public module and grep its imported names. For method additions on an existing class (e.g., SolverXPBD.update_contacts()), inspect the class body at base in its real source (resolved via _src/). For retrospective mode, "HEAD" in the symbol-resolution text below means the v<target> tag, not the working tree. Use git show v<target>:<path> everywhere the pre-release / RC flow uses the working tree.
Classification:
- Genuinely new (symbol was not present at base) → New API section.
- Existed at base (entry is adding a new parameter, option, or capability to something that already existed) → Changes to Existing API section with a "capability extension" or "new parameter" kind. Cite the backing commit's signature diff.
- Does not name a single symbol (e.g., "Interactive example browser in the GL viewer with tree-view navigation") and describes a cross-cutting capability → Behavioral & Support Changes section unless one of the mentioned symbols is genuinely new (in which case split: put the new symbol in New API, the capability description in Behavioral).
If an entry mentions multiple symbols where some are new and some pre-existed (e.g., "Add newton.geometry.compute_offset_mesh() and a viewer toggle"), split: the genuinely new symbols each get a New API entry; the extensions to existing symbols each get a Changes entry.
Public-API declaration and exposure check. At HEAD, inspect every public
module discovered by api_modules() and solver_submodule_pages(). Each module
must define __all__ as a list or tuple containing only strings; a missing or
invalid declaration is a policy finding in "CHANGELOG Review Notes". For every
symbol that passes the "genuinely new" test, collect its memberships across
those declarations:
- No memberships: report 🕵️ Private-only. The symbol exists only under
newton._src.*and was not re-exported through a public module. - One membership: the symbol has a canonical public import path.
- More than one membership: report a duplicate-public-export policy finding and list every public module that exports it.
CODING_GUIDELINES.rst requires each public symbol to appear in exactly one
public module's __all__ and forbids examples/docs from importing
newton._src. Do not treat the private-only finding as a hard block on the
entry; surface it so the release manager can confirm the symbol was intended
to be public. Treat missing or invalid declarations and duplicate exports as
policy findings that require an explicit release decision.
4b — Resolve New API signatures + docstrings
For each symbol confirmed as new:
- Find its real source module by following the re-export in the public module (e.g.,
newton/solvers.py→newton/_src/solvers/xpbd.py). ast.parsethe source module; find theFunctionDef/ClassDef/AsyncFunctionDef.- Extract the signature by re-stringifying the args (preserve type annotations).
- Extract the docstring verbatim via
ast.get_docstring(node). - Render as shown in the template.
Newton has no kernel-scope builtins layer to extract (no add_builtin() registry like Warp). All user-facing symbols live in Python modules and ast resolves them directly.
4c — Symbol resolution fallbacks
- Symbol in backticks doesn't resolve at HEAD: render the entry with a ⚠️ note: "Couldn't resolve
<symbol>in source — verify entry names a real public symbol." Do not fabricate a header likenewton.*(no symbol)*. Use the entry's natural subject as the section title. - Entry describes a topic, not a symbol (e.g., "Interactive example browser in the GL viewer"): use a short descriptive title summarized from the entry (e.g., "Interactive example browser"), not a synthetic
newton.*name. - Header naming rule: section headers in the report should be either real fully-qualified symbols (
newton.solvers.SolverXPBD.update_contacts) OR short descriptive titles extracted from the entry. Nevernewton.*,newton.(something), or similar stub patterns.
4d — Changed / Removed / Deprecated signature diffs
For each CHANGELOG entry in Changed / Removed / Deprecated (plus any "capability extension" entries routed here from 4a):
- Compute signatures at base and HEAD (same resolution as 4b).
- For signature-shape changes: render a fenced
diffblock showing-and+lines. - For semantic-only changes (no signature shift) where the prose describes a rename / reorder / behavioral flip: skip the diff block; include the backing commit's URL and the full CHANGELOG text.
- For Removed entries: show the old signature on a
-line; omit+.
Deprecation-window lookup for Removed entries. Newton's policy (per CODING_GUIDELINES.rst) is: breaking changes require a deprecation first. Every Removed entry needs evidence of a deprecation in a prior release. Start with the released CHANGELOG, then fall back to code-level runtime-warning evidence at the base ref. For every Removed entry (and every Changed entry whose prose describes a removal), search CHANGELOG.md for the matching prior Deprecated entry:
- Extract distinctive tokens from the Removed entry: the named symbol(s) in backticks and, if the entry carries a GH ref, that ref number.
- Scan the appropriate released-version sections of CHANGELOG.md for a
### Deprecatedbullet that names the same symbol(s) OR the same GH ref. The search scope depends on mode:- Pre-release / RC: scan all dated sections, top-down. Pending fragments and legacy current-cycle entries do not prove a prior shipped deprecation.
- Retrospective (current target is
X.Y.Z): scan ONLY the sections strictly below## [X.Y.Z]in CHANGELOG.md — the prior-release sections. Do NOT consider## [Unreleased]or later-version sections; they were written afterX.Y.Zshipped and cannot have preceded it. Top-down within the allowed range. The FIRST such entry (highest version, since CHANGELOG is reverse-chronological) is the deprecation introduction.
- If a matching CHANGELOG entry is found, record: (a) the release version heading that contains the Deprecated entry (e.g.,
1.0.0), (b) the full Deprecated entry text. - If no matching prior CHANGELOG entry is found, check for a code-level runtime deprecation at the base ref. Resolve candidate source paths using the removed symbol, legacy parameter, or literal value, then grep those paths for deprecation warnings and inspect the surrounding source:
If the candidate uses a shared deprecation helper or decorator, resolve the imported name and inspect its definition at the base ref too:git grep -n -F '<distinctive-symbol-or-legacy-value>' <base-ref> -- newton git grep -n -E 'DeprecationWarning|deprecated' <base-ref> -- <candidate-paths> git show <base-ref>:<candidate-path>
Count this as prior-deprecation evidence when a runtimegit grep -n -F '<helper-or-decorator-name>' <base-ref> -- newton git show <base-ref>:<helper-definition-path>DeprecationWarningat the base ref clearly applies to the exact removed symbol or behavior, either through a directwarnings.warn(...)call or through a shared helper / decorator. For helper-mediated evidence, verify both sides of the connection: the candidate path applies or calls the helper for the removed API, and the helper definition emitsDeprecationWarningfor the calling mode or behavior being removed. For example,@deprecate_nonkeyword_argumentsis evidence for removing positional-argument support only when it decorates that callable at the base ref; it is not evidence that the callable itself was deprecated. A generic helper that merely exists or is imported, a warning elsewhere in the same file, a docstring without a runtime warning, or a warning added only after the base release does not count. Record the warning text, the helper application path, and the warning-emission path. The base ref proves the deprecation shipped by that release; only claim an earlier introduction version if the same connected evidence is verified at that earlier tag. - In the rendered Removed entry, include one of these deprecation-window lines:
- CHANGELOG evidence:
Deprecated in X.Y.Z; removed here. - Runtime-only evidence:
Runtime deprecation present in <base-ref>; removed here. No matching prior CHANGELOG Deprecated entry was found. - No evidence:
No prior deprecation found in released CHANGELOG sections or in code at <base-ref> — Newton's policy requires deprecation before removal.Do NOT fabricate a version.
- CHANGELOG evidence:
The deprecation window belongs in BOTH the Breaking Changes entry for the removal AND the Changes-to-Existing-API row (in the Description cell or as an appended sentence in the detail block). A reader should never have to ask "was this deprecated first, and for how long?"
Missing-deprecation flag. Surface 🚨 Policy: removed without prior deprecation only when BOTH checks fail: no prior Deprecated entry exists in a previously-released CHANGELOG section, and no matching direct or helper-mediated runtime DeprecationWarning exists at the base ref. This fires whether or not the current release's own ### Deprecated section also names the symbol because a warning added only in the removal release did not ship in a prior release. The release manager needs this to block the release or add migration tooling.
If a matching runtime warning exists at the base ref but the released CHANGELOG has no corresponding Deprecated entry, the deprecation window is real. Do not emit a policy violation. Instead, add a non-blocking 🧾 Deprecation omitted from CHANGELOG review note with the base ref, warning text, and the direct source path or connected helper application / emission paths so the release-note gap remains visible.
Exception: 1.0.0 pre-stable cleanup. Removed entries in the 1.0.0 release (and 1.0.0rcN) are exempt from the deprecation-first policy — PRs labeled 1.0-release are the pre-stable API cleanup and were not required to go through a prior deprecation window. When the target version is 1.0.0 or 1.0.0rcN, do not emit the 🚨 Policy flag or the "No prior Deprecated entry found" line for its Removed entries. Still render the deprecation-window line if a matching Deprecated entry happens to exist; otherwise note 1.0 pre-stable cleanup; no prior deprecation required.
4e — Signature-AST diff for unlabeled migration-required changes
Independently of CHANGELOG content, compute the public API surface at base vs. HEAD by walking every symbol re-exported from the public modules in 4a:
- Base:
git show <base-ref>:<module.py>for each public module and for every real-source module it re-exports from → parse withast. Resolve each re-export's real signature at base. - Target: same as Base, but at the target ref — the working tree in pre-release / RC mode, or
git show v<target>:<module.py>in retrospective mode. Do NOT read the working tree in retrospective mode; later commits onmainwould otherwise be falsely attributed tovX.Y.Z. - For each symbol whose signature shape changed AND whose matching CHANGELOG entry (if any) doesn't indicate a rename / parameter shift / removal → add to Breaking Changes section as "unlabeled signature change — please verify".
Exception: Removed symbols are breaking by definition. A symbol that appears in CHANGELOG's Removed section does NOT need prose hedging to be valid. Do NOT flag Removed entries as "unlabeled breaking" — the section name itself communicates the breakage. Removed entries surface in the Breaking Changes callout and in the Changes-to-Existing-API section (as "removed" kind), but the report must not whinge about missing Breaking labels on them.
Exception: additive keyword-only parameters with defaults are not migration-required. A signature change that only adds new kwargs with defaults after * is additive; do not surface in Breaking Changes. It belongs in Changes-to-Existing-API with Kind new parameter and Breaking No.
4f — Semantic-change candidates from solver / integrator commits
Newton has no native C++/CUDA code (unlike Warp); there is no build-and-run verification step. However, changes to solver internals, integrators, collision pipelines, and math helpers can still change observable numerical behavior without altering any signature.
For each commit in commit_list_json that touches paths under newton/_src/solvers/**, newton/_src/sim/** (integrator / collision code), or newton/_src/math/**:
- Skip if the commit is already mapped to a CHANGELOG entry explicitly describing the semantic shift.
- Read the commit's diff:
git show --stat <sha>thengit show <sha>for small diffs, or read specific hunks for large ones. - Triage into one of three buckets:
- Clearly not semantic-breaking → drop. Examples: renaming internal symbols, comment / format changes, pure internal refactors, performance optimizations that preserve output, test-only changes, bug fixes where the pre-fix behavior was itself a bug.
- Clearly semantic-shifting with an obvious user-observable numerical / behavioral change visible from the diff alone → include in Breaking Changes with a short "Before / After" explanation derived from the diff and commit message.
- Ambiguous — the diff suggests the change could affect numerical output but Claude cannot tell from reading alone whether users would notice → add a concise entry in a "Review candidates" subsection of CHANGELOG Review Notes (NOT Breaking Changes). Include the commit, the touched file(s), and a one-sentence hypothesis. Do not speculate in Breaking Changes.
Do NOT attempt to build and run Newton at base vs. HEAD. Newton's solver outputs depend on Warp, MuJoCo, and GPU state; reproducing a numerical diff in a one-shot audit is not reliable and not worth the setup cost. When in doubt, route to the review-candidates list and leave verification to the release manager.
4g — Experimental-marker cross-reference
Some Newton features ship with an explicit experimental note in the CHANGELOG entry that introduced them (e.g., "Add differentiable rigid contacts (experimental)") or in docstrings. Changes to those features do NOT carry the same stability contract as changes to stable APIs. The report must reflect that so the release manager does not over-weight the concern.
For each entry in Breaking Changes, Changes to Existing API, and Removed (as collected through 4a–4f), determine whether the affected symbol or feature area is currently experimental:
- Collect candidate symbols / feature-area phrases from the entry: backticked identifiers, class names, and (for topic-style entries) the most distinctive descriptive noun phrase.
- Search CHANGELOG.md in released-version sections for bullets that name one of the candidates AND contain the literal substring
experimental(case-insensitive). Also match via GH ref if the current entry and a prior experimental entry share a GH number. Scope:- Pre-release / RC: all dated sections.
- Retrospective (current target is
X.Y.Z): everything below## [X.Y.Z]. The stability-promotion check (step 3 below) likewise only considers versions strictly beforeX.Y.Z.
- If a match exists AND there is no subsequent CHANGELOG bullet in a later released version explicitly promoting the symbol to stable (e.g., "Promote X out of experimental", "Stabilize Y"), the symbol is still experimental. Record: (a) the release version that introduced the symbol as experimental, (b) the full text of that introduction bullet.
- Also check the module source at HEAD for an in-code
.. experimental/Experimental:/experimental_api/@experimentalannotation on the symbol's declaration. If present, treat as experimental regardless of CHANGELOG signal.
Tag every matched entry internally as experimental=True. Do not alter the CHANGELOG text itself.
How the tag changes rendering:
- Breaking Changes heading for the entry: prefix with
Experimental:and do not use a warning emoji. Include a short sentence reminding readers that the symbol is opt-in and has a looser stability contract, with the release where the experimental marker was introduced. - Changes-to-Existing-API table: the Breaking column shows
Experimentalrather thanYes. - Release Highlights bullet (Phase 7a): if the item independently clears the significance bar, use a neutral
Experimental:label without a warning emoji. Experimental status alone does not make an item a highlight.
Never drop the entry from the detailed audit. Experimental status changes the stability interpretation, not the underlying API diff. A removed or signature-changed experimental symbol still appears in Breaking Changes and Changes to Existing API, but it is not presented as a release-manager warning.
Phase 5 — Review CHANGELOG language and bake
5a — Language review
Read references/language-review-examples.md. For EACH CHANGELOG entry, apply LLM judgment:
- 🔗 Wrong ref (tier-1): for every GH ref in the entry, fetch the mapped commits' subjects and paths. If the entry topic doesn't match the commits' actual scope, flag.
- 🔗 Wrong ref (tier-2): if
gh --versionandgh auth statusboth succeed, rungh issue view <num> --json title,bodyper ref and compare issue title to entry topic. Skip silently ifghunavailable. - 🗣️ Internal language: internal module paths (
newton._src.*), private identifiers with a leading underscore, Warp-internal types (wp.types.*that are not documented user types), implementation-detail verbs ("refactor", "reorganize", "rewrite") without a user-visible outcome. - 📝 Too terse: under ~10 words with no context, or missing migration guidance in a Deprecated / Changed entry that names a rename or removal.
- 🕵️ Private-only symbol: the CHANGELOG
### Addedentry names a symbol that exists only innewton._src.*at HEAD and is not re-exported through a public module. See Phase 4a. - ⬆️ Missing migration guidance (Newton-specific): entries in
### Deprecated,### Removed, or### Changed(where the prose indicates rename / reorder / removal) MUST include migration guidance perchangelog/README.md("UseYinstead", "in favor ofY", "switch toY"). Flag entries that rename or remove symbols without pointing to the replacement. - 🏷️ Naming-convention drift (Newton-specific): new public symbols in
### Addedwhose names violate Newton's prefix-first convention (e.g.,PDActuatorshould beActuatorPD;add_sphere_shape()should beadd_shape_sphere()). SeeCODING_GUIDELINES.rst.
Record flagged entries. Keep the FULL entry text in the audit table — do not truncate.
5b — Bake aggregation
From commit_list_json:
- Bucket each commit's
days_in_maininto 🟢 (>14 days), 🟡 (7–14 days), 🟠 (<7 days). Commits whosedays_in_mainisnull(no main equivalent) skip bucketing — do NOT coercenullto0or compare it to a
…(truncated)