From 28f62770015847198c337de54b8023edb41f897c Mon Sep 17 00:00:00 2001 From: kikashy <35789537+kikashy@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:08:30 -0400 Subject: [PATCH 1/5] Prepare the v0.3.0 final: immutable notes, the retained-column leaf in the final bundle, a release draft that is not a pre-release (#231) - releases/v0.3.0.md: the final notes, from the prepared notes, naming both candidates (rc.1 2026-10-25, rc.2 2026-10-27) and what each carried, the selected retained-column/1 leaf as part of the release, RFC 0024 as not in it, the Policy companion unchanged, what the release pins, and a checks sentence whose run id and commit are completed at the tag. docs/releases/v0.3.0.md becomes a pointer (the published RC1 notes link to it); the site's 0.3.0 release page renders the immutable notes. - tools/build_release.py: the v0.3.0 bundle carries the leaf (the RC2 reader paths, releases/v0.3.0.retained-column.json and the milestone link) and no mapping. The final is held to RC2's leaf rules (inventory, RFC 0029 acceptance, draft maturity, milestone #2) and refused when its inventory differs from the published RC2 inventory in anything but its distribution, when either inventory is missing, or when a mapping sits beside a tag the RC allowlist does not name. - releases/v0.3.0.retained-column.json: RC2's inventory with distribution v0.3.0; every digest is unchanged and recomputed by the builder. - release.yml: the evidence job hands its route (the tag grammar) to the release job; a plain vMAJOR.MINOR.PATCH tag is drafted as a release, every other tag and any other route value as a pre-release. - VERSIONING.md names the rc.N and alpha.N forms; CHANGELOG.md records 0.3.0 and, for the move to be exact, 0.2.0-draft under their own headings, with RFC 0027 (0.4.0) left under Unreleased. Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- .github/ci-evidence.json | 2 +- .github/workflows/release.yml | 18 +- CHANGELOG.md | 236 +++++++++++++++------------ VERSIONING.md | 4 + docs/releases/v0.3.0.md | 85 +--------- releases/v0.3.0.md | 138 ++++++++++++++++ releases/v0.3.0.retained-column.json | 39 +++++ tests/test_historical_release.py | 2 +- tests/test_release.py | 54 +++++- tests/test_release_evidence.py | 54 +++++- tests/test_retained_release.py | 116 ++++++++++++- tests/test_site.py | 20 +++ tools/build_release.py | 52 ++++-- web/DEPLOYMENT.md | 9 +- web/build.py | 2 +- 15 files changed, 611 insertions(+), 220 deletions(-) create mode 100644 releases/v0.3.0.md create mode 100644 releases/v0.3.0.retained-column.json diff --git a/.github/ci-evidence.json b/.github/ci-evidence.json index 9e14cd3..252d389 100644 --- a/.github/ci-evidence.json +++ b/.github/ci-evidence.json @@ -19,7 +19,7 @@ "release tag, commit, clean tree, artifact pins and Policy tag history (tools/build_release.py)", "immutable release bundles and checksums", "release notes", - "draft prerelease", + "draft release, marked pre-release for every tag outside vMAJOR.MINOR.PATCH", "bundle attestation" ], "profiles": { diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 41360ed..78711e0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: # run again when the evidence is reused; a refusal fails the release, and the fresh route is the # operator's choice. A tag the gate does not admit (a release candidate, a policy-v tag) reuses # nothing: the release job runs the whole suite itself, as before. The tag, milestone, Policy, - # bundle, notes, prerelease and attestation steps of the release job stay fresh either way. + # bundle, notes, draft and attestation steps of the release job stay fresh either way. evidence: name: CI evidence at the tag runs-on: ubuntu-latest @@ -28,9 +28,11 @@ jobs: # Reading this repository's workflow runs, jobs and artifacts. actions: read # true only when the gate exited 0 and its decision record says reuse; anything else, an empty - # value included, makes the release job run the whole suite. + # value included, makes the release job run the whole suite. route is the tag grammar alone + # (evidence for vMAJOR.MINOR.PATCH, fresh otherwise); the draft step reads it, not the evidence. outputs: reuse: ${{ steps.reused.outputs.reuse }} + route: ${{ steps.route.outputs.route }} steps: # The gate's tag grammar: vMAJOR.MINOR.PATCH in ASCII digits, no leading zeroes, nothing after. - name: Choose reuse or the whole suite at the tag @@ -191,18 +193,26 @@ jobs: --commit "${GITHUB_SHA}" --output dist - - name: Create draft prerelease + # A plain vMAJOR.MINOR.PATCH tag is drafted as a release the maintainer publishes. Every other + # tag (a release candidate, a policy-v tag), and any route value but evidence, is drafted as a + # pre-release. + - name: Create draft release shell: bash run: | + prerelease=(--prerelease) + if [[ "${RELEASE_ROUTE}" == "evidence" ]]; then + prerelease=() + fi gh release create "${GITHUB_REF_NAME}" \ dist/* \ --draft \ - --prerelease \ + "${prerelease[@]}" \ --verify-tag \ --title "Judgment Pack Specification ${GITHUB_REF_NAME}" \ --notes-file "releases/${GITHUB_REF_NAME}.md" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_ROUTE: ${{ needs.evidence.outputs.route }} - name: Attest release bundles uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index c47b795..0f73886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,19 @@ here. analysis, and carries explicit resource limits and hand-written positive/adversarial cases. This targets specification `0.4.0`; it changes no Core resolution or released conformance claim. +## `0.3.0` + +The first Core release without a `-draft` suffix, at maturity `draft`, after the candidates +`v0.3.0-rc.1` (2026-10-25) and `v0.3.0-rc.2` (2026-10-27); the +[release notes](releases/v0.3.0.md) give its scope and migration note. + +### Added + +- The selected `retained-column/1` leaf ([RFC 0029](rfcs/0029-retained-column-conversions.md), + accepted at draft maturity; #274): the [selected retained columns](spec/retained-column.md) + companion, the versioned schema `schema/retained-column/1/contract.schema.json`, the corpus + `conformance/retained-column/1/` and the two offline readers. It was first distributed in + `v0.3.0-rc.2`; the final carries the same bytes under `releases/v0.3.0.retained-column.json`. - Name the next Core release `0.3.0`, define conformance maturity independently, and require `v0.3.0-rc.1` before release (#231). - Re-pin released Core schemas, manifests, packs, vectors, readers, and site routes to exact `specVersion` `0.3.0`, preserving `0.2.0-draft` artifacts (#231). @@ -237,71 +250,6 @@ here. questions now render as stable semantic headings without adding executable JavaScript, and a concise experimental `/llms.txt` navigation aid points machine readers to the normative, informative, proposed, and companion-project surfaces without claiming crawler or ranking support. -- **Core `0.2.0-draft`: the evaluator conformance class.** [RFC 0006](rfcs/0006-evaluator-conformance.md) - is accepted at draft maturity and lands in the specification: §3.4 defines the class and §3.4.1 the - single form of claim permitted against it, §§7–8 become normative for that class (and stay - informative for every other consumer), §8.2 defines the evaluation inputs including the tri-state - evidence-availability document, §8.3 pins the portable disposition, and §8.4 makes an evaluation - error something other than a result — in a fixed precedence order where more than one class applies, - with implementation-defined classes required to use §9's reverse-domain form. §7.4 requires every - operator of an implementation claiming the class, states that equality of decimal strings is string - equality and deliberately not decimal-aware, and confines "cannot compare exactly" to JSON numbers - outside an implementation's exact range — the single seam §8.3 excludes from its byte-agreement - requirement and §13 leaves open. §3.4.1 freezes the corpus at the release of a `specVersion` and names - `suiteVersion` as the corpus version a claim must state. §10 raises collection-size and - evaluation-work limits to a MUST for implementations claiming the class. Document conformance is unchanged, and §3.5's non-claims - apply to the new class with one narrow, stated exception. Full scope and caveats: - [`releases/v0.2.0-draft.md`](releases/v0.2.0-draft.md). -- A 20-case **seed** evaluation corpus in [`conformance/evaluation/`](conformance/evaluation/README.md), - with its own case carrier and four pack fixtures. Thirteen rows are imported from the - cross-implementation agreement harness — ten over the nine walked RFC 0006 appendix instances, - counting both variants of instance 7, plus three probes. Seven are constructed here from the - specification text — reproduced on one of those two implementations, which checks the derivation - rather than establishing agreement between two of them: the handoff subset rule and - `state: none`, `no-match` with no fallback, a direct exception escalation in a pack with no - `escalation` object, and §7.4's ordered comparison including its refusal to coerce a JSON number. It - is normative for the evaluator class only, version-pinned, and grows by RFC. Its README states its - gaps: no error rows, no number-representability row (deliberately — that question is open), handoff - coverage counted row by row, three mandatory operators (`not-equals`, `greater-than`, - `less-than-or-equal`) with no row, no `literal`, `not`, composite-equality, or fallback-selection row, - no `suppress-rule` row, no permutation or hostile rows, and the inputs its carrier cannot yet express. -- **The claim's scope is the contract, not the corpus (§§3.4, 3.4.1, 3.5).** An evaluator-conformance - claim asserts compliance with the whole §§7–10 contract for every input the implementation admits; - corpus results are required, non-exhaustive evidence of that. A claim must state that every row of the - named corpus version passed, and a failed row blocks the claim. -- **An erratum mechanism for a frozen corpus** at - [`conformance/evaluation/errata.md`](conformance/evaluation/errata.md). Only a project-issued, versioned - erratum can mark a row of a released corpus defective — a claimant cannot — and an erratum edits no row. - It is in place and empty for `suiteVersion` `0.2.0-draft`. -- **Input preflight (§8.2).** The inputs are admitted before §8 runs, in the order pack, facts, evidence - availability; an omitted evidence document is the implicit empty object; any violation of §8.2's shape, - including a non-object evidence input, is `malformed-input`; and because preflight completes before step - 1, an input error can never be outraced by a `not-applicable` result. -- Two optional case members in the evaluation carrier, defined now and used by no row so that later rows - need no carrier change: `workBudget` (a positive integer of work units, in the units a future accounting - model will define) and `expectedErrorPhase` (`preflight` or `evaluation`, alongside `expectedErrorClass` - only). -- A top-level `FAQ.md`; an informative, non-normative architecture-vision page - (`docs/architecture/vision.md`); a prior-art comparison page (`docs/concepts/comparison.md`); and - seeded draft RFCs 0001–0005 (pack manifest, Judgment Graph composition, evidence reference, - planner interface, and pack discovery) that record open design questions without adding anything - normative. -- Site navigation: a Concepts hub (`concepts/index.html`) gathering the conceptual and reference - pages, breadcrumbs on every page, and footer links to the Concepts and Project & docs hubs, so - every page is reachable through structured navigation rather than only through in-prose links. -- Static SVG architecture diagrams (shipped-versus-proposed, the three-property split, and evidence - sources feeding a pack) on the architecture-vision page, rendered without any JavaScript so the - strict `script-src 'none'` policy is preserved. -- FAQ: a "Skills, tools, and agent integration" section (with a "Skills and Tools" subsection) - that explains, honestly, how Judgment Packs relate to tools, agent skills, and `SKILL.md` — - including when a skill alone is sufficient — with a static SVG integration diagram. -- A `NOTICE` file naming Brian Jin as the copyright holder, included in the release bundle. The - repository previously identified no owner: `LICENSE` is the unmodified Apache-2.0 text, whose - appendix is a template rather than a filled-in field. `LICENSE` is deliberately left - byte-identical to the canonical text so automated license scanners continue to report a clean - Apache-2.0 match. -- A superseded-draft signpost at `/spec/0.1.0-draft/`, so a versioned URL that citers may already have - used resolves to the tagged source and the current draft instead of a 404. ### Changed @@ -586,46 +534,6 @@ here. receipt version 3 exists and is neither independent nor clean-room. Two update statements marked "at the time of writing": runtime 0.21.0 is released, and the demo's Acts 7 and 8 are merged. The descriptions of this directory no longer call it open proposals only. -- Version identifiers move to `0.2.0-draft`: Core prose, the structural schema (`$id` and the exact - `specVersion` constant), the document-conformance manifest, every example, and every fixture. The - document format is unchanged, so a `0.1.0-draft` pack is unchanged in representation and - document-conformance meaning and needs only that one value edited; because the value is exact, an - unedited pack is not structurally conforming here. Re-declaration is not inert, though: it opts the pack - into this draft's evaluator semantics, which existed for no consumer under `0.1.0-draft`, and confers no - conformance on any implementation. The `0.1.0-draft` schema is preserved at - `schema/judgment-pack-core-0.1.0-draft.schema.json`, and that draft's document-conformance manifest - schema at `schema/conformance-manifest-0.1.0-draft.schema.json`, both served at the `$id` each was - published under so a previously cited identifier still resolves. Each preserved file matches the tagged - `v0.1.0-draft` artifact in every byte except its `$id` member, which was re-pointed to the project's - neutral domain when the retired pre-publication identifier was withdrawn; `releases/v0.2.0-draft.md` - publishes the digests of both forms. `conformance/structural/invalid-spec-version.json` now carries - `0.1.0-draft` as its negative value, which makes the re-declaration requirement a corpus case. -- §8.4's classes are split by phase so the error contract cannot contradict §10: a documented document or - carrier limit reached while admitting an input is `malformed-input` (§2.1 refuses the document, so - preflight never admits it), while a collection-size or evaluation-work limit reached during evaluation is - `resource-exhaustion`. Every error carries exactly one class — a Core class where one applies, otherwise - a documented implementation-defined class in §9's reverse-domain form. -- `tools/build_release.py` now requires `HEAD` to be the resolved release commit. Artifacts are validated - from the worktree and archived from the commit, so the gate previously accepted a validated tree and an - archived tree that were not the same tree. -- The evidence behind the change is two implementations agreeing 13/13. Both trace to one maintainer's - direction, so that agreement corroborates the semantics rather than independently confirming them, - and RFC 0000's bar for a *stable* feature is still unmet — carried as a caveat in the RFC's adoption - record, the release notes, and the corpus README rather than stated once and forgotten. -- Every assertion that evaluator conformance cannot be claimed is now the `0.2.0-draft` truth: the - class exists, claims are governed by §3.4.1, and no implementation ships a claim with this release. - The reference runtime's evaluator remains experimental and claims nothing; whether it claims the - class is a decision for that repository. The site says the same: the implementations listing no longer - says the runtime does not evaluate rules, and the example guides no longer call a conflict result - unportable now that §8.3 makes it a portable `unresolved` disposition. - -- Renamed the change-proposal process from Judgment Enhancement Proposal (JEP) to Request for - Comments (RFC); moved `jeps/` to `rfcs/`. The required sections and evidence bar are unchanged. - The historical `0.1.0-draft` entry below is left as shipped. -- Implementations page: replaced the earlier vendor-branded CLI listing with the vendor-neutral - `judgment-pack` reference runtime (which inherited that CLI's implementation), linking to its own - repository, and removed the vendor-branded on-site CLI page. Every page on the site is now - vendor-neutral, and no page names the retired pre-publication brand. - **The first evaluator-conformance claim now exists, and the site stops saying it does not.** The reference runtime states one for `0.2.0-draft` in its own repository. Four surfaces asserted the opposite as present fact and are corrected: the overview (`README.md`), the architecture vision's @@ -633,7 +541,7 @@ here. where it lives, and none restates any part of it — a partial restatement is what §3.4.1 forbids, and the distinction the corrected text carries is that `experimental` on a runtime surface reports *stability* and never conformance. The dated records are deliberately not edited: the - `0.2.0-draft` release notes and the entries above describe what was true at that release. + `0.2.0-draft` release notes and its entries below describe what was true at that release. - Implementations page: added the clean-room Python evaluator as a second listing, with a stated caveat that both implementations trace to one maintainer and so corroborate the specification's precision rather than evidencing independent interoperability. @@ -675,6 +583,122 @@ here. [staged-dialect]: https://github.com/Judgment-Pack/judgment-pack-spec/tree/main/conformance/dialect +## `0.2.0-draft` — 2026-07-28 + +The draft that added the evaluator conformance class; its +[release notes](releases/v0.2.0-draft.md) give its scope and limitations. + +### Added + +- **Core `0.2.0-draft`: the evaluator conformance class.** [RFC 0006](rfcs/0006-evaluator-conformance.md) + is accepted at draft maturity and lands in the specification: §3.4 defines the class and §3.4.1 the + single form of claim permitted against it, §§7–8 become normative for that class (and stay + informative for every other consumer), §8.2 defines the evaluation inputs including the tri-state + evidence-availability document, §8.3 pins the portable disposition, and §8.4 makes an evaluation + error something other than a result — in a fixed precedence order where more than one class applies, + with implementation-defined classes required to use §9's reverse-domain form. §7.4 requires every + operator of an implementation claiming the class, states that equality of decimal strings is string + equality and deliberately not decimal-aware, and confines "cannot compare exactly" to JSON numbers + outside an implementation's exact range — the single seam §8.3 excludes from its byte-agreement + requirement and §13 leaves open. §3.4.1 freezes the corpus at the release of a `specVersion` and names + `suiteVersion` as the corpus version a claim must state. §10 raises collection-size and + evaluation-work limits to a MUST for implementations claiming the class. Document conformance is unchanged, and §3.5's non-claims + apply to the new class with one narrow, stated exception. Full scope and caveats: + [`releases/v0.2.0-draft.md`](releases/v0.2.0-draft.md). +- A 20-case **seed** evaluation corpus in [`conformance/evaluation/`](conformance/evaluation/README.md), + with its own case carrier and four pack fixtures. Thirteen rows are imported from the + cross-implementation agreement harness — ten over the nine walked RFC 0006 appendix instances, + counting both variants of instance 7, plus three probes. Seven are constructed here from the + specification text — reproduced on one of those two implementations, which checks the derivation + rather than establishing agreement between two of them: the handoff subset rule and + `state: none`, `no-match` with no fallback, a direct exception escalation in a pack with no + `escalation` object, and §7.4's ordered comparison including its refusal to coerce a JSON number. It + is normative for the evaluator class only, version-pinned, and grows by RFC. Its README states its + gaps: no error rows, no number-representability row (deliberately — that question is open), handoff + coverage counted row by row, three mandatory operators (`not-equals`, `greater-than`, + `less-than-or-equal`) with no row, no `literal`, `not`, composite-equality, or fallback-selection row, + no `suppress-rule` row, no permutation or hostile rows, and the inputs its carrier cannot yet express. +- **The claim's scope is the contract, not the corpus (§§3.4, 3.4.1, 3.5).** An evaluator-conformance + claim asserts compliance with the whole §§7–10 contract for every input the implementation admits; + corpus results are required, non-exhaustive evidence of that. A claim must state that every row of the + named corpus version passed, and a failed row blocks the claim. +- **An erratum mechanism for a frozen corpus** at + [`conformance/evaluation/errata.md`](conformance/evaluation/errata.md). Only a project-issued, versioned + erratum can mark a row of a released corpus defective — a claimant cannot — and an erratum edits no row. + It is in place and empty for `suiteVersion` `0.2.0-draft`. +- **Input preflight (§8.2).** The inputs are admitted before §8 runs, in the order pack, facts, evidence + availability; an omitted evidence document is the implicit empty object; any violation of §8.2's shape, + including a non-object evidence input, is `malformed-input`; and because preflight completes before step + 1, an input error can never be outraced by a `not-applicable` result. +- Two optional case members in the evaluation carrier, defined now and used by no row so that later rows + need no carrier change: `workBudget` (a positive integer of work units, in the units a future accounting + model will define) and `expectedErrorPhase` (`preflight` or `evaluation`, alongside `expectedErrorClass` + only). +- A top-level `FAQ.md`; an informative, non-normative architecture-vision page + (`docs/architecture/vision.md`); a prior-art comparison page (`docs/concepts/comparison.md`); and + seeded draft RFCs 0001–0005 (pack manifest, Judgment Graph composition, evidence reference, + planner interface, and pack discovery) that record open design questions without adding anything + normative. +- Site navigation: a Concepts hub (`concepts/index.html`) gathering the conceptual and reference + pages, breadcrumbs on every page, and footer links to the Concepts and Project & docs hubs, so + every page is reachable through structured navigation rather than only through in-prose links. +- Static SVG architecture diagrams (shipped-versus-proposed, the three-property split, and evidence + sources feeding a pack) on the architecture-vision page, rendered without any JavaScript so the + strict `script-src 'none'` policy is preserved. +- FAQ: a "Skills, tools, and agent integration" section (with a "Skills and Tools" subsection) + that explains, honestly, how Judgment Packs relate to tools, agent skills, and `SKILL.md` — + including when a skill alone is sufficient — with a static SVG integration diagram. +- A `NOTICE` file naming Brian Jin as the copyright holder, included in the release bundle. The + repository previously identified no owner: `LICENSE` is the unmodified Apache-2.0 text, whose + appendix is a template rather than a filled-in field. `LICENSE` is deliberately left + byte-identical to the canonical text so automated license scanners continue to report a clean + Apache-2.0 match. +- A superseded-draft signpost at `/spec/0.1.0-draft/`, so a versioned URL that citers may already have + used resolves to the tagged source and the current draft instead of a 404. + +### Changed + +- Version identifiers move to `0.2.0-draft`: Core prose, the structural schema (`$id` and the exact + `specVersion` constant), the document-conformance manifest, every example, and every fixture. The + document format is unchanged, so a `0.1.0-draft` pack is unchanged in representation and + document-conformance meaning and needs only that one value edited; because the value is exact, an + unedited pack is not structurally conforming here. Re-declaration is not inert, though: it opts the pack + into this draft's evaluator semantics, which existed for no consumer under `0.1.0-draft`, and confers no + conformance on any implementation. The `0.1.0-draft` schema is preserved at + `schema/judgment-pack-core-0.1.0-draft.schema.json`, and that draft's document-conformance manifest + schema at `schema/conformance-manifest-0.1.0-draft.schema.json`, both served at the `$id` each was + published under so a previously cited identifier still resolves. Each preserved file matches the tagged + `v0.1.0-draft` artifact in every byte except its `$id` member, which was re-pointed to the project's + neutral domain when the retired pre-publication identifier was withdrawn; `releases/v0.2.0-draft.md` + publishes the digests of both forms. `conformance/structural/invalid-spec-version.json` now carries + `0.1.0-draft` as its negative value, which makes the re-declaration requirement a corpus case. +- §8.4's classes are split by phase so the error contract cannot contradict §10: a documented document or + carrier limit reached while admitting an input is `malformed-input` (§2.1 refuses the document, so + preflight never admits it), while a collection-size or evaluation-work limit reached during evaluation is + `resource-exhaustion`. Every error carries exactly one class — a Core class where one applies, otherwise + a documented implementation-defined class in §9's reverse-domain form. +- `tools/build_release.py` now requires `HEAD` to be the resolved release commit. Artifacts are validated + from the worktree and archived from the commit, so the gate previously accepted a validated tree and an + archived tree that were not the same tree. +- The evidence behind the change is two implementations agreeing 13/13. Both trace to one maintainer's + direction, so that agreement corroborates the semantics rather than independently confirming them, + and RFC 0000's bar for a *stable* feature is still unmet — carried as a caveat in the RFC's adoption + record, the release notes, and the corpus README rather than stated once and forgotten. +- Every assertion that evaluator conformance cannot be claimed is now the `0.2.0-draft` truth: the + class exists, claims are governed by §3.4.1, and no implementation ships a claim with this release. + The reference runtime's evaluator remains experimental and claims nothing; whether it claims the + class is a decision for that repository. The site says the same: the implementations listing no longer + says the runtime does not evaluate rules, and the example guides no longer call a conflict result + unportable now that §8.3 makes it a portable `unresolved` disposition. + +- Renamed the change-proposal process from Judgment Enhancement Proposal (JEP) to Request for + Comments (RFC); moved `jeps/` to `rfcs/`. The required sections and evidence bar are unchanged. + The historical `0.1.0-draft` entry below is left as shipped. +- Implementations page: replaced the earlier vendor-branded CLI listing with the vendor-neutral + `judgment-pack` reference runtime (which inherited that CLI's implementation), linking to its own + repository, and removed the vendor-branded on-site CLI page. Every page on the site is now + vendor-neutral, and no page names the retired pre-publication brand. + ## `0.1.0-draft` — 2026-07-22 Initial research preview. diff --git a/VERSIONING.md b/VERSIONING.md index 2f3490e..0ded8e7 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -54,6 +54,10 @@ tooling can read the renamed manifest status. The unsuffixed `v0.3.0` tag is the companion's `policy-v-draft.N` route remains independent and is not replaced by this Core convention. +Across the project, `vX.Y.Z-rc.N` is a candidate of the complete milestone that graduates to +`vX.Y.Z` with no code change, and `vX.Y.Z-alpha.N`, used by the implementations and never by this +repository's Core route, is a scoped cut for a named consumer that never graduates. + That first candidate is a distribution of the proposed exact `specVersion` `0.3.0` artifacts; `0.3.0-rc.1` is its distribution identity, not a pack version selector. Its closed mapping at `releases/v0.3.0-rc.1.json` is required by the builder. Both manifests remain maturity `draft`: diff --git a/docs/releases/v0.3.0.md b/docs/releases/v0.3.0.md index 6b827bc..b1b72fc 100644 --- a/docs/releases/v0.3.0.md +++ b/docs/releases/v0.3.0.md @@ -1,83 +1,6 @@ # v0.3.0 -`0.3.0`, tagged `v0.3.0`, is the first Core release whose `specVersion` has no `-draft` suffix. -The released `0.2.0-draft` identifier and its tag keep their names. Before the release, one -`v0.3.0-rc.1` GitHub pre-release gives adopters about a week to verify that their tooling reads the -manifest maturity `status` as `draft` rather than the retired `research-preview` value. - -## Included work - -- Core escalation routes from RFC 0025: an exclusive single target or bounded conditional routes, - with routed handoff provenance and complete retained reasons. -- The `base` and `extended` document ceiling profiles from RFC 0026, including reader negotiation - and explicit refusal of an unsupported profile. -- The non-normative rule-overlap check and its closed `JPS-RULES-OVERLAP` diagnostic. -- RFC 0020's bulk amendment: contributing-rule output records, disposition summaries, reproducible - seeded samples, drift comparison, and the closed workflow schema and hand-written fixture that - verify those records. -- RFC 0020's later-sibling rule, limited to instances admitted for acquisition so an admission - failure retains its own finding. -- Dialect data-quality conversions, reconciliation, typed research-result states, lineage, - versioned source profiles, resolved settings, and byte-retaining replay contracts. -- RFC 0022's alignment with the landed composition decisions for subgraph bounds, routed handoff - provenance, dialect-owned remapping, and cross-installation packaging. - -## Versioning and maturity - -Pre-`1.0.0` Core releases may be incompatible when their notes record the change and give a -`specVersion` migration note. Conformance manifest `status` is now the independent maturity ladder -`draft`, `candidate`, and `release`; `0.3.0` remains `draft`. The `release` maturity is reserved for -the `1.0.0` standards claim after the governance acceptance evidence is met and recorded. Production -use does not by itself establish that claim. - -The Policy companion remains on its independent `policy-v1.0.0-draft.N` route until that format's -first release and is not renamed by the Core scheme change. - -RFC 0024 and the Policy companion's record land in the release if merged before the tag. - -## Migration note - -Packs targeting this release declare exact `specVersion` `0.3.0`. Reader and writer compatibility -is not inferred from a shared `0.x` prefix: historical readers reject this selector, and `0.3.0` -profiles and routed escalations require corresponding reader support. Writers targeting -`0.2.0-draft` must obey its historical schema, which admits neither `profile` nor escalation `routes`. -Multiple-version readers dispatch on the complete selector; they do not apply current rules to old -selectors or rewrite old documents on read. - -Carrier admission is tighter: Unicode escapes in the UTF-16 surrogate range must form well-formed -surrogate pairs. -For example, a lone `\uD800` title escape accepted by the published `0.2.0-draft` carrier and -structural schema is malformed in `0.3.0`. A version-only edit cannot repair those bytes. The author -must provide the intended scalar or valid surrogate pair; automatic replacement would lose meaning. - -Selector-only migration is sufficient only for documents that already meet the current carrier, -structural and semantic requirements. Revalidate profile/ceiling support and escalation shapes; -otherwise review and edit the content deliberately. No general automatic migration or unchanged -conformance-verdict promise is made. Re-declaration also selects the exact new evaluator semantics -and corpus, not inherited evaluator conformance. The Runtime must vendor the exact resulting -artifacts beside historical releases and retain exact-selector dispatch; it must not overwrite the -existing `0.2.0-draft` artifacts. - -## Preserved historical schema identities - -Historical resources come from immutable source tags, not mutable pre-repin `main`: -`v0.1.0-draft` is commit `80958f50c851e9809cb8036a23622391cf437c99`, and `v0.2.0-draft` is -commit `0f30fd7b2c3fd7792249c8f3c5335b9e697a2c7a`. The two `0.1.0-draft` resources retain the -already documented neutral-domain `$id` substitution and no other byte change, as recorded in the -[0.2.0-draft preservation note](../../releases/v0.2.0-draft.md#what-preserved-means-for-the-010-draft-schemas). -The three `0.2.0-draft` resources are byte-identical to their original tagged forms, including -`research-preview` maturity and the original structural rules. Their historical identities remain -published; new maturity values and profile/routes rules apply only to the current resources. - -| Preserved file | Original tagged SHA-256 | Preserved SHA-256 | -| --- | --- | --- | -| `schema/judgment-pack-core-0.1.0-draft.schema.json` | `81f6986edc8d2bbf6f49c28a9bbec4f9054f7c72cab5a5028130e01d2e7274d8` | `c65c559db33d19d2f327858635266fca7fcc049fb75083a7a8d47517e27224fc` | -| `schema/conformance-manifest-0.1.0-draft.schema.json` | `fa3f55f53712253551d6374ff3f06621e936b56c04dabd399703bbb05285c740` | `f073bde8f6f2189edd0334c81c03fecfacf8fae2bd49305a788c75550dcbc2c5` | -| `schema/judgment-pack-core-0.2.0-draft.schema.json` | `847910902fa202d9fb3a97062071b532f11649031c9dad32490bf191dca5b3b5` | `847910902fa202d9fb3a97062071b532f11649031c9dad32490bf191dca5b3b5` | -| `schema/conformance-manifest-0.2.0-draft.schema.json` | `e531a1a89f188dc12ff682263fdd6a0022bcfb6977dfeb642dd05fcbec5e0723` | `e531a1a89f188dc12ff682263fdd6a0022bcfb6977dfeb642dd05fcbec5e0723` | -| `schema/conformance-evaluation-manifest-0.2.0-draft.schema.json` | `142c30a33c7272cff5e85522b95d36316d2e195565bc5bef6c04a999e0da51f6` | `142c30a33c7272cff5e85522b95d36316d2e195565bc5bef6c04a999e0da51f6` | - -The evaluation manifest schema remains at -`https://judgmentpack.org/schema/0.2.0-draft/conformance/evaluation/manifest.schema.json`; -the historical Core and document-manifest `$id` paths also remain served. The Core bundles carry -all five preserved resources. Separately pinned Policy consumer bytes and format digests are unchanged. +The `0.3.0` release notes are now the immutable [`releases/v0.3.0.md`](../../releases/v0.3.0.md): +the two release candidates, the included work, the selected retained-column leaf, versioning and +maturity, the migration note, the preserved historical schema identities, what the release pins, +and the checks run for it. diff --git a/releases/v0.3.0.md b/releases/v0.3.0.md new file mode 100644 index 0000000..5e658c1 --- /dev/null +++ b/releases/v0.3.0.md @@ -0,0 +1,138 @@ +# v0.3.0 + +`0.3.0`, tagged `v0.3.0`, is the first Core release whose `specVersion` has no `-draft` suffix. +The released `0.2.0-draft` identifier and its tag keep their names. Two GitHub pre-releases came +before it, and both remain immutable with their mappings and notes: + +- [`v0.3.0-rc.1`](https://github.com/Judgment-Pack/judgment-pack-spec/releases/tag/v0.3.0-rc.1), + published 2026-10-25, carried the renamed manifest maturity (`status` `draft` in place of the + retired `research-preview`) and the re-pin of the Core schemas, manifests, packs, vectors, + readers and site routes to exact `specVersion` `0.3.0` (#231). +- [`v0.3.0-rc.2`](https://github.com/Judgment-Pack/judgment-pack-spec/releases/tag/v0.3.0-rc.2), + published 2026-10-27, added exactly the selected `retained-column/1` leaf, accepted at draft + maturity through [RFC 0029](../rfcs/0029-retained-column-conversions.md) (#274). + +The leaf is part of this release. Packs declare `specVersion` `0.3.0`; neither candidate's +distribution identity is a pack version selector. + +## Included work + +- Core escalation routes from RFC 0025: an exclusive single target or bounded conditional routes, + with routed handoff provenance and complete retained reasons. +- The `base` and `extended` document ceiling profiles from RFC 0026, including reader negotiation + and explicit refusal of an unsupported profile. +- The non-normative rule-overlap check and its closed `JPS-RULES-OVERLAP` diagnostic. +- RFC 0020's bulk amendment: contributing-rule output records, disposition summaries, reproducible + seeded samples, drift comparison, and the closed workflow schema and hand-written fixture that + verify those records. +- RFC 0020's later-sibling rule, limited to instances admitted for acquisition so an admission + failure retains its own finding. +- Dialect data-quality conversions, reconciliation, typed research-result states, lineage, + versioned source profiles, resolved settings, and byte-retaining replay contracts. +- RFC 0022's alignment with the landed composition decisions for subgraph bounds, routed handoff + provenance, dialect-owned remapping, and cross-installation packaging. +- The selected `retained-column/1` leaf from RFC 0029: its companion + [selected retained columns](../spec/retained-column.md), its versioned schema and corpus, and the + two offline readers, at draft maturity. + +## The selected retained-column leaf + +The release bundles carry the leaf's schema, corpus, companion prose and both offline readers with +the bytes `v0.3.0-rc.2` published. The final inventory `releases/v0.3.0.retained-column.json` +names distribution `v0.3.0` and records the same schema, corpus and file digests as the +candidate's inventory; the release builder refuses a final whose inventory differs from the +candidate's in anything but that name. + +The leaf's support tuple is the one its companion states: contract `retained-column/1`, +distribution version `0.3.0-rc.2`, the candidate's source commit, and the schema and corpus +digests below. It is independent of the pack `specVersion`, and declaring `0.3.0` does not select +the leaf. The final bundle has no generated `release-identity.json`, so its readers answer a +retained-column selection with `capability-unsupported`; a selected-leaf support claim is made +against an installation of the `v0.3.0-rc.2` distribution. The leaf's support claim is limited to +its named scope: it is no general Core, stable interoperability or general preparation-verifier +conformance claim. + +## Versioning and maturity + +Pre-`1.0.0` Core releases may be incompatible when their notes record the change and give a +`specVersion` migration note. Conformance manifest `status` is now the independent maturity ladder +`draft`, `candidate`, and `release`; `0.3.0` remains `draft`. The `release` maturity is reserved for +the `1.0.0` standards claim after the governance acceptance evidence is met and recorded. Production +use does not by itself establish that claim. + +The Policy companion remains on its independent `policy-v1.0.0-draft.N` route until that format's +first release and is not renamed by the Core scheme change. This release leaves it unchanged: the +Core bundles exclude its schemas and corpus, and RFC 0023 stays Accepted at draft maturity with +format `1.0.0` pinned (#211). + +RFC 0024 is not part of this release; it remains a Draft. + +## Migration note + +Packs targeting this release declare exact `specVersion` `0.3.0`. Reader and writer compatibility +is not inferred from a shared `0.x` prefix: historical readers reject this selector, and `0.3.0` +profiles and routed escalations require corresponding reader support. Writers targeting +`0.2.0-draft` must obey its historical schema, which admits neither `profile` nor escalation `routes`. +Multiple-version readers dispatch on the complete selector; they do not apply current rules to old +selectors or rewrite old documents on read. + +Carrier admission is tighter: Unicode escapes in the UTF-16 surrogate range must form well-formed +surrogate pairs. +For example, a lone `\uD800` title escape accepted by the published `0.2.0-draft` carrier and +structural schema is malformed in `0.3.0`. A version-only edit cannot repair those bytes. The author +must provide the intended scalar or valid surrogate pair; automatic replacement would lose meaning. + +Selector-only migration is sufficient only for documents that already meet the current carrier, +structural and semantic requirements. Revalidate profile/ceiling support and escalation shapes; +otherwise review and edit the content deliberately. No general automatic migration or unchanged +conformance-verdict promise is made. Re-declaration also selects the exact new evaluator semantics +and corpus, not inherited evaluator conformance. The Runtime must vendor the exact resulting +artifacts beside historical releases and retain exact-selector dispatch; it must not overwrite the +existing `0.2.0-draft` artifacts. + +## Preserved historical schema identities + +Historical resources come from immutable source tags, not mutable pre-repin `main`: +`v0.1.0-draft` is commit `80958f50c851e9809cb8036a23622391cf437c99`, and `v0.2.0-draft` is +commit `0f30fd7b2c3fd7792249c8f3c5335b9e697a2c7a`. The two `0.1.0-draft` resources retain the +already documented neutral-domain `$id` substitution and no other byte change, as recorded in the +[0.2.0-draft preservation note](v0.2.0-draft.md#what-preserved-means-for-the-010-draft-schemas). +The three `0.2.0-draft` resources are byte-identical to their original tagged forms, including +`research-preview` maturity and the original structural rules. Their historical identities remain +published; new maturity values and profile/routes rules apply only to the current resources. + +| Preserved file | Original tagged SHA-256 | Preserved SHA-256 | +| --- | --- | --- | +| `schema/judgment-pack-core-0.1.0-draft.schema.json` | `81f6986edc8d2bbf6f49c28a9bbec4f9054f7c72cab5a5028130e01d2e7274d8` | `c65c559db33d19d2f327858635266fca7fcc049fb75083a7a8d47517e27224fc` | +| `schema/conformance-manifest-0.1.0-draft.schema.json` | `fa3f55f53712253551d6374ff3f06621e936b56c04dabd399703bbb05285c740` | `f073bde8f6f2189edd0334c81c03fecfacf8fae2bd49305a788c75550dcbc2c5` | +| `schema/judgment-pack-core-0.2.0-draft.schema.json` | `847910902fa202d9fb3a97062071b532f11649031c9dad32490bf191dca5b3b5` | `847910902fa202d9fb3a97062071b532f11649031c9dad32490bf191dca5b3b5` | +| `schema/conformance-manifest-0.2.0-draft.schema.json` | `e531a1a89f188dc12ff682263fdd6a0022bcfb6977dfeb642dd05fcbec5e0723` | `e531a1a89f188dc12ff682263fdd6a0022bcfb6977dfeb642dd05fcbec5e0723` | +| `schema/conformance-evaluation-manifest-0.2.0-draft.schema.json` | `142c30a33c7272cff5e85522b95d36316d2e195565bc5bef6c04a999e0da51f6` | `142c30a33c7272cff5e85522b95d36316d2e195565bc5bef6c04a999e0da51f6` | + +The evaluation manifest schema remains at +`https://judgmentpack.org/schema/0.2.0-draft/conformance/evaluation/manifest.schema.json`; +the historical Core and document-manifest `$id` paths also remain served. The Core bundles carry +all five preserved resources. Separately pinned Policy consumer bytes and format digests are unchanged. + +## What this release pins + +- Format: exact `specVersion` `0.3.0`, the Core schema's `specVersion` constant and the selector + both corpus manifests name. +- Frozen corpora: the document corpus (`conformance/manifest.json`) and the evaluation corpus + (`conformance/evaluation/manifest.json`), both `suiteVersion` `0.3.0` at maturity `draft`. A final + exact-version conformance claim names these corpora and requires every applicable row to pass. +- Core schema `$id`: `https://judgmentpack.org/schema/0.3.0/judgment-pack-core.schema.json`. +- Retained-column schema: `schema/retained-column/1/contract.schema.json`, `$id` + `https://judgmentpack.org/schema/retained-column/1/contract.schema.json`, SHA-256 + `sha256:0cb49f238c78391cb163a65474cc9af962df2bb1024a55c5310c2b784b2f0557`. +- Retained-column corpus: `conformance/retained-column/1/cases.json`, SHA-256 + `sha256:17b16ff7a9b74fe4cc81eb6f6129a6aabdd9834621d9e756aa2d4c1a3f245863`. + +## Checks run for this release + +Repository conformance run `` passed the whole check list on `main` at +`
`, the commit this tag names. The release workflow reuses that exact-commit evidence +and runs the milestone, tag, artifact-pin and bundle checks again at the tag. + +Results against this release name its tag and commit. Conformance never proves truth, +authorization, safety or operational fitness. diff --git a/releases/v0.3.0.retained-column.json b/releases/v0.3.0.retained-column.json new file mode 100644 index 0000000..49e3577 --- /dev/null +++ b/releases/v0.3.0.retained-column.json @@ -0,0 +1,39 @@ +{ + "contract": "retained-column/1", + "distribution": "v0.3.0", + "maturity": "draft", + "schema": "schema/retained-column/1/contract.schema.json", + "schemaSha256": "sha256:0cb49f238c78391cb163a65474cc9af962df2bb1024a55c5310c2b784b2f0557", + "corpus": "conformance/retained-column/1/cases.json", + "corpusSha256": "sha256:17b16ff7a9b74fe4cc81eb6f6129a6aabdd9834621d9e756aa2d4c1a3f245863", + "files": { + "conformance/retained-column/1/README.md": "sha256:dc4ee78be4339ccf56f8bda34d8ae175fb07605cfb77d003e55c781ebd16308c", + "conformance/retained-column/1/capabilities.json": "sha256:fd8b969e5372cdf630fe12ec4034a2bf929d47ea56664abb0b3f510ad761f644", + "conformance/retained-column/1/cases.json": "sha256:17b16ff7a9b74fe4cc81eb6f6129a6aabdd9834621d9e756aa2d4c1a3f245863", + "conformance/retained-column/1/examples.json": "sha256:113354682037e3b576be23d812ab86d280718a50ca1bf1a631e85ba91eea5cca", + "conformance/retained-column/1/manifest.json": "sha256:b78cdac121b4b936614c9e652af3c78bde8b84184fd0a04f84effdd2c3eaf2c8", + "conformance/retained-column/1/preparations.json": "sha256:ff9450279cc65db4422f3644975b5b1128396e2e9a1f17effd4dcc732bb5317a", + "schema/retained-column/1/contract.schema.json": "sha256:0cb49f238c78391cb163a65474cc9af962df2bb1024a55c5310c2b784b2f0557", + "spec/dialect.md": "sha256:a10d6fc0f50b1cf450b2bd9156175529bed2a7d926d24567b47677076066cb5a", + "spec/judgment-pack-core.md": "sha256:0ceb66c15ce6b8268e02f319720eb40766c616423cfa08caaeea56ae8ba3d2ef", + "spec/retained-column.md": "sha256:f17747d5daeba6953cef335900569c5b1cf1052b7f982c0146b6322687375b7d", + "tools/_retained_column.py": "sha256:65c91f1363769ecfdfbebb5b42ade4258aaaf7b61620b23ee68b4000fee96233", + "tools/check_retained_column.py": "sha256:946709a979f9dd9fc1aa10ab4500d77128c053ad3e46f653537f83c29becad18", + "tools/dialect-verify-ts/src/bounds.ts": "sha256:c90d6c5db69206d4361409e502bb54a5e81505e4ac5842b987b44e60fbdb53c1", + "tools/dialect-verify-ts/src/ed25519.ts": "sha256:4f5c9c8eb015ba7a690e8998fdcb5440b7282bbd6deb04c53ddd7bbd2dd6174a", + "tools/dialect-verify-ts/src/endpoint.ts": "sha256:50e0d5d5b3d6c0c12a54e9b07e5daf2a499c84499e6890bfe85127ca6d6c7616", + "tools/dialect-verify-ts/src/forms.ts": "sha256:bc4f81670ea5c8710072ee3ec7f5fe8951f2155c430da3e00755b39777054326", + "tools/dialect-verify-ts/src/json.ts": "sha256:724e1408cd54032dded8f5677665a7a31a25b9111ad3695f401ce64207d61a6e", + "tools/dialect-verify-ts/src/main.ts": "sha256:ef1e5f28c2608d51f719adc74bad03266ce5f2fb5526673e878229dc260b99be", + "tools/dialect-verify-ts/src/prepare.ts": "sha256:e185b78a8f4d51cac63557f6973ef8b4af99660c3cef15b1509156acc8bbf027", + "tools/dialect-verify-ts/src/retained-engine.mjs": "sha256:4ab0e7bcae864552a987a53fcec97f834e280c4dbf8b3ae37b6ae950739a9825", + "tools/dialect-verify-ts/src/retained.ts": "sha256:5ae44bc9782168cdae87467a0eb9f4229382f46058ac64ef6369b1236fbe905d", + "tools/dialect-verify-ts/src/schema.ts": "sha256:938ad0ffefc378a7b1cc3bc7777063f4ef2929fbf8b90132ca5eb76537981dd3", + "tools/dialect-verify-ts/src/time.ts": "sha256:54afe326be3d163d325ea64ccaf907e83530a38250824d1877cc1b4726c92ca7", + "tools/dialect-verify-ts/src/vector.ts": "sha256:289b68479edf607df1480e46b0e6a6ee04091feb5dd56ddee696fbb2a337a1cb", + "tools/dialect-verify-ts/src/verify.ts": "sha256:5ae002b07b0f9d3755c9d00bc828420e27b9927948402d8e4b266540c4f5cc1b", + "tools/dialect_verify.py": "sha256:fe3b773ce30ad8c395fe85500714a12d6cc4262ccc6bca05c44f7b3b40a60b60", + "tools/retained_column.py": "sha256:0598c3b31a0a4115eb4efdf2e8fccc6b75c558dfe7d09180b3acf2241e937447" + }, + "claim": "selected-leaf-support" +} diff --git a/tests/test_historical_release.py b/tests/test_historical_release.py index b421707..ed84ca6 100644 --- a/tests/test_historical_release.py +++ b/tests/test_historical_release.py @@ -226,7 +226,7 @@ def test_selector_only_migration_counterexample_uses_actual_old_and_current_carr def test_migration_guidance_records_the_demonstrated_exceptions(self): core = (ROOT / "spec/judgment-pack-core.md").read_text() - notes = (ROOT / "docs/releases/v0.3.0.md").read_text() + notes = (ROOT / "releases/v0.3.0.md").read_text() self.assertNotIn("changes no part of the document format", core) self.assertNotIn("every conformance verdict of §§3.1–3.3 is the same", core) for text in (core, notes): diff --git a/tests/test_release.py b/tests/test_release.py index e58a38b..c0df1e8 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -4,6 +4,7 @@ import io import json import hashlib +import re import shutil import sys import zipfile @@ -27,11 +28,39 @@ class ReleaseBuilderTests(unittest.TestCase): def test_0_3_0_release_naming_is_recorded(self) -> None: versioning = (ROOT / "VERSIONING.md").read_text(encoding="utf-8") - notes = ROOT / "docs" / "releases" / "v0.3.0.md" + notes = ROOT / "releases" / "v0.3.0.md" self.assertIn("`v0.3.0-rc.1`", versioning) self.assertIn("`v0.3.0`", versioning) + # The two prerelease forms, and which of them this repository's Core route uses. + self.assertIn("`vX.Y.Z-rc.N` is a candidate of the complete milestone", versioning) + self.assertIn("`vX.Y.Z-alpha.N`, used by the implementations and never by this", versioning) self.assertTrue(notes.is_file()) self.assertEqual("# v0.3.0", notes.read_text(encoding="utf-8").splitlines()[0]) + # The prepared notes stay as a pointer: the published RC1 notes link to them. + pointer = (ROOT / "docs" / "releases" / "v0.3.0.md").read_text(encoding="utf-8") + self.assertEqual("# v0.3.0", pointer.splitlines()[0]) + self.assertIn("(../../releases/v0.3.0.md)", pointer) + self.assertIn("(../docs/releases/v0.3.0.md)", + (ROOT / "releases" / "v0.3.0-rc.1.md").read_text(encoding="utf-8")) + + def test_final_notes_name_both_candidates_the_leaf_and_the_pins(self) -> None: + notes = (ROOT / "releases" / "v0.3.0.md").read_text(encoding="utf-8") + inventory = json.loads( + (ROOT / "releases" / "v0.3.0.retained-column.json").read_text(encoding="utf-8") + ) + for text in ("`v0.3.0-rc.1`", "published 2026-10-25", "`v0.3.0-rc.2`", + "published 2026-10-27", "The leaf is part of this release.", + "RFC 0024 is not part of this release; it remains a Draft.", + "## What this release pins", "## Checks run for this release", + "https://judgmentpack.org/schema/0.3.0/judgment-pack-core.schema.json", + inventory["schemaSha256"], inventory["corpusSha256"], + "## Migration note", "## Included work", "## Versioning and maturity"): + with self.subTest(text=text): + self.assertIn(text, notes) + self.assertNotIn("if merged before the tag", notes) + # Only these two are completed at the tag, and completing them keeps this test passing. + self.assertLessEqual(set(re.findall(r"<[a-z][a-z ]+>", notes)), + {"", "
"}) def test_release_version_accepts_exact_semver_tags(self) -> None: self.assertEqual(BUILD_RELEASE.release_version("v0.1.0-draft"), "0.1.0-draft") @@ -63,12 +92,17 @@ def test_artifact_validation_accepts_the_current_version(self) -> None: self.assertEqual("0.3.0", version) BUILD_RELEASE.validate_artifacts(version, f"v{version}") - def test_core_bundle_uses_prepared_notes_and_keeps_historical_notes(self) -> None: + def test_core_bundle_uses_immutable_final_notes_and_keeps_historical_notes(self) -> None: current = BUILD_RELEASE.bundle_paths("v0.3.0", "HEAD") historical = BUILD_RELEASE.bundle_paths("v0.2.0-draft", "HEAD") - self.assertIn("docs/releases/v0.3.0.md", current) - self.assertNotIn("releases/v0.3.0.md", current) + self.assertEqual(ROOT / "releases" / "v0.3.0.md", BUILD_RELEASE.core_release_notes_path("v0.3.0")) + self.assertIn("releases/v0.3.0.md", current) + self.assertNotIn("docs/releases/v0.3.0.md", current) + # A final has no distribution mapping; only the two RC mappings are allowlisted. + self.assertNotIn("releases/v0.3.0.json", current) + self.assertFalse((ROOT / "releases" / "v0.3.0.json").exists()) self.assertIn("releases/v0.2.0-draft.md", historical) + self.assertNotIn("releases/v0.3.0.retained-column.json", historical) def test_artifact_validation_pins_the_evaluation_corpus(self) -> None: # An evaluator-conformance claim binds to the corpus published for the exact specVersion @@ -413,6 +447,18 @@ def _release_tree(directory: Path, version: str) -> Path: encoding="utf-8", ) (directory / "releases" / f"v{version}.md").write_text("notes\n", encoding="utf-8") + if version == "0.3.0": + # The v0.3.0 final also carries RC2's selected leaf: its inventories, the leaf bytes they + # pin, the milestone link and RFC 0029's acceptance are copied, not invented. + inventory = json.loads( + (ROOT / "releases" / "v0.3.0.retained-column.json").read_text(encoding="utf-8") + ) + for name in ("releases/v0.3.0.retained-column.json", + "releases/v0.3.0-rc.2.retained-column.json", + "releases/v0.3.0.milestone.json", + "rfcs/0029-retained-column-conversions.md", *inventory["files"]): + (directory / name).parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(ROOT / name, directory / name) return directory diff --git a/tests/test_release_evidence.py b/tests/test_release_evidence.py index 017a63a..baf386c 100644 --- a/tests/test_release_evidence.py +++ b/tests/test_release_evidence.py @@ -15,6 +15,7 @@ import os import re import shlex +import shutil import subprocess import sys import tempfile @@ -27,7 +28,8 @@ # lib/release_evidence.py. A re-vendor changes both, and line 2 of the copy. KIT_RELEASE = "v0.11.0" KIT_LIBRARY_SHA256 = "f272b7a43272bac31bb9b0c37a583d9c40d376972b04c812e576bd82c1ef18e3" -# The release job's steps, in order. Only the repository suite depends on the evidence job. +# The release job's steps, in order. Only the repository suite depends on the evidence decision; +# the draft step reads the route, the tag grammar alone. RELEASE_STEPS = [ "Check out tagged source", "Validate release milestone and tagged checkout", @@ -39,10 +41,11 @@ "Check repository artifacts and static site", "Check exact Policy format and agreement corpus", "Build immutable release bundles", - "Create draft prerelease", + "Create draft release", "Attest release bundles", ] SUITE_STEP = "Check repository artifacts and static site" +DRAFT_STEP = "Create draft release" EVIDENCE_STEPS = [ "Choose reuse or the whole suite at the tag", "Check out the release commit", @@ -292,6 +295,7 @@ def test_reuse_skips_only_the_repository_suite_and_a_refusal_stops_the_release(s self.assertIsNone(field(job, "if", 4)) self.assertNotIn("continue-on-error", self.release) self.assertEqual(field(evidence, "reuse", 6), "${{ steps.reused.outputs.reuse }}") + self.assertEqual(field(evidence, "route", 6), "${{ steps.route.outputs.route }}") self.assertEqual([field(step, "- name", 0) for step in steps_of(evidence)], EVIDENCE_STEPS) permissions = evidence.split(" permissions:\n", 1)[1].split(" outputs:\n", 1)[0] self.assertEqual(re.findall(r"^ ([a-z-]+): (\w+)$", permissions, re.MULTILINE), @@ -300,8 +304,13 @@ def test_reuse_skips_only_the_repository_suite_and_a_refusal_stops_the_release(s # the gate step succeeded and its record says reuse: a missing or other value runs it. self.assertEqual(field(step_named(release, SUITE_STEP), "if", 8), "needs.evidence.outputs.reuse != 'true'") + self.assertEqual([name for name in RELEASE_STEPS + if "needs.evidence.outputs.reuse" in step_named(release, name)], [SUITE_STEP]) + # The draft step reads only the route, which is the tag grammar and never the gate's decision. self.assertEqual([name for name in RELEASE_STEPS if "needs.evidence" in step_named(release, name)], - [SUITE_STEP]) + [SUITE_STEP, DRAFT_STEP]) + self.assertEqual(re.findall(r"needs\.evidence\.outputs\.\w+", step_named(release, DRAFT_STEP)), + ["needs.evidence.outputs.route"]) self.assertIn("python -m unittest discover -s tests -v", step_named(release, SUITE_STEP)) verify = step_named(evidence, "Reuse exact-commit main CI evidence, or refuse") self.assertEqual(field(verify, "if", 8), "steps.route.outputs.route == 'evidence'") @@ -321,11 +330,46 @@ def test_release_steps_keep_their_order_and_release_semantics(self): self.assertEqual([field(step, "- name", 0) for step in steps_of(release)], RELEASE_STEPS) self.assertEqual(field(step_named(release, "Check exact Policy format and agreement corpus"), "if", 8), "startsWith(github.ref_name, 'policy-v')") - draft = step_named(release, "Create draft prerelease") - for flag in ("--draft", "--prerelease", "--verify-tag", '--notes-file "releases/${GITHUB_REF_NAME}.md"'): + draft = step_named(release, DRAFT_STEP) + for flag in ("--draft", "--verify-tag", '--notes-file "releases/${GITHUB_REF_NAME}.md"'): self.assertIn(flag, draft) + self.assertEqual(field(draft, "RELEASE_ROUTE", 10), "${{ needs.evidence.outputs.route }}") self.assertIn("subject-checksums: dist/checksums.txt", step_named(release, "Attest release bundles")) + def test_only_a_plain_release_tag_is_drafted_without_the_prerelease_flag(self): + # The route step's own output decides: a plain vMAJOR.MINOR.PATCH tag is drafted as a release + # the maintainer publishes; every other tag, and any route value but evidence, as a pre-release. + route = script_of(step_named(self.jobs["evidence"], "Choose reuse or the whole suite at the tag")) + draft = script_of(step_named(self.jobs["release"], DRAFT_STEP)) + with tempfile.TemporaryDirectory() as directory: + stub = Path(directory) / "bin" + stub.mkdir() + # A stand-in for gh that records its arguments; PATH holds it and bash, nothing else. + (stub / "gh").write_text('#!/bin/bash\nprintf "%s\\n" "$@" > "${GH_ARGUMENTS}"\n') + (stub / "gh").chmod(0o755) + (stub / "bash").symlink_to(shutil.which("bash")) + arguments = Path(directory) / "arguments" + + def drafted(route_value): + arguments.unlink(missing_ok=True) + run_step(draft, {"PATH": str(stub), "RELEASE_ROUTE": route_value, "GH_ARGUMENTS": str(arguments), + "GITHUB_REF_NAME": "tag-fixture"}, directory) + return arguments.read_text().splitlines() + + tags = ("v0.3.0", "v1.10.0", "v0.3.0-rc.1", "v0.3.0-rc.2", "v1.2.3-alpha.1", "v0.2.0-draft", + "policy-v1.0.0-draft.1", "v01.0.0", "v0.3.0 ") + for tag in tags: + with self.subTest(tag=tag): + written = run_step(route, {"LC_ALL": "C", "RELEASE_TAG": tag, "GITHUB_SHA": "a" * 40}, directory) + called = drafted(written["route"]) + self.assertEqual(called[:2], ["release", "create"]) + self.assertIn("--draft", called) + self.assertIn("--verify-tag", called) + self.assertEqual("--prerelease" in called, self.gate.TAG.fullmatch(tag) is None) + for value in ("", "fresh", "Evidence", "evidence "): + with self.subTest(route=value): + self.assertIn("--prerelease", drafted(value)) + def test_the_evidence_job_sets_up_its_runner_as_main_ci_does(self): evidence = self.jobs["evidence"] [ci] = self.conformance.values() diff --git a/tests/test_retained_release.py b/tests/test_retained_release.py index 7d6fcc0..fc33303 100644 --- a/tests/test_retained_release.py +++ b/tests/test_retained_release.py @@ -1,4 +1,4 @@ -"""Exact RC2 inventory, preserved RC1 identity, and new schema publication.""" +"""Exact RC2 inventory, preserved RC1 identity, the final's leaf, and new schema publication.""" from pathlib import Path from urllib.parse import urlsplit from unittest.mock import patch @@ -7,13 +7,28 @@ import hashlib import json import shutil +import subprocess +import sys +import tarfile import tempfile import unittest +import zipfile from tools import build_release ROOT=Path(__file__).resolve().parents[1] INVENTORY='releases/v0.3.0-rc.2.retained-column.json' +FINAL_INVENTORY='releases/v0.3.0.retained-column.json' +FINAL_FIXTURE_FILES=('releases/v0.3.0.md','releases/v0.3.0.milestone.json','rfcs/0029-retained-column-conversions.md', + 'schema/judgment-pack-core.schema.json','conformance/manifest.json','conformance/evaluation/manifest.json') + + +def final_fixture(root): + """Copy the final's leaf set, both inventories and its Core pins; nothing is generated.""" + inventory=json.loads((ROOT/FINAL_INVENTORY).read_text()) + for name in {INVENTORY,FINAL_INVENTORY,*inventory['files'],*FINAL_FIXTURE_FILES}: + target=root/name;target.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(ROOT/name,target) + return inventory class RetainedRelease(unittest.TestCase): @@ -100,6 +115,105 @@ def git(*args): attributes=(ROOT/'.gitattributes').read_text() self.assertIn('conformance/dialect/** export-ignore -text',attributes) + def test_final_inventory_is_rc2s_but_for_its_distribution(self): + build_release.validate_final_retained_inventory() + final=json.loads((ROOT/FINAL_INVENTORY).read_text());candidate=json.loads((ROOT/INVENTORY).read_text()) + self.assertEqual(('v0.3.0','v0.3.0-rc.2'),(final['distribution'],candidate['distribution'])) + self.assertEqual({**candidate,'distribution':'v0.3.0'},final) + self.assertEqual({'v0.3.0-rc.2':INVENTORY,'v0.3.0':FINAL_INVENTORY},build_release.RETAINED_INVENTORIES) + self.assertEqual({'number':2,'title':'v0.3.0'},json.loads((ROOT/'releases/v0.3.0.milestone.json').read_text())) + self.assertNotIn('v0.3.0',build_release.CORE_RC_IDENTITIES) + build_release.validate_artifacts('0.3.0','v0.3.0') + + def test_final_route_refuses_each_leaf_rule(self): + # Disposable fixture of the final's tree; each refusal is restored before the next one. + with tempfile.TemporaryDirectory(prefix='final-leaf-fixture-') as temporary: + root=Path(temporary);inventory=final_fixture(root) + build_release.validate_artifacts('0.3.0','v0.3.0',root) + def refuses(message): + with self.assertRaisesRegex(ValueError,message): + build_release.validate_artifacts('0.3.0','v0.3.0',root) + def json_change(name,change): + path=root/name;original=path.read_bytes();value=json.loads(original);change(value);path.write_text(json.dumps(value)) + return lambda:path.write_bytes(original) + # Changed leaf bytes with a final inventory regenerated to match them: not RC2's bytes. + corpus=root/inventory['corpus'];original_corpus=corpus.read_bytes();corpus.write_bytes(original_corpus+b' ') + pin='sha256:'+hashlib.sha256(corpus.read_bytes()).hexdigest() + def repin(value): + value['files'][inventory['corpus']]=pin;value['corpusSha256']=pin + restore=json_change(FINAL_INVENTORY,repin) + build_release.validate_retained_inventory(root,'v0.3.0') + refuses('differs from the published RC2 inventory') + restore();refuses('immutable artifact bytes') + corpus.write_bytes(original_corpus) + for name,change,message in [ + (INVENTORY,lambda v:v.update(claim='other'),'differs from the published RC2 inventory'), + (INVENTORY,lambda v:v.update(extra=True),'differs from the published RC2 inventory'), + (FINAL_INVENTORY,lambda v:v.update(distribution='v0.3.0-rc.2'),'identity or scope differs'), + (FINAL_INVENTORY,lambda v:v.update(maturity='candidate'),'identity or scope differs'), + ('releases/v0.3.0.milestone.json',lambda v:v.update(number=3),'final Core milestone')]: + with self.subTest(name=name,message=message): + restore=json_change(name,change);refuses(message);restore() + for name,message in [(INVENTORY,'published RC2 retained-column inventory is missing'), + (FINAL_INVENTORY,'inventory is missing for the exact tag'), + ('releases/v0.3.0.milestone.json','final Core milestone')]: + with self.subTest(missing=name): + path=root/name;original=path.read_bytes();path.unlink();refuses(message);path.write_bytes(original) + rfc=root/'rfcs/0029-retained-column-conversions.md';accepted=rfc.read_text() + rfc.write_text(accepted.replace('- Status: Accepted','- Status: Review'));refuses('RFC0029 acceptance') + rfc.write_text(accepted) + # A final has no separate distribution identity: no mapping is admitted beside it. + (root/'releases/v0.3.0.json').write_text(json.dumps({**build_release.CORE_RC2_IDENTITY,'tag':'v0.3.0'})) + refuses('only the allowlisted Core RCs have a release mapping') + (root/'releases/v0.3.0.json').unlink() + build_release.validate_artifacts('0.3.0','v0.3.0',root) + + def test_final_bundle_paths_carry_the_leaf_without_a_mapping(self): + def git(*args): + if args[0]=='ls-tree': + directory=args[-1].split(':',1)[1] + return '\n'.join(p.name for p in (ROOT/directory).iterdir()) + raise AssertionError(args) + with patch.object(build_release,'git',side_effect=git): + paths=build_release.bundle_paths('v0.3.0','synthetic-tree') + for path in (*build_release.RETAINED_BUNDLE_PATHS,FINAL_INVENTORY,'releases/v0.3.0.milestone.json', + 'releases/v0.3.0.md','conformance/retained-column','schema/retained-column','spec'): + self.assertIn(path,paths) + for path in (INVENTORY,'releases/v0.3.0.json','docs/releases/v0.3.0.md','releases/v0.3.0-rc.2.milestone.json', + 'conformance/policy','schema/policy'): + self.assertNotIn(path,paths) + + def test_final_cli_archives_the_leaf_with_no_rc_identity(self): + # The actual builder CLI on a disposable fixture repository and a real annotated tag. + with tempfile.TemporaryDirectory(prefix='final-archive-fixture-') as temporary: + root=Path(temporary);final_fixture(root) + for name in ('tools/build_release.py','tools/dialect_verify.py','requirements-dev.txt','.gitattributes'): + (root/name).parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(ROOT/name,root/name) + for name in ('README.md','LICENSE','NOTICE','CHANGELOG.md','VERSIONING.md','TESTING.md','SECURITY.md'): + (root/name).write_text('Hand-authored archive fixture, not release artifacts.\n') + (root/'examples').mkdir();(root/'examples/control.json').write_text('{"fixture":true}\n') + def git(*args): + return subprocess.run(['git','-c','user.name=Fixture','-c','user.email=fixture@example.invalid', + '-c','commit.gpgsign=false','-c','tag.gpgsign=false',*args], + cwd=root,check=True,capture_output=True,text=True).stdout.strip() + git('init','--quiet');git('add','.');git('commit','--quiet','-s','-m','Final archive fixture') + commit=git('rev-parse','HEAD');git('tag','-a','v0.3.0','-m','Fixture only') + built=subprocess.run([sys.executable,'tools/build_release.py','--tag','v0.3.0','--commit',commit,'--output','dist'], + cwd=root,capture_output=True,text=True) + self.assertEqual(0,built.returncode,built.stderr) + prefix='judgment-pack-spec-0.3.0/' + with tarfile.open(root/'dist/judgment-pack-spec_0.3.0_artifacts.tar.gz') as archive: + names={m.name[len(prefix):] for m in archive.getmembers() if m.isfile()} + self.assertEqual((root/FINAL_INVENTORY).read_bytes(),archive.extractfile(prefix+FINAL_INVENTORY).read()) + with zipfile.ZipFile(root/'dist/judgment-pack-spec_0.3.0_artifacts.zip') as archive: + self.assertEqual(names,{n[len(prefix):] for n in archive.namelist() if not n.endswith('/')}) + for name in (FINAL_INVENTORY,'releases/v0.3.0.milestone.json','releases/v0.3.0.md','tools/retained_column.py', + 'tools/dialect-verify-ts/src/retained.ts','schema/retained-column/1/contract.schema.json', + 'conformance/retained-column/1/cases.json'): + self.assertIn(name,names) + for name in ('release-identity.json','releases/v0.3.0.json',INVENTORY,'rfcs/0029-retained-column-conversions.md'): + self.assertNotIn(name,names) + def test_schema_publisher_serves_exact_new_id_and_bytes(self): tree=ast.parse((ROOT/'web/build.py').read_text()) nodes=[node for node in tree.body if diff --git a/tests/test_site.py b/tests/test_site.py index baed70a..1ab3fcc 100644 --- a/tests/test_site.py +++ b/tests/test_site.py @@ -687,6 +687,8 @@ def test_living_pages_link_to_current_source(self) -> None: "conformance/evaluation/index.html": "conformance/evaluation/README.md", "spec/checkpoint-witness/index.html": "spec/checkpoint-witness.md", "spec/dialect/index.html": "spec/dialect.md", + # Rendered from the immutable final notes; docs/releases/v0.3.0.md is only a pointer. + "project/releases/0.3.0/index.html": "releases/v0.3.0.md", } for output, source in cases.items(): with self.subTest(output=output): @@ -703,6 +705,24 @@ def test_pages_carry_neutral_generator_and_commit_provenance(self) -> None: generator = overview.split('name="generator" content="')[1].split('"')[0] self._assert_no_retired_brand(generator, "generator meta") + def test_the_0_3_0_release_page_serves_the_final_notes_and_pins(self) -> None: + page = (self.output / "project/releases/0.3.0/index.html").read_text(encoding="utf-8") + inventory = json.loads( + (ROOT / "releases/v0.3.0.retained-column.json").read_text(encoding="utf-8") + ) + for text in ("What this release pins", "Checks run for this release", + "https://judgmentpack.org/schema/0.3.0/judgment-pack-core.schema.json", + inventory["schemaSha256"], inventory["corpusSha256"]): + with self.subTest(text=text): + self.assertIn(text, page) + # The resources those pins name stay served at their exact $id paths. + for served, source in ( + ("schema/0.3.0/judgment-pack-core.schema.json", "schema/judgment-pack-core.schema.json"), + ("schema/retained-column/1/contract.schema.json", inventory["schema"]), + ): + with self.subTest(served=served): + self.assertEqual((ROOT / source).read_bytes(), (self.output / served).read_bytes()) + def test_schemas_are_served_at_their_canonical_id_path(self) -> None: cases = { "schema/0.3.0/judgment-pack-core.schema.json": ROOT diff --git a/tools/build_release.py b/tools/build_release.py index 4a1f28b..bb08106 100644 --- a/tools/build_release.py +++ b/tools/build_release.py @@ -51,13 +51,23 @@ "tools/_retained_column.py", "tools/check_retained_column.py", "tools/dialect-verify-ts", "requirements-dev.txt") - -def validate_retained_inventory(root: Path = ROOT) -> None: - release = json.loads((root / RETAINED_INVENTORY).read_text("utf-8")) +# The final carries RC2's selected leaf byte for byte under its own inventory. It has no +# distribution mapping: VERSIONING.md allowlists the two RC mappings only. +CORE_FINAL_TAG = "v0.3.0" +FINAL_RETAINED_INVENTORY = "releases/v0.3.0.retained-column.json" +RETAINED_INVENTORIES = {CORE_RC2_TAG: RETAINED_INVENTORY, CORE_FINAL_TAG: FINAL_RETAINED_INVENTORY} +CORE_MILESTONE = {"number": 2, "title": "v0.3.0"} + + +def validate_retained_inventory(root: Path = ROOT, tag: str = CORE_RC2_TAG) -> None: + inventory = root / RETAINED_INVENTORIES[tag] + if not inventory.is_file(): + raise ValueError("retained-column inventory is missing for the exact tag") + release = json.loads(inventory.read_text("utf-8")) if set(release) != {"contract", "distribution", "maturity", "schema", "schemaSha256", "corpus", "corpusSha256", "files", "claim"}: raise ValueError("retained-column inventory has an invalid closed shape") - if (release["contract"] != "retained-column/1" or release["distribution"] != CORE_RC2_TAG + if (release["contract"] != "retained-column/1" or release["distribution"] != tag or release["maturity"] != "draft" or release["claim"] != "selected-leaf-support"): raise ValueError("retained-column inventory identity or scope differs") schema = "schema/retained-column/1/contract.schema.json" @@ -89,6 +99,17 @@ def validate_retained_inventory(root: Path = ROOT) -> None: raise ValueError("retained-column corpus companion input differs") +def validate_final_retained_inventory(root: Path = ROOT) -> None: + """Hold the final's inventory to the bytes RC2 published: only its distribution differs.""" + validate_retained_inventory(root, CORE_FINAL_TAG) + final = json.loads((root / FINAL_RETAINED_INVENTORY).read_text("utf-8")) + published = root / RETAINED_INVENTORY + if not published.is_file(): + raise ValueError("the published RC2 retained-column inventory is missing") + if {**final, "distribution": CORE_RC2_TAG} != json.loads(published.read_text("utf-8")): + raise ValueError("final retained-column inventory differs from the published RC2 inventory") + + POLICY_FORMAT = "1.0.0" POLICY_ROOT = "conformance/policy/1.0.0" POLICY_SCHEMA_ROOT = "schema/policy/1.0.0" @@ -209,8 +230,8 @@ def bundle_paths(tag: str, commit: str) -> tuple[str, ...]: paths.append(path) if tag in CORE_RC_IDENTITIES: paths.extend((f"releases/{tag}.json", "docs/releases/v0.3.0.md")) - if tag == CORE_RC2_TAG: - paths.extend((*RETAINED_BUNDLE_PATHS, RETAINED_INVENTORY, "releases/v0.3.0-rc.2.milestone.json")) + if tag in RETAINED_INVENTORIES: + paths.extend((*RETAINED_BUNDLE_PATHS, RETAINED_INVENTORIES[tag], f"releases/{tag}.milestone.json")) return (*paths, core_release_notes_path(tag).relative_to(ROOT).as_posix()) @@ -243,13 +264,16 @@ def release_version(tag: str) -> str: def validate_artifacts(version: str, tag: str, root: Path = ROOT) -> None: - """Check exact format pins; only the reviewed RC has a separate distribution identity. + """Check exact format pins; only the reviewed RCs have a separate distribution identity. The RC distributes proposed 0.3.0 artifacts, not a released conformance corpus. No arbitrary prerelease suffix may be stripped to infer a compatible specVersion. + The v0.3.0 final carries RC2's selected leaf under its own inventory and no mapping. """ if version != release_version(tag): raise ValueError("release version does not match the exact tag") + if tag not in CORE_RC_IDENTITIES and (root / "releases" / f"{tag}.json").exists(): + raise ValueError("only the allowlisted Core RCs have a release mapping") if tag in CORE_RC_IDENTITIES: mapping = root / "releases" / f"{tag}.json" if not mapping.is_file(): @@ -282,12 +306,16 @@ def validate_artifacts(version: str, tag: str, root: Path = ROOT) -> None: ) if schema.get("$id") != expected_schema_id: raise ValueError("schema $id does not point at the exact release version") - if tag == CORE_RC2_TAG: - validate_retained_inventory(root) - if json.loads((root / "releases/v0.3.0-rc.2.milestone.json").read_text("utf-8")) != {"number": 2, "title": "v0.3.0"}: - raise ValueError("RC2 must preserve the final Core milestone") + if tag in RETAINED_INVENTORIES: + if tag == CORE_FINAL_TAG: + validate_final_retained_inventory(root) + else: + validate_retained_inventory(root, tag) + milestone = root / "releases" / f"{tag}.milestone.json" + if not milestone.is_file() or json.loads(milestone.read_text("utf-8")) != CORE_MILESTONE: + raise ValueError(f"{tag} must preserve the final Core milestone") if "- Status: Accepted" not in (root / "rfcs/0029-retained-column-conversions.md").read_text("utf-8").splitlines(): - raise ValueError("RC2 release requires recorded RFC0029 acceptance at draft maturity") + raise ValueError(f"{tag} requires recorded RFC0029 acceptance at draft maturity") release_notes = core_release_notes_path(tag, root) if not release_notes.is_file(): raise ValueError("release notes are missing for the exact tag") diff --git a/web/DEPLOYMENT.md b/web/DEPLOYMENT.md index 4769f81..bc94a08 100644 --- a/web/DEPLOYMENT.md +++ b/web/DEPLOYMENT.md @@ -20,9 +20,10 @@ not edit it in place. Pull requests retain the required `repository-conformance` check, and pushes to `main` run it once. Neither event deploys a preview or the live site. Tag pushes use the release workflow to -verify artifacts, build checksummed bundles, create a draft prerelease, and attest the bundles; -maintainers still decide when to publish that draft. A `vMAJOR.MINOR.PATCH` tag takes main's -conformance evidence at the tagged commit instead of running the repository suite again +verify artifacts, build checksummed bundles, create a draft release, and attest the bundles; +maintainers still decide when to publish that draft. The draft of a `vMAJOR.MINOR.PATCH` tag is a +release; the draft of every other tag is marked pre-release. A `vMAJOR.MINOR.PATCH` tag takes +main's conformance evidence at the tagged commit instead of running the repository suite again (Release evidence, below). For every next Core or Policy tag, commit `releases/.milestone.json` before tagging, with @@ -90,7 +91,7 @@ step and its test in the same change. `policy-v` tags are outside every kit gram the whole suite. Either way the release job keeps, fresh and in the same order, the milestone and tagged-checkout admission, the exact Policy format and agreement corpus for `policy-v` tags, the bundle build with its tag, clean-tree, artifact-pin -and Policy tag-history checks, the release notes, the draft prerelease and the bundle attestation. +and Policy tag-history checks, the release notes, the draft release and the bundle attestation. Publication and its approval are unchanged. The suite can be reused because none of its tests reads the repository's tags, branch or diff --git a/web/build.py b/web/build.py index df9e3f8..a686ba1 100644 --- a/web/build.py +++ b/web/build.py @@ -782,7 +782,7 @@ def archive(self) -> str: source_ref="main", ), Page( - "docs/releases/v0.3.0.md", + "releases/v0.3.0.md", PurePosixPath("project/releases/0.3.0/index.html"), "0.3.0 release notes", "Scope, migration, included work, and maturity of the first Core release without a -draft suffix.", From d079cda0868a9389d90cfc84f7bdf54de4fe043f Mon Sep 17 00:00:00 2001 From: kikashy <35789537+kikashy@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:16:39 -0400 Subject: [PATCH 2/5] Final notes: state how the release's checks are recorded, with no placeholders (#231) The "Checks run for this release" section now states the mechanism: the release reuses main's repository-conformance evidence for the exact commit through the release evidence gate, the reused run's id and attempt are in the release's decision artifact and are appended to the published release body at publication, and the candidates were reviewed on their pull requests (#246, #248 and #258 for rc.1; #274 for rc.2, with its same-vendor statement). The notes test checks that form and that no placeholder remains. Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- releases/v0.3.0.md | 13 ++++++++++--- tests/test_release.py | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/releases/v0.3.0.md b/releases/v0.3.0.md index 5e658c1..d2b564f 100644 --- a/releases/v0.3.0.md +++ b/releases/v0.3.0.md @@ -130,9 +130,16 @@ all five preserved resources. Separately pinned Policy consumer bytes and format ## Checks run for this release -Repository conformance run `` passed the whole check list on `main` at -`
`, the commit this tag names. The release workflow reuses that exact-commit evidence -and runs the milestone, tag, artifact-pin and bundle checks again at the tag. +The release reused `main`'s `repository-conformance` evidence for this exact commit through the +release evidence gate (`scripts/dev/release_evidence.py`, configured by `.github/ci-evidence.json`) +instead of running the repository suite again. The milestone, tag, artifact-pin and bundle checks +ran again at the tag. The reused run's id and attempt are recorded in the release's decision +artifact (`release-evidence-`) and are appended to the published release body at +publication. + +The candidates were reviewed on their pull requests: `v0.3.0-rc.1` on #246, #248 and #258, and +`v0.3.0-rc.2` on #274, where same-vendor review under the authorized exception is identified as +such, as the `v0.3.0-rc.2` notes record. Results against this release name its tag and commit. Conformance never proves truth, authorization, safety or operational fitness. diff --git a/tests/test_release.py b/tests/test_release.py index c0df1e8..c2e3ef8 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -58,9 +58,16 @@ def test_final_notes_name_both_candidates_the_leaf_and_the_pins(self) -> None: with self.subTest(text=text): self.assertIn(text, notes) self.assertNotIn("if merged before the tag", notes) - # Only these two are completed at the tag, and completing them keeps this test passing. - self.assertLessEqual(set(re.findall(r"<[a-z][a-z ]+>", notes)), - {"", "
"}) + # The checks section states the mechanism, not values to fill in at the tag: the reused run's + # identity lives in the decision artifact and the published release body. + checks = notes.split("## Checks run for this release", 1)[1] + for text in ("evidence for this exact commit", "release evidence gate", + "(`release-evidence-`)", "appended to the published release body", + "#274", "same-vendor review under the authorized exception"): + with self.subTest(checks=text): + self.assertIn(text, re.sub(r"\s+", " ", checks)) + for placeholder in ("", "
"): + self.assertNotIn(placeholder, notes) def test_release_version_accepts_exact_semver_tags(self) -> None: self.assertEqual(BUILD_RELEASE.release_version("v0.1.0-draft"), "0.1.0-draft") From cdb9f6d450beba05e81b1e848729c6763994e3d1 Mon Sep 17 00:00:00 2001 From: kikashy <35789537+kikashy@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:35:06 -0400 Subject: [PATCH 3/5] Make v0.3.0 the retained-column leaf's normative support distribution (#231) The maintainer chose to make the final the leaf's normative home. - spec/retained-column.md, three lines: the support tuple's distribution version is 0.3.0; the inventory is releases/v0.3.0.retained-column.json; the normative support version is exactly 0.3.0 (the v0.3.0 distribution), v0.3.0-rc.2 was the candidate distribution of the same leaf bytes and remains published, and a support claim is made against a v0.3.0 installation. - tools/retained_column.py and dialect-verify-ts/src/retained.ts check the generated release-identity.json for tag v0.3.0 and releaseVersion 0.3.0, and releases/v0.3.0.retained-column.json with distribution v0.3.0, and report support version 0.3.0. They name 0.3.0 only: rc.2 installations run rc.2's published readers. - tools/build_release.py: the v0.3.0 bundles carry a generated release-identity.json (tag, releaseVersion, specVersion, maturity, sourceCommit), never a tracked mapping. The final's inventory must equal RC2's in schema, corpus and every other digest; only the companion page and the two readers may differ. Both readers must name the tag, its version and its inventory. - releases/v0.3.0.retained-column.json re-pins those three files; the schema and corpus digests are unchanged and the frozen corpus keeps its rc.2 labels, which the notes say in one sentence. - Tests: both readers' real installation check on a v0.3.0 copy; the builder's new rules; the CLI test builds a final bundle and both of its readers answer a 0.3.0 selection with support and a 0.3.0-rc.2 one with capability-unsupported. The RC2 route tests no longer validate RC2 on this tree, whose support identity is the final's. Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +- releases/v0.3.0.md | 32 +++-- releases/v0.3.0.retained-column.json | 6 +- spec/retained-column.md | 6 +- tests/test_release.py | 4 +- tests/test_retained_column.py | 36 +++++ tests/test_retained_release.py | 128 +++++++++++++----- tools/build_release.py | 50 +++++-- tools/dialect-verify-ts/src/retained.ts | 10 +- tools/dialect-verify-ts/test/retained.test.ts | 49 ++++++- tools/retained_column.py | 13 +- 11 files changed, 263 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f73886..2d4ab60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,8 @@ The first Core release without a `-draft` suffix, at maturity `draft`, after the accepted at draft maturity; #274): the [selected retained columns](spec/retained-column.md) companion, the versioned schema `schema/retained-column/1/contract.schema.json`, the corpus `conformance/retained-column/1/` and the two offline readers. It was first distributed in - `v0.3.0-rc.2`; the final carries the same bytes under `releases/v0.3.0.retained-column.json`. + `v0.3.0-rc.2`. The final is its normative support distribution (support version `0.3.0`, inventory + `releases/v0.3.0.retained-column.json`), with the schema and corpus bytes unchanged. - Name the next Core release `0.3.0`, define conformance maturity independently, and require `v0.3.0-rc.1` before release (#231). - Re-pin released Core schemas, manifests, packs, vectors, readers, and site routes to exact `specVersion` `0.3.0`, preserving `0.2.0-draft` artifacts (#231). diff --git a/releases/v0.3.0.md b/releases/v0.3.0.md index d2b564f..769a360 100644 --- a/releases/v0.3.0.md +++ b/releases/v0.3.0.md @@ -37,20 +37,22 @@ distribution identity is a pack version selector. ## The selected retained-column leaf -The release bundles carry the leaf's schema, corpus, companion prose and both offline readers with -the bytes `v0.3.0-rc.2` published. The final inventory `releases/v0.3.0.retained-column.json` -names distribution `v0.3.0` and records the same schema, corpus and file digests as the -candidate's inventory; the release builder refuses a final whose inventory differs from the -candidate's in anything but that name. - -The leaf's support tuple is the one its companion states: contract `retained-column/1`, -distribution version `0.3.0-rc.2`, the candidate's source commit, and the schema and corpus -digests below. It is independent of the pack `specVersion`, and declaring `0.3.0` does not select -the leaf. The final bundle has no generated `release-identity.json`, so its readers answer a -retained-column selection with `capability-unsupported`; a selected-leaf support claim is made -against an installation of the `v0.3.0-rc.2` distribution. The leaf's support claim is limited to -its named scope: it is no general Core, stable interoperability or general preparation-verifier -conformance claim. +This release is the leaf's normative support distribution. The support tuple is contract +`retained-column/1`, distribution version `0.3.0`, this release's source commit (supplied by the +generated `release-identity.json` in each bundle), and the schema and corpus digests below. It is +independent of the pack `specVersion`, and declaring `0.3.0` does not select the leaf. A leaf +support claim is made against a `v0.3.0` installation. `v0.3.0-rc.2` was the candidate +distribution of the same leaf and remains published; its installations keep their own readers and +`0.3.0-rc.2` tuple, and this release's readers name `0.3.0` only. + +The schema, the corpus and every other leaf file keep the bytes `v0.3.0-rc.2` published, so the +schema and corpus digests are unchanged. Only the companion's three support-identity sentences and +the two readers' installation identity changed. The final inventory +`releases/v0.3.0.retained-column.json` records those bytes, and the release builder refuses a final +whose inventory differs from the candidate's anywhere else. The frozen corpus files keep their +`v0.3.0-rc.2` labels: they name the candidate distribution whose bytes this release publishes +unchanged. The leaf's support claim is limited to its named scope: it is no general Core, stable +interoperability or general preparation-verifier conformance claim. ## Versioning and maturity @@ -127,6 +129,8 @@ all five preserved resources. Separately pinned Policy consumer bytes and format `sha256:0cb49f238c78391cb163a65474cc9af962df2bb1024a55c5310c2b784b2f0557`. - Retained-column corpus: `conformance/retained-column/1/cases.json`, SHA-256 `sha256:17b16ff7a9b74fe4cc81eb6f6129a6aabdd9834621d9e756aa2d4c1a3f245863`. +- Retained-column support: contract `retained-column/1`, support version `0.3.0`, inventory + `releases/v0.3.0.retained-column.json`. ## Checks run for this release diff --git a/releases/v0.3.0.retained-column.json b/releases/v0.3.0.retained-column.json index 49e3577..9724307 100644 --- a/releases/v0.3.0.retained-column.json +++ b/releases/v0.3.0.retained-column.json @@ -16,7 +16,7 @@ "schema/retained-column/1/contract.schema.json": "sha256:0cb49f238c78391cb163a65474cc9af962df2bb1024a55c5310c2b784b2f0557", "spec/dialect.md": "sha256:a10d6fc0f50b1cf450b2bd9156175529bed2a7d926d24567b47677076066cb5a", "spec/judgment-pack-core.md": "sha256:0ceb66c15ce6b8268e02f319720eb40766c616423cfa08caaeea56ae8ba3d2ef", - "spec/retained-column.md": "sha256:f17747d5daeba6953cef335900569c5b1cf1052b7f982c0146b6322687375b7d", + "spec/retained-column.md": "sha256:8a7ea107712899c1b9295269f421ff925e971d91288f1fa17e34f64388cd7b64", "tools/_retained_column.py": "sha256:65c91f1363769ecfdfbebb5b42ade4258aaaf7b61620b23ee68b4000fee96233", "tools/check_retained_column.py": "sha256:946709a979f9dd9fc1aa10ab4500d77128c053ad3e46f653537f83c29becad18", "tools/dialect-verify-ts/src/bounds.ts": "sha256:c90d6c5db69206d4361409e502bb54a5e81505e4ac5842b987b44e60fbdb53c1", @@ -27,13 +27,13 @@ "tools/dialect-verify-ts/src/main.ts": "sha256:ef1e5f28c2608d51f719adc74bad03266ce5f2fb5526673e878229dc260b99be", "tools/dialect-verify-ts/src/prepare.ts": "sha256:e185b78a8f4d51cac63557f6973ef8b4af99660c3cef15b1509156acc8bbf027", "tools/dialect-verify-ts/src/retained-engine.mjs": "sha256:4ab0e7bcae864552a987a53fcec97f834e280c4dbf8b3ae37b6ae950739a9825", - "tools/dialect-verify-ts/src/retained.ts": "sha256:5ae44bc9782168cdae87467a0eb9f4229382f46058ac64ef6369b1236fbe905d", + "tools/dialect-verify-ts/src/retained.ts": "sha256:156ca057e967e3ae36ba4c8a740ee0f41e87588826c9f55af70bd00688163302", "tools/dialect-verify-ts/src/schema.ts": "sha256:938ad0ffefc378a7b1cc3bc7777063f4ef2929fbf8b90132ca5eb76537981dd3", "tools/dialect-verify-ts/src/time.ts": "sha256:54afe326be3d163d325ea64ccaf907e83530a38250824d1877cc1b4726c92ca7", "tools/dialect-verify-ts/src/vector.ts": "sha256:289b68479edf607df1480e46b0e6a6ee04091feb5dd56ddee696fbb2a337a1cb", "tools/dialect-verify-ts/src/verify.ts": "sha256:5ae002b07b0f9d3755c9d00bc828420e27b9927948402d8e4b266540c4f5cc1b", "tools/dialect_verify.py": "sha256:fe3b773ce30ad8c395fe85500714a12d6cc4262ccc6bca05c44f7b3b40a60b60", - "tools/retained_column.py": "sha256:0598c3b31a0a4115eb4efdf2e8fccc6b75c558dfe7d09180b3acf2241e937447" + "tools/retained_column.py": "sha256:ba3e97b1a9f4e46fff808f9ac79bc208d5b2a0a4c4f71ea0d760b259ba12bc5c" }, "claim": "selected-leaf-support" } diff --git a/spec/retained-column.md b/spec/retained-column.md index 02c141d..203d602 100644 --- a/spec/retained-column.md +++ b/spec/retained-column.md @@ -20,12 +20,12 @@ below are required behavior of a selected supporting role, not optional implemen ## Installed identity and certification The complete trusted support tuple is contract `retained-column/1`, Spec distribution version -`0.3.0-rc.2`, the exact immutable Spec source commit, the new schema SHA-256, complete selected +`0.3.0`, the exact immutable Spec source commit, the new schema SHA-256, complete selected corpus SHA-256, profile and role (plus producerGrammar where applicable). Core packs continue to declare exact specVersion `0.3.0`; that does not select this leaf. RC2 evidence remains provisional for final Core conformance and is never described as a final Core release claim. -`releases/v0.3.0-rc.2.retained-column.json` inventories the new schema and all corpus inputs. +`releases/v0.3.0.retained-column.json` inventories the new schema and all corpus inputs. `corpusSha256` hashes the exact selected `conformance/retained-column/1/cases.json` bytes; that file names every normative case group and its external companion inputs by digest. The release inventory also binds the handwritten full-preparation fixtures and their driver. @@ -263,7 +263,7 @@ or a boolean support flag. Each entry is a closed object with required `contract producer section below for its three applicable roles. `contract` is exactly `"retained-column/1"`; `profile` is exactly `"base"` or `"extended"`. `spec` is closed with exactly `version` (nonempty version label), `revision` (40 lower-case hex characters identifying the exact Spec commit), `schemaSha256` -and `corpusSha256` (each `sha256:` plus 64 lower-case hex characters). The normative support version is exactly `0.3.0-rc.2`; the final immutable source revision +and `corpusSha256` (each `sha256:` plus 64 lower-case hex characters). The normative support version is exactly `0.3.0`, the `v0.3.0` distribution; `v0.3.0-rc.2` was the candidate distribution of the same leaf bytes and remains published, and a leaf support claim is made against a `v0.3.0` installation. The final immutable source revision and new schema/corpus digests come from the external release inventory described above. Test example pins are synthetic and grant no real support. Consumers compare all identity members exactly to trusted expected pins, never values learned solely from the report. Member order does not affect comparison. diff --git a/tests/test_release.py b/tests/test_release.py index c2e3ef8..66ad2e5 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -50,13 +50,15 @@ def test_final_notes_name_both_candidates_the_leaf_and_the_pins(self) -> None: ) for text in ("`v0.3.0-rc.1`", "published 2026-10-25", "`v0.3.0-rc.2`", "published 2026-10-27", "The leaf is part of this release.", + "This release is the leaf's normative support distribution.", + "support version `0.3.0`", "keep their `v0.3.0-rc.2` labels", "RFC 0024 is not part of this release; it remains a Draft.", "## What this release pins", "## Checks run for this release", "https://judgmentpack.org/schema/0.3.0/judgment-pack-core.schema.json", inventory["schemaSha256"], inventory["corpusSha256"], "## Migration note", "## Included work", "## Versioning and maturity"): with self.subTest(text=text): - self.assertIn(text, notes) + self.assertIn(text, re.sub(r"\s+", " ", notes)) self.assertNotIn("if merged before the tag", notes) # The checks section states the mechanism, not values to fill in at the tag: the reused run's # identity lives in the decision artifact and the published release body. diff --git a/tests/test_retained_column.py b/tests/test_retained_column.py index 43d0494..6018246 100644 --- a/tests/test_retained_column.py +++ b/tests/test_retained_column.py @@ -165,6 +165,42 @@ def test_review_cli_refusal_protocol(self): result=subprocess.run(command+args,capture_output=True,text=True,timeout=30,env={**os.environ,'NODE_OPTIONS':''}) self.assertEqual(3,result.returncode,result.stderr);self.assertEqual('',result.stdout) + def test_installed_identity_is_the_v0_3_0_distribution(self): + # The real installation check, on a disposable copy of a v0.3.0 installation. + inventory_path='releases/v0.3.0.retained-column.json' + inventory=json.loads((ROOT/inventory_path).read_text()) + commit='a'*40 + final={'releaseVersion':'0.3.0','tag':'v0.3.0','specVersion':'0.3.0','maturity':'draft','sourceCommit':commit} + spec={'version':'0.3.0','revision':commit,'schemaSha256':inventory['schemaSha256'],'corpusSha256':inventory['corpusSha256']} + def select(version='0.3.0'): + return retained_column.begin(json.dumps({'contract':'retained-column/1','spec':{**spec,'version':version}, + 'profile':'base','roles':['preparation-verification']}).encode()) + def unsupported(action): + with self.assertRaises(retained_column.engine.Refusal) as raised: + action() + self.assertEqual(('capability-unsupported',),raised.exception.args) + with tempfile.TemporaryDirectory(prefix='retained-installation-') as temporary: + root=Path(temporary) + for name in (inventory_path,*inventory['files']): + target=root/name;target.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(ROOT/name,target) + identity=root/'release-identity.json' + with patch.object(retained_column,'ROOT',root): + identity.write_text(json.dumps(final)) + self.assertEqual(spec,retained_column.installed_spec()) + self.assertEqual('0.3.0',select().selector['spec']['version']) + unsupported(lambda:select('0.3.0-rc.2')) + for changed in ({'tag':'v0.3.0-rc.2','releaseVersion':'0.3.0-rc.2'},{'releaseVersion':'0.3.0-rc.2'}, + {'maturity':'candidate'},{'sourceCommit':'A'*40}): + with self.subTest(changed=changed): + identity.write_text(json.dumps({**final,**changed}));unsupported(retained_column.installed_spec) + identity.write_text(json.dumps(final)) + listed=root/inventory_path;original=listed.read_bytes() + listed.write_text(json.dumps({**inventory,'distribution':'v0.3.0-rc.2'}));unsupported(retained_column.installed_spec) + listed.write_bytes(original) + pinned=root/inventory['corpus'];pinned.write_bytes(pinned.read_bytes()+b' ');unsupported(retained_column.installed_spec) + shutil.copyfile(ROOT/inventory['corpus'],pinned) + identity.unlink();unsupported(select) + def test_origin_specific_raw_value_bounds(self): result=subprocess.run([sys.executable,'-B',str(ROOT/'tools/check_retained_column.py'),'--raw-values-only'],cwd=ROOT,capture_output=True,text=True,timeout=60) self.assertEqual(0,result.returncode,result.stdout+result.stderr) diff --git a/tests/test_retained_release.py b/tests/test_retained_release.py index fc33303..7140e55 100644 --- a/tests/test_retained_release.py +++ b/tests/test_retained_release.py @@ -6,6 +6,7 @@ import copy import hashlib import json +import os import shutil import subprocess import sys @@ -33,49 +34,52 @@ def final_fixture(root): class RetainedRelease(unittest.TestCase): def test_closed_inventory_and_non_circular_pins(self): - build_release.validate_retained_inventory() - manifest=json.loads((ROOT/INVENTORY).read_text()) - self.assertNotIn(INVENTORY,manifest['files']) - for name in ('schema','corpus'): - self.assertEqual(manifest[name+'Sha256'],'sha256:'+hashlib.sha256((ROOT/manifest[name]).read_bytes()).hexdigest()) + build_release.validate_retained_inventory(ROOT,'v0.3.0') + digest=lambda name:'sha256:'+hashlib.sha256((ROOT/name).read_bytes()).hexdigest() + for inventory in (FINAL_INVENTORY,INVENTORY): + manifest=json.loads((ROOT/inventory).read_text()) + self.assertNotIn(inventory,manifest['files']) + for name in ('schema','corpus'): + self.assertEqual(manifest[name+'Sha256'],digest(manifest[name])) + # RC2's published pins still hold for every leaf byte but the final's revised support identity. + published=json.loads((ROOT/INVENTORY).read_text())['files'] + self.assertEqual(build_release.FINAL_REVISED_LEAF_FILES,{name for name,pin in published.items() if pin!=digest(name)}) source=(ROOT/'tools/build_release.py').read_text() self.assertIn('"sourceCommit": git(',source) def test_inventory_refuses_changed_and_missing_corpus_inputs(self): - manifest=json.loads((ROOT/INVENTORY).read_text()) + manifest=json.loads((ROOT/FINAL_INVENTORY).read_text()) with tempfile.TemporaryDirectory(prefix='retained-inventory-') as temporary: root=Path(temporary) - for name in [INVENTORY,*manifest['files']]: + for name in [FINAL_INVENTORY,*manifest['files']]: destination=root/name;destination.parent.mkdir(parents=True,exist_ok=True) shutil.copyfile(ROOT/name,destination) - build_release.validate_retained_inventory(root) + build_release.validate_retained_inventory(root,'v0.3.0') cases_path=root/manifest['corpus'];original_cases=cases_path.read_bytes();cases=json.loads(original_cases) del cases['inputs']['conformance/retained-column/1/preparations.json'] cases_path.write_text(json.dumps(cases)) changed=copy.deepcopy(manifest);changed['files'][manifest['corpus']]='sha256:'+hashlib.sha256(cases_path.read_bytes()).hexdigest();changed['corpusSha256']=changed['files'][manifest['corpus']] - (root/INVENTORY).write_text(json.dumps(changed)) + (root/FINAL_INVENTORY).write_text(json.dumps(changed)) with self.assertRaisesRegex(ValueError,'complete corpus input set'): - build_release.validate_retained_inventory(root) - cases_path.write_bytes(original_cases);(root/INVENTORY).write_text(json.dumps(manifest)) + build_release.validate_retained_inventory(root,'v0.3.0') + cases_path.write_bytes(original_cases);(root/FINAL_INVENTORY).write_text(json.dumps(manifest)) corpus=root/manifest['corpus'];corpus.write_bytes(corpus.read_bytes()+b' ') with self.assertRaisesRegex(ValueError,'immutable artifact bytes'): - build_release.validate_retained_inventory(root) + build_release.validate_retained_inventory(root,'v0.3.0') shutil.copyfile(ROOT/manifest['corpus'],corpus) (root/'conformance/retained-column/1/preparations.json').unlink() with self.assertRaisesRegex(ValueError,'complete artifact inventory'): - build_release.validate_retained_inventory(root) + build_release.validate_retained_inventory(root,'v0.3.0') def test_rc2_mapping_is_closed_and_keeps_final_milestone(self): self.assertEqual(build_release.CORE_RC2_IDENTITY,json.loads((ROOT/'releases/v0.3.0-rc.2.json').read_text())) self.assertEqual({'v0.3.0-rc.1','v0.3.0-rc.2'},set(build_release.CORE_RC_IDENTITIES)) self.assertEqual({'number':2,'title':'v0.3.0'},json.loads((ROOT/'releases/v0.3.0-rc.2.milestone.json').read_text())) - # Preparation does not satisfy adoption or release publication. - status=(ROOT/'rfcs/0029-retained-column-conversions.md').read_text() - if '- Status: Accepted' in status.splitlines(): + self.assertIn('- Status: Accepted',(ROOT/'rfcs/0029-retained-column-conversions.md').read_text().splitlines()) + # RC2 is built only from its own tag: the final revised the leaf's support identity, so RC2's + # route refuses this tree instead of re-publishing RC2 with the final's readers. + with self.assertRaisesRegex(ValueError,'immutable artifact bytes'): build_release.validate_artifacts('0.3.0-rc.2','v0.3.0-rc.2') - else: - with self.assertRaisesRegex(ValueError,'RFC0029 acceptance'): - build_release.validate_artifacts('0.3.0-rc.2','v0.3.0-rc.2') def test_rc2_scoped_mapping_inventory_and_acceptance_gates(self): # Disposable adoption fixture, not a claim that this candidate's public gates passed. @@ -88,6 +92,12 @@ def test_rc2_scoped_mapping_inventory_and_acceptance_gates(self): root=Path(temporary) for name in paths: target=root/name;target.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(ROOT/name,target) + # The fixture stands in for RC2's tree: RC2's own bytes for the three files the final revised + # live only at its tag, so the fixture's copy of the inventory is re-pinned to these copies. + fixture=json.loads((root/INVENTORY).read_text()) + for name in build_release.FINAL_REVISED_LEAF_FILES: + fixture['files'][name]='sha256:'+hashlib.sha256((root/name).read_bytes()).hexdigest() + (root/INVENTORY).write_text(json.dumps(fixture)) rfc=root/'rfcs/0029-retained-column-conversions.md' rfc.write_text(rfc.read_text().replace('- Status: Review','- Status: Accepted')) build_release.validate_artifacts('0.3.0-rc.2','v0.3.0-rc.2',root) @@ -115,11 +125,15 @@ def git(*args): attributes=(ROOT/'.gitattributes').read_text() self.assertIn('conformance/dialect/** export-ignore -text',attributes) - def test_final_inventory_is_rc2s_but_for_its_distribution(self): + def test_final_inventory_keeps_rc2s_leaf_bytes_but_its_support_identity(self): build_release.validate_final_retained_inventory() + build_release.validate_reader_support_identity('v0.3.0') final=json.loads((ROOT/FINAL_INVENTORY).read_text());candidate=json.loads((ROOT/INVENTORY).read_text()) self.assertEqual(('v0.3.0','v0.3.0-rc.2'),(final['distribution'],candidate['distribution'])) - self.assertEqual({**candidate,'distribution':'v0.3.0'},final) + revised={'spec/retained-column.md','tools/retained_column.py','tools/dialect-verify-ts/src/retained.ts'} + self.assertEqual(revised,set(build_release.FINAL_REVISED_LEAF_FILES)) + self.assertEqual(revised,{path for path,digest in final['files'].items() if candidate['files'][path]!=digest}) + self.assertEqual({**candidate,'distribution':'v0.3.0','files':None},{**final,'files':None}) self.assertEqual({'v0.3.0-rc.2':INVENTORY,'v0.3.0':FINAL_INVENTORY},build_release.RETAINED_INVENTORIES) self.assertEqual({'number':2,'title':'v0.3.0'},json.loads((ROOT/'releases/v0.3.0.milestone.json').read_text())) self.assertNotIn('v0.3.0',build_release.CORE_RC_IDENTITIES) @@ -136,19 +150,40 @@ def refuses(message): def json_change(name,change): path=root/name;original=path.read_bytes();value=json.loads(original);change(value);path.write_text(json.dumps(value)) return lambda:path.write_bytes(original) - # Changed leaf bytes with a final inventory regenerated to match them: not RC2's bytes. + def rewrite(name,old,new): + """Change a leaf file and re-pin it in the final inventory, so only the RC2 rule can refuse.""" + path=root/name;original=path.read_bytes();text=original.decode() + self.assertEqual(1,text.count(old),(name,old));path.write_text(text.replace(old,new)) + pin='sha256:'+hashlib.sha256(path.read_bytes()).hexdigest() + def repin(value): + value['files'][name]=pin + if name==inventory['corpus']:value['corpusSha256']=pin + if name==inventory['schema']:value['schemaSha256']=pin + restore=json_change(FINAL_INVENTORY,repin) + build_release.validate_retained_inventory(root,'v0.3.0') + return lambda:(restore(),path.write_bytes(original)) + # Changed RC2 bytes outside the revised support identity, re-pinned: not RC2's leaf. + for name,old,new in [(inventory['corpus'],'draft selected corpus candidate','draft selected corpus candidate'),(inventory['schema'],'"$id"','"$id" '), + ('tools/_retained_column.py','# Promoted','# Promoted'), + ('conformance/retained-column/1/README.md','This is','This is')]: + with self.subTest(changed=name): + restore=rewrite(name,old,new);refuses('differs from the published RC2 inventory');restore() + # The revised support identity may differ, re-pinned; the readers must still name the final. + restore=rewrite('spec/retained-column.md','exactly `0.3.0`, the','exactly `0.3.0` , the') + build_release.validate_artifacts('0.3.0','v0.3.0',root);restore() + for name,old,new in [('tools/retained_column.py','TAG = "v0.3.0"\n','TAG = "v0.3.0-rc.2"\n'), + ('tools/retained_column.py','VERSION = "0.3.0"\n','VERSION = "0.3.0-rc.2"\n'), + ('tools/dialect-verify-ts/src/retained.ts','INVENTORY = "releases/v0.3.0.retained-column.json"', + 'INVENTORY = "releases/v0.3.0-rc.2.retained-column.json"'), + ('tools/dialect-verify-ts/src/retained.ts','const TAG = "v0.3.0"','const TAG = "v0.3.0-rc.2"')]: + with self.subTest(reader=name,changed=new): + restore=rewrite(name,old,new);refuses('readers do not name the exact release distribution');restore() corpus=root/inventory['corpus'];original_corpus=corpus.read_bytes();corpus.write_bytes(original_corpus+b' ') - pin='sha256:'+hashlib.sha256(corpus.read_bytes()).hexdigest() - def repin(value): - value['files'][inventory['corpus']]=pin;value['corpusSha256']=pin - restore=json_change(FINAL_INVENTORY,repin) - build_release.validate_retained_inventory(root,'v0.3.0') - refuses('differs from the published RC2 inventory') - restore();refuses('immutable artifact bytes') - corpus.write_bytes(original_corpus) + refuses('immutable artifact bytes');corpus.write_bytes(original_corpus) for name,change,message in [ (INVENTORY,lambda v:v.update(claim='other'),'differs from the published RC2 inventory'), (INVENTORY,lambda v:v.update(extra=True),'differs from the published RC2 inventory'), + (INVENTORY,lambda v:v['files'].update({'tools/extra.py':'sha256:'+'0'*64}),'differs from the published RC2 inventory'), (FINAL_INVENTORY,lambda v:v.update(distribution='v0.3.0-rc.2'),'identity or scope differs'), (FINAL_INVENTORY,lambda v:v.update(maturity='candidate'),'identity or scope differs'), ('releases/v0.3.0.milestone.json',lambda v:v.update(number=3),'final Core milestone')]: @@ -183,8 +218,11 @@ def git(*args): 'conformance/policy','schema/policy'): self.assertNotIn(path,paths) - def test_final_cli_archives_the_leaf_with_no_rc_identity(self): - # The actual builder CLI on a disposable fixture repository and a real annotated tag. + def test_final_cli_archives_the_leaf_and_its_readers_support_it(self): + # The actual builder CLI on a disposable fixture repository and a real annotated tag; then both + # readers of the extracted final bundle answer a leaf selection. + node=shutil.which('node') + self.assertIsNotNone(node,'the bundle probe runs the TypeScript reader') with tempfile.TemporaryDirectory(prefix='final-archive-fixture-') as temporary: root=Path(temporary);final_fixture(root) for name in ('tools/build_release.py','tools/dialect_verify.py','requirements-dev.txt','.gitattributes'): @@ -205,14 +243,36 @@ def git(*args): with tarfile.open(root/'dist/judgment-pack-spec_0.3.0_artifacts.tar.gz') as archive: names={m.name[len(prefix):] for m in archive.getmembers() if m.isfile()} self.assertEqual((root/FINAL_INVENTORY).read_bytes(),archive.extractfile(prefix+FINAL_INVENTORY).read()) + self.assertEqual({'releaseVersion':'0.3.0','tag':'v0.3.0','specVersion':'0.3.0','maturity':'draft', + 'sourceCommit':commit},json.load(archive.extractfile(prefix+'release-identity.json'))) + archive.extractall(root/'installed',filter='data') with zipfile.ZipFile(root/'dist/judgment-pack-spec_0.3.0_artifacts.zip') as archive: self.assertEqual(names,{n[len(prefix):] for n in archive.namelist() if not n.endswith('/')}) for name in (FINAL_INVENTORY,'releases/v0.3.0.milestone.json','releases/v0.3.0.md','tools/retained_column.py', 'tools/dialect-verify-ts/src/retained.ts','schema/retained-column/1/contract.schema.json', - 'conformance/retained-column/1/cases.json'): + 'conformance/retained-column/1/cases.json','release-identity.json'): self.assertIn(name,names) - for name in ('release-identity.json','releases/v0.3.0.json',INVENTORY,'rfcs/0029-retained-column-conversions.md'): + for name in ('releases/v0.3.0.json',INVENTORY,'rfcs/0029-retained-column-conversions.md'): self.assertNotIn(name,names) + installed=root/'installed'/prefix.rstrip('/');inventory=json.loads((installed/FINAL_INVENTORY).read_text()) + def selector(version): + return json.dumps({'contract':'retained-column/1','profile':'base','roles':['preparation-verification'], + 'spec':{'version':version,'revision':commit,'schemaSha256':inventory['schemaSha256'], + 'corpusSha256':inventory['corpusSha256']}}) + python=('import sys\nsys.path.insert(0,sys.argv[1])\nfrom tools import retained_column\n' + 'try:\n retained_column.begin(sys.argv[2].encode());print("supported")\n' + 'except retained_column.engine.Refusal as refusal:print(refusal.args[0])\n') + typescript=('const [root,raw]=process.argv.slice(1);const m=await import(new URL("tools/dialect-verify-ts/src/retained.ts",' + '"file://"+root+"/").href);try{m.admitSelection(new TextEncoder().encode(raw));console.log("supported")}' + 'catch(e){if(e.code===undefined)throw e;console.log(e.code)}') + for version,answer in (('0.3.0','supported'),('0.3.0-rc.2','capability-unsupported')): + for language,command in (('python',[sys.executable,'-I','-c',python]), + ('typescript',[node,'--disable-wasm-trap-handler','--no-warnings','--input-type=module','-e',typescript])): + with self.subTest(version=version,language=language): + probe=subprocess.run([*command,str(installed),selector(version)],cwd=temporary,capture_output=True, + text=True,timeout=60,env={**os.environ,'NODE_OPTIONS':''}) + self.assertEqual(0,probe.returncode,probe.stderr) + self.assertEqual(answer,probe.stdout.strip()) def test_schema_publisher_serves_exact_new_id_and_bytes(self): tree=ast.parse((ROOT/'web/build.py').read_text()) diff --git a/tools/build_release.py b/tools/build_release.py index bb08106..02dec7a 100644 --- a/tools/build_release.py +++ b/tools/build_release.py @@ -4,6 +4,7 @@ from __future__ import annotations import argparse +import ast import hashlib import json import re @@ -51,11 +52,19 @@ "tools/_retained_column.py", "tools/check_retained_column.py", "tools/dialect-verify-ts", "requirements-dev.txt") -# The final carries RC2's selected leaf byte for byte under its own inventory. It has no -# distribution mapping: VERSIONING.md allowlists the two RC mappings only. +# The final is the leaf's normative support distribution. It carries RC2's schema, corpus and +# every other leaf byte unchanged under its own inventory; only the companion's support-version +# sentences and the two readers' installation identity may differ from RC2's. It has no +# distribution mapping (VERSIONING.md allowlists the two RC mappings only); its bundles carry a +# generated release-identity.json, never a tracked mapping, so readers can check sourceCommit. CORE_FINAL_TAG = "v0.3.0" +CORE_FINAL_IDENTITY = {"releaseVersion": "0.3.0", "tag": CORE_FINAL_TAG, "specVersion": "0.3.0", + "maturity": "draft"} +GENERATED_IDENTITIES = {**CORE_RC_IDENTITIES, CORE_FINAL_TAG: CORE_FINAL_IDENTITY} FINAL_RETAINED_INVENTORY = "releases/v0.3.0.retained-column.json" RETAINED_INVENTORIES = {CORE_RC2_TAG: RETAINED_INVENTORY, CORE_FINAL_TAG: FINAL_RETAINED_INVENTORY} +FINAL_REVISED_LEAF_FILES = frozenset({"spec/retained-column.md", "tools/retained_column.py", + "tools/dialect-verify-ts/src/retained.ts"}) CORE_MILESTONE = {"number": 2, "title": "v0.3.0"} @@ -100,16 +109,40 @@ def validate_retained_inventory(root: Path = ROOT, tag: str = CORE_RC2_TAG) -> N def validate_final_retained_inventory(root: Path = ROOT) -> None: - """Hold the final's inventory to the bytes RC2 published: only its distribution differs.""" + """Hold the final's inventory to the bytes RC2 published. + + The schema, corpus and every other pinned file keep RC2's digests; only the distribution and + the files that name the final's support identity may differ. + """ validate_retained_inventory(root, CORE_FINAL_TAG) final = json.loads((root / FINAL_RETAINED_INVENTORY).read_text("utf-8")) - published = root / RETAINED_INVENTORY - if not published.is_file(): + published_path = root / RETAINED_INVENTORY + if not published_path.is_file(): raise ValueError("the published RC2 retained-column inventory is missing") - if {**final, "distribution": CORE_RC2_TAG} != json.loads(published.read_text("utf-8")): + published = json.loads(published_path.read_text("utf-8")) + if (not isinstance(published, dict) or not isinstance(published.get("files"), dict) + or {**final, "distribution": CORE_RC2_TAG, "files": None} != {**published, "files": None} + or set(final["files"]) != set(published["files"]) + or any(digest != published["files"][path] for path, digest in final["files"].items() + if path not in FINAL_REVISED_LEAF_FILES)): raise ValueError("final retained-column inventory differs from the published RC2 inventory") +def validate_reader_support_identity(tag: str, root: Path = ROOT) -> None: + """Both readers must name this exact distribution, its version and its inventory.""" + expected = (tag, release_version(tag), RETAINED_INVENTORIES[tag]) + module = ast.parse((root / "tools/retained_column.py").read_text("utf-8")) + constants = {target.id: node.value.value for node in module.body if isinstance(node, ast.Assign) + for target in node.targets if isinstance(target, ast.Name) + and isinstance(node.value, ast.Constant)} + typescript = re.search(r'^const TAG = "([^"]*)", VERSION = "([^"]*)", INVENTORY = "([^"]*)";$', + (root / "tools/dialect-verify-ts/src/retained.ts").read_text("utf-8"), + re.MULTILINE) + if (tuple(constants.get(name) for name in ("TAG", "VERSION", "INVENTORY")) != expected + or typescript is None or typescript.groups() != expected): + raise ValueError("retained-column readers do not name the exact release distribution") + + POLICY_FORMAT = "1.0.0" POLICY_ROOT = "conformance/policy/1.0.0" POLICY_SCHEMA_ROOT = "schema/policy/1.0.0" @@ -309,6 +342,7 @@ def validate_artifacts(version: str, tag: str, root: Path = ROOT) -> None: if tag in RETAINED_INVENTORIES: if tag == CORE_FINAL_TAG: validate_final_retained_inventory(root) + validate_reader_support_identity(tag, root) else: validate_retained_inventory(root, tag) milestone = root / "releases" / f"{tag}.milestone.json" @@ -367,9 +401,9 @@ def build(tag: str, commit: str, output: Path) -> list[Path]: artifact = "judgment-pack-policy" if is_policy_tag(tag) else "judgment-pack-spec" prefix = f"{artifact}-{version}/" extra = () - if tag in CORE_RC_IDENTITIES: + if tag in GENERATED_IDENTITIES: # This generated member is covered by both archive checksums/attestations. - identity = {**CORE_RC_IDENTITIES[tag], "sourceCommit": git("rev-parse", "--verify", f"{commit}^{{commit}}")} + identity = {**GENERATED_IDENTITIES[tag], "sourceCommit": git("rev-parse", "--verify", f"{commit}^{{commit}}")} encoded = json.dumps(identity, sort_keys=True, separators=(",", ":")) + "\n" extra = (f"--add-virtual-file={prefix}release-identity.json:{encoded}",) common = ( diff --git a/tools/dialect-verify-ts/src/retained.ts b/tools/dialect-verify-ts/src/retained.ts index cda9ed4..5877358 100644 --- a/tools/dialect-verify-ts/src/retained.ts +++ b/tools/dialect-verify-ts/src/retained.ts @@ -19,16 +19,18 @@ const digest = (text: Uint8Array | string) => "sha256:" + createHash("sha256").u const plain = (v: JVal): any => engine.parse(decode(preparationForm(v))); const jval = (v: any): JVal => parse(JSON.stringify(v)); const bad = (code = "input-malformed"): never => { throw new Refusal(code, "selected retained-column contract"); }; +const TAG = "v0.3.0", VERSION = "0.3.0", INVENTORY = "releases/v0.3.0.retained-column.json"; export const installation = { identity(): Record { const identity = JSON.parse(readFileSync(pathResolve(ROOT, "release-identity.json"), "utf8")); - const inventory = JSON.parse(readFileSync(pathResolve(ROOT, "releases/v0.3.0-rc.2.retained-column.json"), "utf8")); - if (identity.tag !== "v0.3.0-rc.2" || identity.releaseVersion !== "0.3.0-rc.2" || identity.specVersion !== "0.3.0" - || identity.maturity !== "draft" || !/^[0-9a-f]{40}$/.test(identity.sourceCommit)) bad("capability-unsupported"); + const inventory = JSON.parse(readFileSync(pathResolve(ROOT, INVENTORY), "utf8")); + if (identity.tag !== TAG || identity.releaseVersion !== VERSION || identity.specVersion !== "0.3.0" + || identity.maturity !== "draft" || !/^[0-9a-f]{40}$/.test(identity.sourceCommit) + || inventory.distribution !== TAG) bad("capability-unsupported"); for (const [name, sha] of Object.entries(inventory.files)) { if (digest(readFileSync(pathResolve(ROOT, name))) !== sha) bad("capability-unsupported"); } - return { version: "0.3.0-rc.2", revision: identity.sourceCommit, schemaSha256: inventory.schemaSha256, corpusSha256: inventory.corpusSha256 }; + return { version: VERSION, revision: identity.sourceCommit, schemaSha256: inventory.schemaSha256, corpusSha256: inventory.corpusSha256 }; }, }; type Selection = { selector: any; raw: WeakMap; origin: "asserted" | "generated"; profilesBytes: number }; diff --git a/tools/dialect-verify-ts/test/retained.test.ts b/tools/dialect-verify-ts/test/retained.test.ts index c028fce..1920af8 100644 --- a/tools/dialect-verify-ts/test/retained.test.ts +++ b/tools/dialect-verify-ts/test/retained.test.ts @@ -1,5 +1,8 @@ // Both readers consume these same handwritten full-preparation expectations. -import { readFileSync } from "node:fs"; +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; import test from "node:test"; import assert from "node:assert/strict"; import { verify, verifySourceInput, verifyExport } from "../src/verify.ts"; @@ -9,6 +12,50 @@ const corpus = JSON.parse(readFileSync(new URL("../../../conformance/retained-co function inputs(row: any, selector = corpus.selector) { return { dialect: utf8(row.dialectJson), case: utf8(row.caseJson), acquisitions: utf8(row.acquisitionsJson), profiles: utf8(row.profilesJson), preparation: utf8(row.preparationJson), requireBinding: false, conversionContract: utf8(JSON.stringify(selector)) }; } +// The real installation check, exercised on a disposable copy of a v0.3.0 installation. +test("installed identity is the v0.3.0 distribution", async () => { + const repo = fileURLToPath(new URL("../../../", import.meta.url)); + const root = mkdtempSync(join(tmpdir(), "retained-installation-")); + try { + const inventoryPath = "releases/v0.3.0.retained-column.json"; + const inventory = JSON.parse(readFileSync(join(repo, inventoryPath), "utf8")); + for (const name of [inventoryPath, ...Object.keys(inventory.files)]) { + mkdirSync(dirname(join(root, name)), { recursive: true }); + copyFileSync(join(repo, name), join(root, name)); + } + const installed = await import(pathToFileURL(join(root, "tools/dialect-verify-ts/src/retained.ts")).href); + const commit = "a".repeat(40); + const final = { releaseVersion: "0.3.0", tag: "v0.3.0", specVersion: "0.3.0", maturity: "draft", sourceCommit: commit }; + const write = (identity: object) => writeFileSync(join(root, "release-identity.json"), JSON.stringify(identity)); + const spec = { version: "0.3.0", revision: commit, schemaSha256: inventory.schemaSha256, corpusSha256: inventory.corpusSha256 }; + const select = (version: string) => installed.admitSelection(utf8(JSON.stringify({ contract: "retained-column/1", + spec: { ...spec, version }, profile: "base", roles: ["preparation-verification"] }))); + const unsupported = (error: any) => error.code === "capability-unsupported"; + write(final); + assert.deepEqual(installed.installation.identity(), spec); + assert.equal(select("0.3.0").selector.spec.version, "0.3.0"); + assert.throws(() => select("0.3.0-rc.2"), unsupported); + for (const changed of [{ tag: "v0.3.0-rc.2", releaseVersion: "0.3.0-rc.2" }, { releaseVersion: "0.3.0-rc.2" }, + { maturity: "candidate" }, { sourceCommit: "A".repeat(40) }]) { + write({ ...final, ...changed }); + assert.throws(() => installed.installation.identity(), unsupported); + } + write(final); + const listed = join(root, inventoryPath); + writeFileSync(listed, JSON.stringify({ ...inventory, distribution: "v0.3.0-rc.2" })); + assert.throws(() => installed.installation.identity(), unsupported); + copyFileSync(join(repo, inventoryPath), listed); + const pinned = join(root, inventory.corpus); + writeFileSync(pinned, readFileSync(pinned, "utf8") + " "); + assert.throws(() => installed.installation.identity(), unsupported); + copyFileSync(join(repo, inventory.corpus), pinned); + assert.deepEqual(installed.installation.identity(), spec); + rmSync(join(root, "release-identity.json")); + assert.throws(() => select("0.3.0"), unsupported); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); installation.identity = () => corpus.testInstallation; for (const row of corpus.cases) test(row.id, () => { const result = verify(inputs(row)); diff --git a/tools/retained_column.py b/tools/retained_column.py index f1a6f50..1b7e8b2 100644 --- a/tools/retained_column.py +++ b/tools/retained_column.py @@ -20,22 +20,25 @@ import _retained_column as engine ROOT = Path(__file__).resolve().parents[1] -TAG = "v0.3.0-rc.2" +TAG = "v0.3.0" +VERSION = "0.3.0" +INVENTORY = "releases/v0.3.0.retained-column.json" ROLES = tuple(engine.ROLE_LIMITS) CAPS = engine.PROFILE_CAPS def installed_spec() -> dict[str, str]: identity = json.loads((ROOT / "release-identity.json").read_text("utf-8")) - inventory = json.loads((ROOT / "releases/v0.3.0-rc.2.retained-column.json").read_text("utf-8")) - if (identity.get("tag") != TAG or identity.get("releaseVersion") != "0.3.0-rc.2" + inventory = json.loads((ROOT / INVENTORY).read_text("utf-8")) + if (identity.get("tag") != TAG or identity.get("releaseVersion") != VERSION or identity.get("specVersion") != "0.3.0" or identity.get("maturity") != "draft" - or not re.fullmatch("[0-9a-f]{40}", identity.get("sourceCommit", ""))): + or not re.fullmatch("[0-9a-f]{40}", identity.get("sourceCommit", "")) + or inventory.get("distribution") != TAG): engine.fail("capability-unsupported") for name, digest in inventory["files"].items(): if "sha256:" + hashlib.sha256((ROOT / name).read_bytes()).hexdigest() != digest: engine.fail("capability-unsupported") - return {"version": "0.3.0-rc.2", "revision": identity["sourceCommit"], + return {"version": VERSION, "revision": identity["sourceCommit"], "schemaSha256": inventory["schemaSha256"], "corpusSha256": inventory["corpusSha256"]} From 8963d15360e54a54515f67ed3026a09793e08f57 Mon Sep 17 00:00:00 2001 From: kikashy <35789537+kikashy@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:59:12 -0400 Subject: [PATCH 4/5] Hold the final to RC2's published inventory; state the checks record and candidate period (#231) Review fixes on the v0.3.0 final preparation: - tools/build_release.py: the final is compared with RC2's inventory only when that file has the SHA-256 tag v0.3.0-rc.2 published, so a leaf change re-pinned in both inventories is refused. Tests cover that case, the honest tree, and the comparison behind the digest. - releases/v0.3.0.md: the reused run's id and attempt are in the gate's decision record kept as the release run's release-evidence- artifact (web/DEPLOYMENT.md, "Release evidence"); nothing claims a body edit. The rc.1 review list adds issues #259 and #261. A "Candidate period" paragraph gives both publication dates, the feedback record on #231 before the tag, and the one-week floor after rc.2. - VERSIONING.md: a candidate graduates with no change to its normative artifacts; text and tooling that named the candidate distribution are updated to name the release, and the release's notes list them. Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- VERSIONING.md | 4 +++- releases/v0.3.0.md | 15 +++++++++++---- tests/test_release.py | 12 +++++++++++- tests/test_retained_release.py | 15 ++++++++++++++- tools/build_release.py | 5 +++++ 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/VERSIONING.md b/VERSIONING.md index 0ded8e7..149f9e6 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -55,7 +55,9 @@ companion's `policy-v-draft.N` route remains independent and is Core convention. Across the project, `vX.Y.Z-rc.N` is a candidate of the complete milestone that graduates to -`vX.Y.Z` with no code change, and `vX.Y.Z-alpha.N`, used by the implementations and never by this +`vX.Y.Z` with no change to its normative artifacts (schemas, corpus, Core and companion rules); +text and tooling that named the candidate distribution are updated to name the release, and the +release's notes list those changes. `vX.Y.Z-alpha.N`, used by the implementations and never by this repository's Core route, is a scoped cut for a named consumer that never graduates. That first candidate is a distribution of the proposed exact `specVersion` `0.3.0` artifacts; diff --git a/releases/v0.3.0.md b/releases/v0.3.0.md index 769a360..4ad1fab 100644 --- a/releases/v0.3.0.md +++ b/releases/v0.3.0.md @@ -15,6 +15,12 @@ before it, and both remain immutable with their mappings and notes: The leaf is part of this release. Packs declare `specVersion` `0.3.0`; neither candidate's distribution identity is a pack version selector. +## Candidate period + +`v0.3.0-rc.1` was published on 2026-10-25 and `v0.3.0-rc.2` on 2026-10-27. Adopter feedback on +the candidates and its disposition are recorded on issue #231 before the tag, and the tag is cut no +earlier than one week after the publication of `v0.3.0-rc.2`. + ## Included work - Core escalation routes from RFC 0025: an exclusive single target or bounded conditional routes, @@ -137,11 +143,12 @@ all five preserved resources. Separately pinned Policy consumer bytes and format The release reused `main`'s `repository-conformance` evidence for this exact commit through the release evidence gate (`scripts/dev/release_evidence.py`, configured by `.github/ci-evidence.json`) instead of running the repository suite again. The milestone, tag, artifact-pin and bundle checks -ran again at the tag. The reused run's id and attempt are recorded in the release's decision -artifact (`release-evidence-`) and are appended to the published release body at -publication. +ran again at the tag. The reused run's id and attempt are in the gate's decision record, which the +release run keeps as its `release-evidence-` artifact (`web/DEPLOYMENT.md`, "Release +evidence"). -The candidates were reviewed on their pull requests: `v0.3.0-rc.1` on #246, #248 and #258, and +The candidates were reviewed on their pull requests: `v0.3.0-rc.1` on #246, #248 and #258, with +its whole-release review recorded on issue #259 and its recovery reviews on issue #261, and `v0.3.0-rc.2` on #274, where same-vendor review under the authorized exception is identified as such, as the `v0.3.0-rc.2` notes record. diff --git a/tests/test_release.py b/tests/test_release.py index 66ad2e5..dad00ad 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -33,6 +33,7 @@ def test_0_3_0_release_naming_is_recorded(self) -> None: self.assertIn("`v0.3.0`", versioning) # The two prerelease forms, and which of them this repository's Core route uses. self.assertIn("`vX.Y.Z-rc.N` is a candidate of the complete milestone", versioning) + self.assertIn("with no change to its normative artifacts", re.sub(r"\s+", " ", versioning)) self.assertIn("`vX.Y.Z-alpha.N`, used by the implementations and never by this", versioning) self.assertTrue(notes.is_file()) self.assertEqual("# v0.3.0", notes.read_text(encoding="utf-8").splitlines()[0]) @@ -64,10 +65,19 @@ def test_final_notes_name_both_candidates_the_leaf_and_the_pins(self) -> None: # identity lives in the decision artifact and the published release body. checks = notes.split("## Checks run for this release", 1)[1] for text in ("evidence for this exact commit", "release evidence gate", - "(`release-evidence-`)", "appended to the published release body", + "`release-evidence-` artifact", "issue #259", "issue #261", "#274", "same-vendor review under the authorized exception"): with self.subTest(checks=text): self.assertIn(text, re.sub(r"\s+", " ", checks)) + # The record of the reused run is the decision artifact (web/DEPLOYMENT.md), nothing else. + self.assertNotIn("appended", checks) + candidate = re.sub(r"\s+", " ", notes.split("## Candidate period", 1)[1].split("\n## ", 1)[0]) + for text in ("published on 2026-10-25", "on 2026-10-27", "recorded on issue #231 before the tag", + "no earlier than one week after the publication of `v0.3.0-rc.2`"): + with self.subTest(candidate=text): + self.assertIn(text, candidate) + self.assertIn("kept as the run's\n`release-evidence-` artifact", + (ROOT / "web" / "DEPLOYMENT.md").read_text(encoding="utf-8")) for placeholder in ("", "
"): self.assertNotIn(placeholder, notes) diff --git a/tests/test_retained_release.py b/tests/test_retained_release.py index 7140e55..9a85ae6 100644 --- a/tests/test_retained_release.py +++ b/tests/test_retained_release.py @@ -44,6 +44,8 @@ def test_closed_inventory_and_non_circular_pins(self): # RC2's published pins still hold for every leaf byte but the final's revised support identity. published=json.loads((ROOT/INVENTORY).read_text())['files'] self.assertEqual(build_release.FINAL_REVISED_LEAF_FILES,{name for name,pin in published.items() if pin!=digest(name)}) + # The builder holds the final to RC2's inventory by its published digest; this copy is unedited. + self.assertEqual('sha256:'+build_release.RC2_RETAINED_INVENTORY_SHA256,digest(INVENTORY)) source=(ROOT/'tools/build_release.py').read_text() self.assertIn('"sourceCommit": git(',source) @@ -168,6 +170,11 @@ def repin(value): ('conformance/retained-column/1/README.md','This is','This is')]: with self.subTest(changed=name): restore=rewrite(name,old,new);refuses('differs from the published RC2 inventory');restore() + # The same change with both inventories re-pinned: RC2's copy no longer has its published bytes. + restore=rewrite('tools/_retained_column.py','# Promoted','# Promoted') + pin='sha256:'+hashlib.sha256((root/'tools/_retained_column.py').read_bytes()).hexdigest() + restore_rc2=json_change(INVENTORY,lambda value:value['files'].update({'tools/_retained_column.py':pin})) + refuses('differs from the bytes RC2 published');restore_rc2();restore() # The revised support identity may differ, re-pinned; the readers must still name the final. restore=rewrite('spec/retained-column.md','exactly `0.3.0`, the','exactly `0.3.0` , the') build_release.validate_artifacts('0.3.0','v0.3.0',root);restore() @@ -188,7 +195,13 @@ def repin(value): (FINAL_INVENTORY,lambda v:v.update(maturity='candidate'),'identity or scope differs'), ('releases/v0.3.0.milestone.json',lambda v:v.update(number=3),'final Core milestone')]: with self.subTest(name=name,message=message): - restore=json_change(name,change);refuses(message);restore() + restore=json_change(name,change) + # An edited RC2 copy is refused by its published digest first; pinning the edited + # bytes here exercises the comparison behind that digest. + with patch.object(build_release,'RC2_RETAINED_INVENTORY_SHA256', + hashlib.sha256((root/INVENTORY).read_bytes()).hexdigest()): + refuses(message) + restore() for name,message in [(INVENTORY,'published RC2 retained-column inventory is missing'), (FINAL_INVENTORY,'inventory is missing for the exact tag'), ('releases/v0.3.0.milestone.json','final Core milestone')]: diff --git a/tools/build_release.py b/tools/build_release.py index 02dec7a..e23435b 100644 --- a/tools/build_release.py +++ b/tools/build_release.py @@ -65,6 +65,9 @@ RETAINED_INVENTORIES = {CORE_RC2_TAG: RETAINED_INVENTORY, CORE_FINAL_TAG: FINAL_RETAINED_INVENTORY} FINAL_REVISED_LEAF_FILES = frozenset({"spec/retained-column.md", "tools/retained_column.py", "tools/dialect-verify-ts/src/retained.ts"}) +# The SHA-256 of releases/v0.3.0-rc.2.retained-column.json as tag v0.3.0-rc.2 published it, so the +# final is held to RC2's published inventory, not to a working copy edited after publication. +RC2_RETAINED_INVENTORY_SHA256 = "b4d62b3259b24dee68d926750ab45247d974004eec2bd15814f7d24c89140160" CORE_MILESTONE = {"number": 2, "title": "v0.3.0"} @@ -119,6 +122,8 @@ def validate_final_retained_inventory(root: Path = ROOT) -> None: published_path = root / RETAINED_INVENTORY if not published_path.is_file(): raise ValueError("the published RC2 retained-column inventory is missing") + if sha256(published_path) != RC2_RETAINED_INVENTORY_SHA256: + raise ValueError("the RC2 retained-column inventory differs from the bytes RC2 published") published = json.loads(published_path.read_text("utf-8")) if (not isinstance(published, dict) or not isinstance(published.get("files"), dict) or {**final, "distribution": CORE_RC2_TAG, "files": None} != {**published, "files": None} From 1131a23547e299c2c883efcfd15c81cff42b2d88 Mon Sep 17 00:00:00 2001 From: kikashy <35789537+kikashy@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:19:07 -0400 Subject: [PATCH 5/5] Name v0.3.0 as the leaf's adoption release in the companion, Core, Dialect and RFC 0029 (#231) The maintainer's decision on the held finding (A2): the normative text stops naming RC2 as the leaf's home. - spec/retained-column.md line 6: accepted at draft maturity in the v0.3.0 release, first distributed as the v0.3.0-rc.2 candidate; line 266: the schema, corpus and digests are the candidate's, while the companion page and the two readers name the release. - spec/judgment-pack-core.md lines 54-55 and 371, spec/dialect.md lines 142 and 144: adopted at draft maturity in v0.3.0 (candidate distribution v0.3.0-rc.2), keeping each sentence's existing non-claims. - rfcs/0029: one dated note in the adoption section naming v0.3.0 as the accepted route's final distribution; its Status and history unchanged. - tools/build_release.py: the files allowed to differ from RC2's inventory are now the companion, Core and Dialect pages and the two readers (the RFC is not inventory-pinned); the final inventory re-pins the three pages. - releases/v0.3.0.md lists these text changes in one sentence. Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- releases/v0.3.0.md | 6 ++++-- releases/v0.3.0.retained-column.json | 6 +++--- rfcs/0029-retained-column-conversions.md | 2 ++ spec/dialect.md | 4 ++-- spec/judgment-pack-core.md | 6 +++--- spec/retained-column.md | 4 ++-- tests/test_release.py | 1 + tests/test_retained_release.py | 11 ++++++++--- tools/build_release.py | 8 +++++--- 9 files changed, 30 insertions(+), 18 deletions(-) diff --git a/releases/v0.3.0.md b/releases/v0.3.0.md index 4ad1fab..87cd144 100644 --- a/releases/v0.3.0.md +++ b/releases/v0.3.0.md @@ -52,8 +52,10 @@ distribution of the same leaf and remains published; its installations keep thei `0.3.0-rc.2` tuple, and this release's readers name `0.3.0` only. The schema, the corpus and every other leaf file keep the bytes `v0.3.0-rc.2` published, so the -schema and corpus digests are unchanged. Only the companion's three support-identity sentences and -the two readers' installation identity changed. The final inventory +schema and corpus digests are unchanged. Beyond the two readers' installation identity, the final +changed only these texts from `v0.3.0-rc.2`: the companion's status, support-tuple, inventory and +support-version sentences, the Core and Dialect sentences on the leaf's adoption, and a dated note +in RFC 0029 naming `v0.3.0` as the accepted route's final distribution. The final inventory `releases/v0.3.0.retained-column.json` records those bytes, and the release builder refuses a final whose inventory differs from the candidate's anywhere else. The frozen corpus files keep their `v0.3.0-rc.2` labels: they name the candidate distribution whose bytes this release publishes diff --git a/releases/v0.3.0.retained-column.json b/releases/v0.3.0.retained-column.json index 9724307..9128136 100644 --- a/releases/v0.3.0.retained-column.json +++ b/releases/v0.3.0.retained-column.json @@ -14,9 +14,9 @@ "conformance/retained-column/1/manifest.json": "sha256:b78cdac121b4b936614c9e652af3c78bde8b84184fd0a04f84effdd2c3eaf2c8", "conformance/retained-column/1/preparations.json": "sha256:ff9450279cc65db4422f3644975b5b1128396e2e9a1f17effd4dcc732bb5317a", "schema/retained-column/1/contract.schema.json": "sha256:0cb49f238c78391cb163a65474cc9af962df2bb1024a55c5310c2b784b2f0557", - "spec/dialect.md": "sha256:a10d6fc0f50b1cf450b2bd9156175529bed2a7d926d24567b47677076066cb5a", - "spec/judgment-pack-core.md": "sha256:0ceb66c15ce6b8268e02f319720eb40766c616423cfa08caaeea56ae8ba3d2ef", - "spec/retained-column.md": "sha256:8a7ea107712899c1b9295269f421ff925e971d91288f1fa17e34f64388cd7b64", + "spec/dialect.md": "sha256:729e491a0d855a51f69e9b9e7aadc24131209c388ee29e8820c57a7a7c845d5a", + "spec/judgment-pack-core.md": "sha256:151d64678db709f91ab0729c2f75516b89a027e24a3e22e1788dcdf0aa6faabc", + "spec/retained-column.md": "sha256:c1c9dd34063b9056175b7740d161341a5529e9642eaead3da263cd29a0b4b5c6", "tools/_retained_column.py": "sha256:65c91f1363769ecfdfbebb5b42ade4258aaaf7b61620b23ee68b4000fee96233", "tools/check_retained_column.py": "sha256:946709a979f9dd9fc1aa10ab4500d77128c053ad3e46f653537f83c29becad18", "tools/dialect-verify-ts/src/bounds.ts": "sha256:c90d6c5db69206d4361409e502bb54a5e81505e4ac5842b987b44e60fbdb53c1", diff --git a/rfcs/0029-retained-column-conversions.md b/rfcs/0029-retained-column-conversions.md index 7aa012b..4d42dfd 100644 --- a/rfcs/0029-retained-column-conversions.md +++ b/rfcs/0029-retained-column-conversions.md @@ -16,6 +16,8 @@ immutable and final v0.3.0 milestone #2 remains open. Acceptance permits this RC it does not establish immutable publication, stable interoperability, production Desk cutover or completion of Agent #171. +Note (2026-10-28): the accepted route's final distribution is `v0.3.0`; the RC2 wording above records the candidate's history. + The [adoption disposition](https://github.com/Judgment-Pack/judgment-pack-spec/pull/274#issuecomment-6431106029) records same-vendor OpenAI Codex review under the authorized exception and dispositions of the pre-adoption RFC comments at draft maturity. It judges the public review window and substantive diff --git a/spec/dialect.md b/spec/dialect.md index e1a6125..297ce32 100644 --- a/spec/dialect.md +++ b/spec/dialect.md @@ -139,9 +139,9 @@ be interpreted as described by BCP 14 when, and only when, they appear in all ca ## Selected retained-column support [Selected retained columns](retained-column.md) is accepted at draft maturity for exact -`retained-column/1` selection in RC2. It adds no implicit support to ordinary inputs. +`retained-column/1` selection, adopted in `v0.3.0` (candidate distribution `v0.3.0-rc.2`). It adds no implicit support to ordinary inputs. Its new leaf corpus/claim does not publish the forty-seven staged recordings or establish the -generic §14 preparation-verifier conformance claim. Immutable RC2 publication remains separate. +generic §14 preparation-verifier conformance claim. ## 1. Terms and the canonical forms diff --git a/spec/judgment-pack-core.md b/spec/judgment-pack-core.md index cb25571..41eea19 100644 --- a/spec/judgment-pack-core.md +++ b/spec/judgment-pack-core.md @@ -51,8 +51,8 @@ The artifacts in this repository have distinct roles: - [`spec/dialect.md`](dialect.md), a companion document, is the normative prose for the preparation-verifier reader conformance class it defines, and for nothing else; - [selected retained columns](retained-column.md) is accepted at draft maturity as normative prose - for the exact RC2 selected leaf support scope only; it establishes no implicit ordinary support - or generic preparation-verifier claim, and publication requires the immutable RC2 release; + for the selected leaf support scope only, adopted in `v0.3.0` (candidate distribution `v0.3.0-rc.2`); + it establishes no implicit ordinary support or generic preparation-verifier claim; - the evaluation corpus — the manifest and case fixtures under [`conformance/evaluation/`](../conformance/evaluation/README.md), not its README — is normative for evaluator conformance (§3.4) and for nothing else. This is the normative status the bullet below @@ -368,7 +368,7 @@ For the proposed exact selected `retained-column/1` contract only, [the retained-column companion](retained-column.md) specifies scoped Dialect artifact ceilings after its own selection and independent role admission. Absence of that selection uses this table. It does not raise Core pack, global transport, canonical parser, target-count or independent -work ceilings. This is an RC2 adoption candidate, not a support claim for an existing release. +work ceilings. It is adopted at draft maturity in `v0.3.0` (candidate distribution `v0.3.0-rc.2`), not a Core conformance claim. The following is the ceiling table for these formats. Numbers elsewhere in the companions state `base`; except for the exactly selected retained-column companion above, their diff --git a/spec/retained-column.md b/spec/retained-column.md index 203d602..a6e88ed 100644 --- a/spec/retained-column.md +++ b/spec/retained-column.md @@ -3,7 +3,7 @@ ## Status and exact scope This companion is accepted at **draft maturity** through [RFC0029](../rfcs/0029-retained-column-conversions.md) -for the exact `v0.3.0-rc.2` distribution. Acceptance is separate from immutable publication: +in the `v0.3.0` release, first distributed as the `v0.3.0-rc.2` candidate. Acceptance is separate from immutable publication: the exact tag, maintainer-published GitHub release and verified artifacts are required before published support can be claimed. RFC0029 is its informative design record. @@ -263,7 +263,7 @@ or a boolean support flag. Each entry is a closed object with required `contract producer section below for its three applicable roles. `contract` is exactly `"retained-column/1"`; `profile` is exactly `"base"` or `"extended"`. `spec` is closed with exactly `version` (nonempty version label), `revision` (40 lower-case hex characters identifying the exact Spec commit), `schemaSha256` -and `corpusSha256` (each `sha256:` plus 64 lower-case hex characters). The normative support version is exactly `0.3.0`, the `v0.3.0` distribution; `v0.3.0-rc.2` was the candidate distribution of the same leaf bytes and remains published, and a leaf support claim is made against a `v0.3.0` installation. The final immutable source revision +and `corpusSha256` (each `sha256:` plus 64 lower-case hex characters). The normative support version is exactly `0.3.0`, the `v0.3.0` distribution; `v0.3.0-rc.2` was its candidate distribution and remains published: the schema, the corpus and their digests are the candidate's, while this companion page and the two offline readers name the release; a leaf support claim is made against a `v0.3.0` installation. The final immutable source revision and new schema/corpus digests come from the external release inventory described above. Test example pins are synthetic and grant no real support. Consumers compare all identity members exactly to trusted expected pins, never values learned solely from the report. Member order does not affect comparison. diff --git a/tests/test_release.py b/tests/test_release.py index dad00ad..ac5bf8f 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -53,6 +53,7 @@ def test_final_notes_name_both_candidates_the_leaf_and_the_pins(self) -> None: "published 2026-10-27", "The leaf is part of this release.", "This release is the leaf's normative support distribution.", "support version `0.3.0`", "keep their `v0.3.0-rc.2` labels", + "the Core and Dialect sentences on the leaf's adoption, and a dated note in RFC 0029", "RFC 0024 is not part of this release; it remains a Draft.", "## What this release pins", "## Checks run for this release", "https://judgmentpack.org/schema/0.3.0/judgment-pack-core.schema.json", diff --git a/tests/test_retained_release.py b/tests/test_retained_release.py index 9a85ae6..95d0630 100644 --- a/tests/test_retained_release.py +++ b/tests/test_retained_release.py @@ -132,7 +132,9 @@ def test_final_inventory_keeps_rc2s_leaf_bytes_but_its_support_identity(self): build_release.validate_reader_support_identity('v0.3.0') final=json.loads((ROOT/FINAL_INVENTORY).read_text());candidate=json.loads((ROOT/INVENTORY).read_text()) self.assertEqual(('v0.3.0','v0.3.0-rc.2'),(final['distribution'],candidate['distribution'])) - revised={'spec/retained-column.md','tools/retained_column.py','tools/dialect-verify-ts/src/retained.ts'} + # The companion, the Core and Dialect sentences on the leaf's adoption, and the two readers. + revised={'spec/retained-column.md','spec/judgment-pack-core.md','spec/dialect.md', + 'tools/retained_column.py','tools/dialect-verify-ts/src/retained.ts'} self.assertEqual(revised,set(build_release.FINAL_REVISED_LEAF_FILES)) self.assertEqual(revised,{path for path,digest in final['files'].items() if candidate['files'][path]!=digest}) self.assertEqual({**candidate,'distribution':'v0.3.0','files':None},{**final,'files':None}) @@ -176,8 +178,11 @@ def repin(value): restore_rc2=json_change(INVENTORY,lambda value:value['files'].update({'tools/_retained_column.py':pin})) refuses('differs from the bytes RC2 published');restore_rc2();restore() # The revised support identity may differ, re-pinned; the readers must still name the final. - restore=rewrite('spec/retained-column.md','exactly `0.3.0`, the','exactly `0.3.0` , the') - build_release.validate_artifacts('0.3.0','v0.3.0',root);restore() + for name,old,new in [('spec/retained-column.md','exactly `0.3.0`, the','exactly `0.3.0` , the'), + ('spec/judgment-pack-core.md','adopted at draft maturity in `v0.3.0`','adopted at draft maturity in `v0.3.0`'), + ('spec/dialect.md','selection, adopted in `v0.3.0`','selection, adopted in `v0.3.0`')]: + with self.subTest(revised=name): + restore=rewrite(name,old,new);build_release.validate_artifacts('0.3.0','v0.3.0',root);restore() for name,old,new in [('tools/retained_column.py','TAG = "v0.3.0"\n','TAG = "v0.3.0-rc.2"\n'), ('tools/retained_column.py','VERSION = "0.3.0"\n','VERSION = "0.3.0-rc.2"\n'), ('tools/dialect-verify-ts/src/retained.ts','INVENTORY = "releases/v0.3.0.retained-column.json"', diff --git a/tools/build_release.py b/tools/build_release.py index e23435b..36ce37f 100644 --- a/tools/build_release.py +++ b/tools/build_release.py @@ -53,8 +53,9 @@ "tools/dialect-verify-ts", "requirements-dev.txt") # The final is the leaf's normative support distribution. It carries RC2's schema, corpus and -# every other leaf byte unchanged under its own inventory; only the companion's support-version -# sentences and the two readers' installation identity may differ from RC2's. It has no +# every other leaf byte unchanged under its own inventory; only the companion page, the Core and +# Dialect pages (their sentences on the leaf's adoption) and the two readers' installation +# identity may differ from RC2's. It has no # distribution mapping (VERSIONING.md allowlists the two RC mappings only); its bundles carry a # generated release-identity.json, never a tracked mapping, so readers can check sourceCommit. CORE_FINAL_TAG = "v0.3.0" @@ -63,7 +64,8 @@ GENERATED_IDENTITIES = {**CORE_RC_IDENTITIES, CORE_FINAL_TAG: CORE_FINAL_IDENTITY} FINAL_RETAINED_INVENTORY = "releases/v0.3.0.retained-column.json" RETAINED_INVENTORIES = {CORE_RC2_TAG: RETAINED_INVENTORY, CORE_FINAL_TAG: FINAL_RETAINED_INVENTORY} -FINAL_REVISED_LEAF_FILES = frozenset({"spec/retained-column.md", "tools/retained_column.py", +FINAL_REVISED_LEAF_FILES = frozenset({"spec/retained-column.md", "spec/judgment-pack-core.md", + "spec/dialect.md", "tools/retained_column.py", "tools/dialect-verify-ts/src/retained.ts"}) # The SHA-256 of releases/v0.3.0-rc.2.retained-column.json as tag v0.3.0-rc.2 published it, so the # final is held to RC2's published inventory, not to a working copy edited after publication.