From a1b2828eeee2853b97f3d6185362355fb77635b2 Mon Sep 17 00:00:00 2001 From: Chris Purcell <168346341+chrisdpurcell@users.noreply.github.com> Date: Tue, 1 Sep 2026 16:54:30 -0400 Subject: [PATCH] docs(release): sequence R2 before --apply-pins, retarget R3, merge CHANGELOG dupes R2 previously ran only --apply-pins, a standalone mode that never bumps pyproject/relocks/converts CHANGELOG (release_prep.py's own docstring: "a separate mode, not an extra step of the default run"). Add the default run first, then --apply-pins, matching the code and R7's claimed commit contents. R3 still told the operator to hand-bump the four allow-listed pin files (README/UPGRADING/mcp-server/activation-constant) that --apply-pins now owns, reinstating the line-number editing hazard #227 E3 item 5 removed, and never mentioned meta/versioning.md, which the allow-list also covers. R3 now names the allow-listed sites from _RELEASE_PIN_SITES/_PACKAGE_PIN_PATH and narrows to the judgment sites the allow-list deliberately excludes. CHANGELOG.md [Unreleased] had duplicate sibling ### Changed headings (MD024 is disabled, so nothing caught it) and filed the new `standards cut-successor` command under Changed instead of Added. Merged into one Added and one Changed section, preserving every bullet. tests/README.md's CI-relationship bullet still named the deleted .github/workflows/coherence.yml as a live, separately named status; #236 C2 folded that coverage into check.yml's ordinary pytest step. Repointed the prose at that step instead of the dead link. Refs #227, #236 --- CHANGELOG.md | 6 +----- docs/reference/release-runbook.md | 16 ++++++++++------ tests/README.md | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26953dac..d63ae223 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,17 +41,13 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version - **`project-standards packages check-release --staged` reports a mid-train working tree without a false red.** Between a landed payload cut and release prep, `.standards/`, the catalog projection, and the project version legitimately lag the catalog, so the command reported `PC-RELEASE-LEVEL`, `PC-RELEASE-PROJECTION`, and `PC-RELEASE-PROJECT-VERSION` and exited `1` on a tree that was correct for its phase ([#227](https://github.com/L3DigitalNet/project-standards/issues/227), [#236](https://github.com/L3DigitalNet/project-standards/issues/236)). `--staged` labels exactly those three codes expected pre-bump — still printed, prefixed `EXPECTED-PRE-BUMP` — and exits `0` when nothing else is found; every other code, including `PC-RELEASE-PAYLOAD-MUTATED`, `PC-CATALOG-DIGEST-REPLACED`, and `PC-RELEASE-PACKAGE-CURRENT`, still fails. Under `--json` the object additively gains `staged` and `expected_pre_bump`. Without the flag, output and exit status are unchanged. - **A committed release runbook** ([`docs/reference/release-runbook.md`](docs/reference/release-runbook.md)) replaces the per-train session log with a durable procedure: every step's command, exit evidence, and rollback, plus a Layer ownership table stating which gate layer (leg, integrated gate, release prep, hosted Check, publication, or periodic) owns each proof ([#236](https://github.com/L3DigitalNet/project-standards/issues/236)). +- **`project-standards standards cut-successor ` performs the mechanical half of a payload cut.** Released payload bytes are immutable, so every fix and every feature reaches a published family as a new `versions//` directory copied from its predecessor — a procedure with four declaration sites that had to be moved by hand and in the right order. The command copies the predecessor tree, stamps the copied `payload.toml` with the new version and freshly computed per-file digests, computes the aggregate through the same `validate_payload_integrity` the repository validators use, indexes the payload in the family's `standard.toml`, adds the catalog entry immediately after the family's last one, and then reruns `sync-payload-projection` and `render-catalog`. The successor inherits the predecessor's catalog role and only a `default` predecessor is demoted to `retained`, so a reference-only or internal family does not acquire a default. Every line in the new tree that still names the predecessor is **reported, never rewritten** — a permalink or a migration `from` endpoint is correct history that a blanket substitution would corrupt — with one exception the payload contract leaves no choice about: a migration's `to` endpoint must name its containing version, and is re-pointed and reported as an applied edit. `--from` selects a predecessor other than the family's newest, `--dry-run` prints the plan and writes nothing, `--scaffold-test` writes the successor's contract-test module with the cut's mechanical assertions and a TODO block listing the predecessor test's behavior cases, and the command refuses outright when the successor directory already exists ([#227](https://github.com/L3DigitalNet/project-standards/issues/227)). ### Changed - **Hosted CI collapses to one gate on `main`, no longer duplicated on pull requests** ([#236](https://github.com/L3DigitalNet/project-standards/issues/236)). `Check` and `Validate standards graph` now trigger on `push: branches: [main]` and `workflow_dispatch` only — neither runs on `pull_request` or on `testing` any more. `Coherence` is deleted outright: `tests/coherence` carries neither the `performance` nor the `compatibility` marker, so it was already selected by `Check`'s ordinary pytest step and the standalone workflow was pure duplication. The seven cheap workflows (format, lint-markdown, validate-markdown-frontmatter, validate-specs, validate-standards, go, and any other under two minutes) keep their `pull_request` triggers unchanged. - **`scripts/verify.sh` stops a battery at the first red lane and sizes the `--full` compatibility lane for the machine that runs it** ([#236](https://github.com/L3DigitalNet/project-standards/issues/236)). `--fail-fast` skips every remaining serial lane once one has come back red and is the default for `--full`, where roughly 35 minutes of compatibility matrix ran after the ordinary lane had already failed on the 2026-09-01 train; `--keep-going` restores the run-every-lane behaviour and stays the default for the fast gate, whose three lanes are already running when the first red appears. A lane cut short is reported in the lane table as `skipped (--fail-fast)`, never omitted. `VERIFY_FULL_COMPAT_WORKERS` now defaults to `16` instead of a literal tuned for the retired 21-core workstation. Repository tooling only: no package, payload, or consumer-visible byte changes. - **`README.md` and the release runbook now state the install path explicitly**: the immutable Git tag is the only supported way to install a release, and GitHub release assets are convenience copies without a per-release byte-verification or reproducibility guarantee — those checks move to a periodic cadence instead ([#236](https://github.com/L3DigitalNet/project-standards/issues/236)). - -- **`project-standards standards cut-successor ` performs the mechanical half of a payload cut.** Released payload bytes are immutable, so every fix and every feature reaches a published family as a new `versions//` directory copied from its predecessor — a procedure with four declaration sites that had to be moved by hand and in the right order. The command copies the predecessor tree, stamps the copied `payload.toml` with the new version and freshly computed per-file digests, computes the aggregate through the same `validate_payload_integrity` the repository validators use, indexes the payload in the family's `standard.toml`, adds the catalog entry immediately after the family's last one, and then reruns `sync-payload-projection` and `render-catalog`. The successor inherits the predecessor's catalog role and only a `default` predecessor is demoted to `retained`, so a reference-only or internal family does not acquire a default. Every line in the new tree that still names the predecessor is **reported, never rewritten** — a permalink or a migration `from` endpoint is correct history that a blanket substitution would corrupt — with one exception the payload contract leaves no choice about: a migration's `to` endpoint must name its containing version, and is re-pointed and reported as an applied edit. `--from` selects a predecessor other than the family's newest, `--dry-run` prints the plan and writes nothing, `--scaffold-test` writes the successor's contract-test module with the cut's mechanical assertions and a TODO block listing the predecessor test's behavior cases, and the command refuses outright when the successor directory already exists ([#227](https://github.com/L3DigitalNet/project-standards/issues/227)). - -### Changed - - **`scripts/release_prep.py --apply-pins` rewrites the release runbook's mechanical pin sites instead of a hand-addressed `sed` batch** ([#227](https://github.com/L3DigitalNet/project-standards/issues/227) E3 item 5). Step R3 of every train was a dozen line-addressed `sed -i` commands whose line numbers had to be re-derived first, because any documentation change landed since the runbook was written shifts them and a mis-addressed edit is silent. The new mode rewrites only sites named by an explicit (path, matcher) allow-list — the README, `UPGRADING.md`, and `docs/mcp-server.md` install, `--version`, pre-commit `rev` and wheel-filename lines, `_RELEASE_VERSION`, and the `meta/versioning.md` package-release contract lines that the release-version sweep never reported and that were left stale through the v5.28.0 train — and only when the matched value is the outgoing version. Judgment sites (`ROADMAP.md`, `UPGRADING.md` history headings, `_BASELINE_REF`) stay reported and untouched by construction. `--apply-pins --dry-run` prints a unified diff and writes nothing; applying still requires the release branch. - **`make release-reconcile` and `make release-golden RELEASE=X.Y.Z` sequence release steps R4 and R5** ([#227](https://github.com/L3DigitalNet/project-standards/issues/227) E3 item 7), whose only sequencer was the runbook prose. The golden-fixture target encodes the recurring trap that `--output` is resolved relative to `--root`, so the synthetic fixture is re-rendered rather than the real repository catalog. - **`make go-check` skips the three-build Go reproducibility proof when nothing it depends on has changed** ([#227](https://github.com/L3DigitalNet/project-standards/issues/227) E3 item 8). `scripts/go-verify-stamp.sh` keys on `internal/**`, `cmd/**`, `go.mod`, `go.sum`, the build scripts, the shared build library, and the committed artifact bytes themselves, so replacing a committed binary without touching its source still re-triggers the proof. A missing, stale, or indeterminate stamp runs it, the stamp is written only after it passes, and `GO_VERIFY_FORCE=1` forces it for the post-publish proof. diff --git a/docs/reference/release-runbook.md b/docs/reference/release-runbook.md index a61d0629..cb01c2d6 100644 --- a/docs/reference/release-runbook.md +++ b/docs/reference/release-runbook.md @@ -56,8 +56,8 @@ A proof that reappears in two layers is a defect in this table, not thoroughness | --- | --- | --- | --- | | R0 | Read-only pre-flight; record the rollback anchor | — | no | | R1 | Push `testing`; fast-forward `main` from `testing` | — | no (local `main` re-pointable) | -| R2 | `release_prep.py $RELEASE --apply-pins` (dry run, then applied) | L2 | no | -| R3 | Hand pin bumps the script does not own (README/UPGRADING/mcp-server/activation constants) | — | no | +| R2 | `release_prep.py $RELEASE` (bump/relock/changelog), then `--apply-pins` (dry run, then applied) | L2 | no | +| R3 | Hand pin bumps the allow-list deliberately excludes (judgment sites only) | — | no | | R4 | `make release-reconcile` (dogfooded control-plane reconcile + validate) | L1 (validators) | no | | R5 | `make release-golden RELEASE=$RELEASE` | L2 | no | | R6a | Rebuild the candidate wheel/sdist locally; record artifact digests | — | no | @@ -111,20 +111,24 @@ ROLLBACK: `git checkout testing && git branch -f main ` (nothi ## R2 — `release_prep.py` +`--apply-pins` is a separate mode, not an extra step of the default run (`scripts/release_prep.py`'s own module docstring): it rewrites only the allow-listed pin sites and exits. The default run — the version bump, relock, and CHANGELOG conversion — must land and be reviewed first; only then do the pin rewrites make sense against the bumped tree. + ```bash +uv run python scripts/release_prep.py $RELEASE --dry-run # read it; nothing is written +uv run python scripts/release_prep.py $RELEASE uv run python scripts/release_prep.py $RELEASE --apply-pins --dry-run # read it; nothing is written uv run python scripts/release_prep.py $RELEASE --apply-pins ``` -OBSERVABLE: `pyproject.toml` at `$RELEASE`; `uv.lock` relocked; `CHANGELOG.md` carries a new `## [$RELEASE] — ` section moved from `## [Unreleased]`. The script's own summary states the `check-release` baseline it used. A FAILED step naming anything other than the catalog projection or release classification (both fixed downstream, by R4 and R3 respectively) is real; stop. +OBSERVABLE: after the default run, `pyproject.toml` is at `$RELEASE`; `uv.lock` is relocked; `CHANGELOG.md` carries a new `## [$RELEASE] — ` section moved from `## [Unreleased]`. The script's own summary states the `check-release` baseline it used. A FAILED step naming anything other than the catalog projection or release classification (both fixed downstream, by R4 and R3 respectively) is real; stop. After `--apply-pins`, `git diff --stat` touches only the allow-listed pin sites (`scripts/release_prep.py`'s `_RELEASE_PIN_SITES` plus `meta/versioning.md`). -ROLLBACK: `git checkout -- pyproject.toml uv.lock CHANGELOG.md`. +ROLLBACK: `git checkout -- pyproject.toml uv.lock CHANGELOG.md README.md UPGRADING.md docs/mcp-server.md tests/package_contract/test_current_catalog_activation.py meta/versioning.md`. ## R3 — hand pin bumps -Re-derive line numbers from the pin-site discovery grep immediately before editing — any doc change landed since this runbook was last run can shift them. Bump only sites classified **bump**; leave sites classified **leave** untouched, including the activation-constant baseline reference (it moves in the payload-cut commit that stages a new activation, not in the release commit). +R2's `--apply-pins` already rewrote every allow-listed site (`scripts/release_prep.py`'s `_RELEASE_PIN_SITES`: README.md's install-tag/wheel-artifact/version-report/product-prose/precommit-rev, UPGRADING.md's install-tag/version-report/upgrade-target, docs/mcp-server.md's install-tag/wheel-artifact/version-report, and tests/package_contract/test_current_catalog_activation.py's release-constant — plus `_PACKAGE_PIN_PATH`, `meta/versioning.md`'s package-contract-prose pin). This step is only the judgment sites the allow-list deliberately excludes: run the pin-site discovery grep and classify each remaining hit against R2's diff. **bump** sites still needing a hand edit are typically `ROADMAP.md`, UPGRADING.md's history section headings (never its allow-listed pin, which R2 already moved), and `_BASELINE_REF`; **leave** sites are deliberate released history (a permalink, a changelog heading, a fixture pinning the release that first shipped something) and stay untouched, including the activation-constant baseline reference (it moves in the payload-cut commit that stages a new activation, not in the release commit). -OBSERVABLE: `git grep -n '' -- README.md UPGRADING.md docs/mcp-server.md tests/package_contract/` returns only the sites classified **leave**, and nothing else. `git diff --stat` touches only the files the classification named. +OBSERVABLE: `git grep -n '' -- .` returns only sites classified **leave**, plus the sites R2's `--apply-pins` already rewrote (now at `$RELEASE`). `git diff --stat` (this step only) touches only the files the classification named as **bump**. ROLLBACK: `git checkout -- `. diff --git a/tests/README.md b/tests/README.md index 57171231..fe3a7abe 100644 --- a/tests/README.md +++ b/tests/README.md @@ -215,7 +215,7 @@ When you add a new **subpackage** with orthogonal concerns (like `adopt/`), spli Repository CI uses deliberately separate enforcement workflows against this repo's own code and content: - **The developer gate** ([.github/workflows/check.yml](../.github/workflows/check.yml)) runs the full verification sequence on push and PR, on Python 3.14: `ruff format --check`, `ruff check`, `basedpyright`, then builds and extracts the candidate wheel, runs the ordinary suite under coverage with the extracted wheel first on `PYTHONPATH` (xdist `-n 4`, matching the hosted runner's 4 vCPU, then `coverage combine` — under xdist the controller process measures nothing itself, so combine promotes the workers' data files into the report), the xdist compatibility matrix, the serial performance gates, `coverage report`, and `pip-audit`. This protects the validator's own logic. The `glob('**')` behaviour change is guarded directly by its version-independent regression test (`test_exclude_dir_glob_matches_nested_files`, which exercises the `fnmatch`-based exclusion), so the gate no longer needs a Python version matrix to bracket it (see the Regression layer above). -- **The coherence gate** ([.github/workflows/coherence.yml](../.github/workflows/coherence.yml)) intentionally repeats the behavioral coherence coverage in the developer gate so the repository retains a separately named coherence status. It proves that the shipped `.markdownlint.json` and `.prettierrc.json` are co-satisfiable — markdownlint accepts Prettier's output over the corpus in `tests/coherence/corpus/`. Both this job and the developer gate install the pinned Node dependencies with `npm ci`, so running `tests/coherence` locally requires `npm ci` first. +- **Coherence coverage** proves that the shipped `.markdownlint.json` and `.prettierrc.json` are co-satisfiable — markdownlint accepts Prettier's output over the corpus in `tests/coherence/corpus/`. `tests/coherence` carries neither the `performance` nor the `compatibility` marker, so it runs as part of the developer gate's ordinary pytest step ([.github/workflows/check.yml](../.github/workflows/check.yml)) rather than a separately named workflow; running it locally requires `npm ci` first. - **The standards-graph gate** ([.github/workflows/validate-standards-graph.yml](../.github/workflows/validate-standards-graph.yml)) enforces the package contract: `standards validate-packages`, `standards validate-graph --require-all-manifests`, `standards generate-package-schemas --check`, `standards sync-payload-projection --check`, `standards render-catalog --check`, then resolves the newest released V2 baseline tag and runs `packages check-release` so a push can never publish a forbidden transition. - **The dogfood caller** ([.github/workflows/validate-standards.yml](../.github/workflows/validate-standards.yml)) invokes the reusable frontmatter workflow against this repository's own Markdown — proving the consumer entry point works here before any downstream repo pins it.