Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <standard-id> <version>` 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/<version>/` 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 <standard-id> <version>` 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/<version>/` 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.
Expand Down
16 changes: 10 additions & 6 deletions docs/reference/release-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -111,20 +111,24 @@ ROLLBACK: `git checkout testing && git branch -f main <PRE_RELEASE_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] — <date>` 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] — <date>` 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 '<PREV-without-v>' -- 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 '<PREV-without-v>' -- .` 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 -- <each file this step touched>`.

Expand Down
2 changes: 1 addition & 1 deletion tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down