From c2c9accca77171781cf6a5bc453aa12edf1ddfc8 Mon Sep 17 00:00:00 2001 From: Gabriel Wu <13583761+lucifer1004@users.noreply.github.com> Date: Mon, 20 Jul 2026 06:51:42 +0800 Subject: [PATCH] fix(lifecycle): bind RFC bumps to sealed amendments --- .claude/skills/gov/SKILL.md | 2 + .claude/skills/rfc-writer/SKILL.md | 2 + .claude/skills/spec/SKILL.md | 2 + AGENTS.md | 8 + docs/guide/rfcs.md | 33 ++- docs/rfc/RFC-0000.md | 27 +- docs/rfc/RFC-0001.md | 16 +- docs/rfc/RFC-0002.md | 28 +- ...on-bumps-to-sealed-version-amendments.toml | 85 ++++++ gov/rfc/RFC-0000/clauses/C-CLAUSE-DEF.toml | 6 +- .../RFC-0000/clauses/C-PHASE-LIFECYCLE.toml | 8 +- gov/rfc/RFC-0000/rfc.toml | 15 +- gov/rfc/RFC-0001/clauses/C-RFC-PHASE.toml | 4 +- gov/rfc/RFC-0001/rfc.toml | 12 +- gov/rfc/RFC-0002/clauses/C-CRUD-VERBS.toml | 5 +- .../RFC-0002/clauses/C-LIFECYCLE-VERBS.toml | 10 +- gov/rfc/RFC-0002/rfc.toml | 15 +- ...-rfc-candidate-version-lifecycle-gaps.toml | 53 ++++ src/cli/resources/clause.rs | 5 + src/cli/resources/rfc.rs | 3 +- src/cmd/edit/delete.rs | 48 ++-- src/cmd/edit/delete_referrers.rs | 108 +++++++- src/cmd/lifecycle/rfc.rs | 87 ++++--- src/cmd/new/artifacts/rfc.rs | 2 +- src/model/rfc.rs | 4 +- src/signature/mod.rs | 9 +- src/signature/tests.rs | 11 + tests/delete_tests/clause.rs | 166 +++++++++++- tests/edit_tests/rfc.rs | 3 +- tests/lifecycle_tests/rfc_cases/advance.rs | 106 +++++++- tests/lifecycle_tests/rfc_cases/bump.rs | 243 ++++++++++++++---- ...te__delete_clause_safeguard_normative.snap | 6 +- ...cess_current_normative_spec_candidate.snap | 26 ++ tests/snapshots/test_help__rfc_bump_help.snap | 3 +- ...ange_does_not_clear_pending_amendment.snap | 28 +- .../test_lifecycle__bump_major_version.snap | 30 ++- .../test_lifecycle__bump_minor_version.snap | 30 ++- .../test_lifecycle__bump_patch_version.snap | 30 ++- ...angelog_only_after_signature_baseline.snap | 24 +- ...s_empty_bump_after_signature_baseline.snap | 22 +- ...rfc_lifecycle__rfc_amendment_tracking.snap | 38 +-- tests/test_rfc_lifecycle.rs | 13 +- 42 files changed, 1113 insertions(+), 263 deletions(-) create mode 100644 gov/adr/ADR-0054-bind-rfc-version-bumps-to-sealed-version-amendments.toml create mode 100644 gov/work/2026-07-20-close-rfc-candidate-version-lifecycle-gaps.toml create mode 100644 tests/snapshots/test_delete__delete_clause_success_current_normative_spec_candidate.snap diff --git a/.claude/skills/gov/SKILL.md b/.claude/skills/gov/SKILL.md index 2d27c51a..1311ddb2 100644 --- a/.claude/skills/gov/SKILL.md +++ b/.claude/skills/gov/SKILL.md @@ -211,6 +211,8 @@ If implementation reveals a spec bug, do not silently deviate: - Release that amendment on the normative RFC with an appropriate patch, minor, or major `govctl rfc bump`; the new version starts in `spec`. - Continue authoring that new version in `spec` without another bump, then advance to `impl` to seal the corrected baseline. - A current-version changelog correction uses `govctl rfc edit changelog.*` and does not affect the sealed content baseline. +- If an RFC in `impl`, `test`, or `stable` has no sealed signature, stop lifecycle mutation. Run migration, or restore a consistent known-good baseline comprising the RFC TOML and its complete Clause TOML set from version-control history; do not reconstruct it from possibly amended content. See [[RFC-0000:C-PHASE-LIFECYCLE]]. +- For a Clause introduced in the current normative `spec` candidate whose `since` equals the RFC version, use `govctl clause delete`; it scans all governed artifacts and reports every referrer before allowing deletion. Inherited Clauses use deprecate or supersede. See [[RFC-0000:C-CLAUSE-DEF]]. Ask before the lifecycle-owned bump unless the user already granted full authority. diff --git a/.claude/skills/rfc-writer/SKILL.md b/.claude/skills/rfc-writer/SKILL.md index 64688f1f..277fbcd8 100644 --- a/.claude/skills/rfc-writer/SKILL.md +++ b/.claude/skills/rfc-writer/SKILL.md @@ -148,6 +148,7 @@ Before editing an RFC, inspect its status and phase: - In `spec`, RFC and Clause edits refine the current version candidate and do not require another version bump. - In `impl`, `test`, or `stable`, edits that change RFC or Clause content create an unversioned amendment. Lifecycle-owned metadata updates performed by dedicated verbs and current-version changelog-only corrections are exempt. The `/spec` or `/gov` workflow must release qualifying amendments with a version bump before further phase progression. - Entry from `spec` to `impl` seals the final RFC and Clause content as that version's implementation baseline. +- A version-changing bump is not available in `spec`; it opens the next candidate only from `impl`, `test`, or `stable` after an amendment. - A deprecated RFC cannot start another version lifecycle. This helper writes specification content but does not perform lifecycle verbs. @@ -164,6 +165,7 @@ it through the generic edit surface: - A Clause created in a draft RFC remains pending until RFC finalization assigns the current version. - A Clause created in a normative RFC already in `spec` receives the current RFC version immediately. - A Clause created in `impl`, `test`, or `stable` remains pending until a content-changing RFC bump assigns the next version. +- An unreferenced Clause introduced in the current normative `spec` candidate may be deleted while its `since` equals the RFC version; inherited Clauses must be deprecated or superseded. - A deprecated RFC does not accept new Clauses. ## Rendering Rules diff --git a/.claude/skills/spec/SKILL.md b/.claude/skills/spec/SKILL.md index b95365a2..5fde997e 100644 --- a/.claude/skills/spec/SKILL.md +++ b/.claude/skills/spec/SKILL.md @@ -108,8 +108,10 @@ For RFC work: - If the RFC is draft, do not bump it; when ready, ask permission before `govctl rfc finalize normative` - If a normative RFC is in `spec`, edit the current version candidate directly; do not bump it again for those edits - If a normative RFC is in `impl`, `test`, or `stable`, an RFC or Clause edit creates an unversioned amendment; after review, ask permission before using `govctl rfc bump` to release it as the next version in `spec` +- If a normative RFC in `impl`, `test`, or `stable` lacks a sealed signature, stop and run migration or restore the baseline; do not use a bump to infer it - If the RFC is deprecated, do not edit or version-bump it - Use `govctl rfc edit changelog.*` for current-version changelog corrections; these do not change version, phase, or the sealed content signature +- Delete an unreferenced Clause only in a draft RFC or when it was introduced in the current normative `spec` candidate (`since` equals the RFC version); inherited Clauses require deprecation or supersession Semver guidance for RFC amendments: diff --git a/AGENTS.md b/AGENTS.md index d879ddcd..071992e6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,6 +52,7 @@ RFCs are constitutional law. Code that conflicts with a normative RFC is a bug. - No silent deviation: fix the code or propose an RFC amendment - Normative RFC content MAY continue changing without another bump while its current version remains in `spec` - Content changed after entry to `impl` is an unversioned amendment and MUST be released by a version bump before further phase progression +- Version-changing bumps open a new candidate only from `impl`, `test`, or `stable`; they MUST NOT be used to retarget an open `spec` candidate - Cite RFC clauses when implementing invariants ### Law 2: Phase Discipline @@ -106,6 +107,10 @@ Within one version, phase transitions are forward-only. A content-changing bump after `impl`, `test`, or `stable` starts the next version in `spec`; it is not a backward transition of the sealed version. +Entry from `spec` to `impl` sets or replaces the sealed content signature. If an +RFC already in `impl`, `test`, or `stable` has no sealed signature, do not bump or +advance it; run migration or restore the baseline from version-control history. + Clause version assignment follows [[RFC-0000:C-CLAUSE-DEF]] and [[RFC-0002:C-LIFECYCLE-VERBS]]. @@ -113,6 +118,9 @@ Clause `since` is lifecycle-owned. Draft clauses receive it at finalization, clauses created in a normative RFC already in `spec` receive the current version immediately, and clauses created while a normative RFC is in `impl`, `test`, or `stable` remain pending until a content bump assigns the next version. +An unreferenced Clause introduced in the current normative `spec` candidate may +be deleted while `since` equals the RFC version. Inherited Clauses must be +deprecated or superseded. --- diff --git a/docs/guide/rfcs.md b/docs/guide/rfcs.md index 72614a50..3d140474 100644 --- a/docs/guide/rfcs.md +++ b/docs/guide/rfcs.md @@ -86,9 +86,10 @@ EOF ``` Finalization is the initial publication boundary for a draft RFC. A -version-changing bump requires normative status, so draft and deprecated RFCs -are rejected. Changelog-only corrections do not change the version and remain -available through either `rfc edit ... changelog` or `rfc bump --change`. +version-changing bump opens the next candidate from a normative RFC in `impl`, +`test`, or `stable`; draft, deprecated, and already-open `spec` RFCs are rejected. +Changelog-only corrections do not change the version and remain available +through either `rfc edit ... changelog` or `rfc bump --change`. ## Working with Clauses @@ -144,18 +145,22 @@ govctl clause edit RFC-0010:C-SCOPE text --stdin < clause-text.md ### Delete a Clause -Accidentally created clauses can be deleted from **draft** RFCs only: +Accidentally created clauses can be deleted before they become part of a sealed +version: ```bash govctl clause delete RFC-0010:C-MISTAKE -f ``` -**Safety:** Deletion is only allowed when: +**Safety:** Deletion is only allowed when no artifact references the Clause and +either: -- The RFC status is `draft` -- No other artifacts reference the clause +- The RFC status is `draft`; or +- The RFC is `normative/spec` and the Clause `since` equals the current RFC version. -For normative RFCs, use `govctl clause deprecate RFC-0010:C-OLD` instead. +The second case identifies a Clause introduced only in the open candidate. +Inherited Clauses and all Clauses in sealed phases use `govctl clause deprecate` +or `govctl clause supersede` instead. ### List Clauses @@ -220,6 +225,7 @@ Phase transitions are gated: - `spec → impl` requires `status = normative` - `spec → impl` requires every Clause to have a resolved `since` version - `spec → impl` seals the current RFC and Clause content signature +- `impl → test` and `test → stable` require that sealed signature to remain present - Each phase has invariants that must be satisfied The sealed signature is a content baseline, not a file lock. A code-only defect @@ -227,7 +233,9 @@ found during `impl` can be fixed without changing the RFC. If RFC or Clause content must change, edit it and then release that amendment with a patch, minor, or major bump. The bump starts the new version in `spec`; phase progression rejects the amendment until that happens. Changelog-only corrections -do not affect the sealed baseline. +do not affect the sealed baseline. If a sealed-phase RFC has no signature, bump +and later phase progression stop without changing files; run `govctl migrate` or +restore the baseline from version-control history instead of guessing it. ## Versioning @@ -242,9 +250,10 @@ govctl rfc bump RFC-0010 --minor -m "Add new clause for edge case" govctl rfc bump RFC-0010 --major -m "Breaking change to API contract" ``` -A content-changing bump starts the new version in `spec`. RFC and Clause content -can continue changing during that `spec` phase without another version bump; -advancing to `impl` seals the final content for the version. +A content-changing bump starts the new version in `spec` only from `impl`, `test`, +or `stable`. RFC and Clause content can continue changing during that `spec` phase +without another version bump; a second version-changing bump is rejected. +Advancing to `impl` seals the final content for the version. Use change-only bump syntax to append categorized metadata without changing the RFC version: diff --git a/docs/rfc/RFC-0000.md b/docs/rfc/RFC-0000.md index 87f19d66..a63972cd 100644 --- a/docs/rfc/RFC-0000.md +++ b/docs/rfc/RFC-0000.md @@ -1,9 +1,9 @@ - + # RFC-0000: govctl Governance Framework -> **Version:** 1.6.1 | **Status:** normative | **Phase:** stable +> **Version:** 1.7.0 | **Status:** normative | **Phase:** stable --- @@ -104,23 +104,25 @@ Phase rules: - Phase transitions MUST NOT skip a phase. - Each phase transition MUST satisfy the preceding phase gate. - `stable` MUST be terminal for one RFC version. -- A later version bump MAY start another phase lifecycle for the RFC. +- A version-changing bump MUST be rejected while the current version remains in `spec`. +- A later version-changing bump MAY start another phase lifecycle only when the current version is in `impl`, `test`, or `stable`. - A version bump that releases a detected content amendment MUST start the new version at `spec`. - Only a normative RFC MAY advance from `spec` to `impl`. - While phase is `spec`, RFC and clause content belongs to the current version candidate and MAY change without another version bump. -- The `spec` -> `impl` transition MUST replace the stored amendment signature with a signature of the current RFC and clause content. +- The `spec` -> `impl` transition MUST set or replace the stored amendment signature with a signature of the current RFC and clause content. - The `spec` -> `impl` transition MUST reject pending clauses as defined by [RFC-0000:C-CLAUSE-DEF](../rfc/RFC-0000.md#rfc-0000c-clause-def). - Changelog-only updates MUST preserve phase. - Changelog-only updates MUST NOT change the amendment signature. - In `impl`, `test`, or `stable`, content that differs from the stored amendment signature is an unversioned amendment. - An unversioned amendment MUST be released by a version bump before further phase progression. +- A normative RFC in `impl`, `test`, or `stable` MUST have a stored amendment signature. A version-changing bump or later phase progression MUST reject a missing signature without mutation and MUST leave pending clauses unchanged. - A draft RFC MUST NOT enter `stable`. - A deprecated RFC MUST NOT enter `impl` or `test`. For amendment detection, RFC content is the canonical RFC and clause data other than RFC `version`, `phase`, `changelog`, and `signature` fields. Those four fields are lifecycle or changelog bookkeeping and MUST be excluded from amendment comparison. A stored signature is the sealed content baseline for the current version after it enters `impl`. While the version remains in `spec`, establishing or replacing that baseline MUST NOT be treated as a content amendment. While the version remains in `spec`, establishing or replacing that baseline MUST NOT require another version bump. **Rationale:** -Scoping phase to one version lets later amendments reuse the existing ordered gates. The `spec` phase is the mutable authoring boundary for that version, and entry into `impl` seals the exact contract that implementation follows. Defining the comparison surface prevents phase progression and changelog bookkeeping from recursively appearing as new content amendments. +Scoping phase to one version lets later amendments reuse the existing ordered gates. The `spec` phase is the mutable authoring boundary for that version, and entry into `impl` seals the exact contract that implementation follows. Defining the comparison surface prevents phase progression and changelog bookkeeping from recursively appearing as new content amendments. Rejecting another version-changing bump from `spec` prevents an unsealed candidate from becoming compatibility history. A missing signature outside `spec` cannot establish a trustworthy amendment baseline through a version change; it requires migration or restoration of the sealed baseline. > **Tags:** `core`, `lifecycle` @@ -194,7 +196,7 @@ Clause kinds: Deprecation is a lifecycle state (in `status`), not a document kind. -The `since` field identifies the first RFC version that contains the clause. A clause whose `since` field is absent is a pending clause. +The `since` field identifies the first RFC version that contains the clause. A clause whose `since` field is absent is a pending clause. `since` is lifecycle-owned: generic editing MUST NOT set, change, or remove it. Finalization and version bumps MUST assign `since` only to pending clauses and MUST preserve every non-pending clause's existing `since` value. Clause creation and version assignment MUST follow these rules: - A new clause in a draft RFC MUST be pending. @@ -204,10 +206,12 @@ Clause creation and version assignment MUST follow these rules: - A version bump of a normative RFC MUST assign the new RFC version to every pending clause. - Creating a clause in a deprecated RFC MUST be rejected. +Permanent clause deletion MUST be allowed when the containing RFC is draft. Permanent clause deletion MUST also be allowed when the containing RFC is normative, its phase is `spec`, and the clause `since` equals the RFC current version. Permanent deletion MUST be rejected for every other RFC status, phase, or clause-version combination. Permanent deletion MUST reject a clause referenced by any other artifact and MUST report the referencing artifact IDs. + Advancing a normative RFC from `spec` to `impl` MUST reject every pending clause. That transition MUST NOT assign clause version metadata. That transition MUST NOT rewrite existing clause version metadata. **Rationale:** -A clause can record its version immediately only after that target version is known. Initial draft clauses receive a version only at finalization, amendments created outside `spec` receive a version at the next normative RFC bump, and clauses added to an already-open normative `spec` version can safely use the current version. This preserves version provenance without making phase advancement own clause metadata. +A clause can record its version immediately only after that target version is known. Initial draft clauses receive a version only at finalization, amendments created outside `spec` receive a version at the next normative RFC bump, and clauses added to an already-open normative `spec` version can safely use the current version. This preserves version provenance without making phase advancement own clause metadata. Lifecycle-only assignment and preservation make `since == current version` reliable evidence that a Clause belongs only to the open candidate, while the reference gate prevents dangling governance links. > **Tags:** `core`, `schema` @@ -430,6 +434,15 @@ A guard check MUST pass only when the command exits successfully. If `pattern` i ## Changelog +### v1.7.0 (2026-07-20) + +Define single-candidate RFC version boundaries + +#### Changed + +- Restrict version-changing bumps to sealed RFC versions +- Allow deletion of unreferenced Clauses introduced in the current spec candidate + ### v1.6.1 (2026-07-16) Clarify Clause version assignment at RFC publication boundaries diff --git a/docs/rfc/RFC-0001.md b/docs/rfc/RFC-0001.md index 5dd6ff7c..ae9f5ff8 100644 --- a/docs/rfc/RFC-0001.md +++ b/docs/rfc/RFC-0001.md @@ -1,9 +1,9 @@ - + # RFC-0001: Lifecycle State Machines -> **Version:** 0.6.0 | **Status:** normative | **Phase:** stable +> **Version:** 0.7.0 | **Status:** normative | **Phase:** stable --- @@ -62,11 +62,11 @@ Within one RFC version, the following transitions MUST be rejected: - Any skip (for example, spec -> test or spec -> stable) - stable -> any (`stable` is terminal for that version) -A version bump that releases a detected RFC or clause content amendment MUST start the new RFC version in `spec`, regardless of the prior version's phase. This starts a new version lifecycle and is not a backward transition within one version. +A version bump that releases a detected RFC or clause content amendment MUST start the new RFC version in `spec`, regardless of the prior version's phase. This starts a new version lifecycle and is not a backward transition within one version. Version-changing bump eligibility MUST follow [RFC-0000:C-PHASE-LIFECYCLE](../rfc/RFC-0000.md#rfc-0000c-phase-lifecycle). Only a normative RFC MAY advance from `spec` to `impl`. A normative RFC's current `spec` content is an authoring candidate rather than an implementation conformance baseline. The most recently sealed version remains the implementation baseline until the current candidate enters `impl`. -RFC and clause content MAY change while the current version remains in `spec`. Such changes belong to the current version. Such changes MUST NOT require another version bump. Advancing from `spec` to `impl` MUST seal the current amendment content as the version's stored signature. The transition MUST reject every pending clause defined by [RFC-0000:C-CLAUSE-DEF](../rfc/RFC-0000.md#rfc-0000c-clause-def). +RFC and clause content MAY change while the current version remains in `spec`. Such changes belong to the current version. Such changes MUST NOT require another version bump. Advancing from `spec` to `impl` MUST set or replace the current version's stored signature with the current amendment content. The transition MUST reject every pending clause defined by [RFC-0000:C-CLAUSE-DEF](../rfc/RFC-0000.md#rfc-0000c-clause-def). After the current version enters `impl`, amendment content that differs from the stored signature MUST be treated as an unversioned amendment. Phase progression MUST reject that amendment until a version bump releases it and starts the next version in `spec`. @@ -239,6 +239,14 @@ Future gates MAY be added via RFC amendment, but MUST NOT break existing valid w ## Changelog +### v0.7.0 (2026-07-20) + +Clarify RFC candidate phase transitions + +#### Changed + +- Bind version bumps and signature sealing to explicit phase boundaries + ### v0.6.0 (2026-07-16) Align RFC phase gates with current-version sealing diff --git a/docs/rfc/RFC-0002.md b/docs/rfc/RFC-0002.md index ed7a1399..dc57c908 100644 --- a/docs/rfc/RFC-0002.md +++ b/docs/rfc/RFC-0002.md @@ -1,9 +1,9 @@ - + # RFC-0002: CLI Resource Model and Command Architecture -> **Version:** 0.13.2 | **Status:** normative | **Phase:** stable +> **Version:** 0.14.0 | **Status:** normative | **Phase:** stable --- @@ -239,9 +239,8 @@ For guards: - MUST error with list of referencing artifacts/config if any references exist For clauses: -- MUST only allow deletion if containing RFC is in `draft` status -- MUST verify no other artifacts reference the clause ID before deletion -- MUST error with list of referencing artifacts if any references exist +- MUST enforce the deletion eligibility and referential-integrity requirements in [RFC-0000:C-CLAUSE-DEF](../rfc/RFC-0000.md#rfc-0000c-clause-def) +- MUST error with the list of referencing artifact IDs when references prevent deletion For Work Items: - MUST only allow deletion if status is `queue` @@ -291,12 +290,14 @@ Resource-specific lifecycle and constrained-mutation operations implement state - A `spec` -> `impl` transition MUST require normative RFC status. - A `spec` -> `impl` transition MUST reject every pending clause. - A `spec` -> `impl` transition MUST NOT assign clause version metadata. - - A `spec` -> `impl` transition MUST replace the stored amendment signature with a signature of the current amendment content. + - A `spec` -> `impl` transition MUST set or replace the stored amendment signature with a signature of the current amendment content. - A later phase transition MUST reject amendment content that differs from the stored signature. 4. `govctl rfc bump -m ` - The version-changing form MUST require normative RFC status. - - Rejection for draft or deprecated RFC status MUST occur without mutation. + - The version-changing form MUST enforce the source-phase and stored-signature eligibility in [RFC-0000:C-PHASE-LIFECYCLE](../rfc/RFC-0000.md#rfc-0000c-phase-lifecycle). + - Rejection for draft or deprecated RFC status, phase `spec`, or a missing sealed signature MUST occur without mutation. + - Missing-signature rejection outside `spec` MUST leave pending clauses unchanged and MUST instruct the user to migrate the repository or restore its sealed baseline. - Version bumping MUST follow semantic versioning. - A version bump MUST update the changelog automatically. - A version bump MAY include `--change ` for additional changelog entries. @@ -304,10 +305,8 @@ Resource-specific lifecycle and constrained-mutation operations implement state - Amendment comparison MUST exclude RFC `version`, `phase`, `changelog`, and `signature` fields. - A bump that releases detected RFC or clause content MUST set phase to `spec`. - A bump MUST assign its new version to pending clauses as defined by [RFC-0000:C-CLAUSE-DEF](../rfc/RFC-0000.md#rfc-0000c-clause-def). - - A bump MAY establish a baseline amendment signature for an RFC that does not yet have one. - - Establishing a baseline without a detected content amendment MUST preserve phase. - `--change` without a bump level MUST remain a changelog-only operation. - - The normative-status requirement for version-changing bumps MUST NOT apply to `--change` without a bump level. + - The normative-status and source-phase requirements for version-changing bumps MUST NOT apply to `--change` without a bump level. - `--change` without a bump level MUST resolve the unique current changelog entry by matching the entry version to the RFC version. - `--change` without a bump level MUST reject zero or multiple current entries without mutation. - `--change` without a bump level MUST preserve the RFC version. @@ -816,6 +815,15 @@ Search is discovery across the governance corpus, not a resource-specific CRUD o ## Changelog +### v0.14.0 (2026-07-20) + +Align lifecycle commands with sealed RFC versions + +#### Changed + +- Enforce candidate bump and signature baseline rules +- Permit deletion of unreferenced current-candidate Clauses + ### v0.13.2 (2026-07-16) Clarify lifecycle rollback restoration state diff --git a/gov/adr/ADR-0054-bind-rfc-version-bumps-to-sealed-version-amendments.toml b/gov/adr/ADR-0054-bind-rfc-version-bumps-to-sealed-version-amendments.toml new file mode 100644 index 00000000..904fde4d --- /dev/null +++ b/gov/adr/ADR-0054-bind-rfc-version-bumps-to-sealed-version-amendments.toml @@ -0,0 +1,85 @@ +#:schema ../schema/adr.schema.json + +[govctl] +id = "ADR-0054" +title = "Bind RFC version bumps to sealed-version amendments" +status = "accepted" +date = "2026-07-20" +refs = [ + "ADR-0016", + "ADR-0051", + "RFC-0000:C-STATUS-LIFECYCLE", + "RFC-0000:C-PHASE-LIFECYCLE", + "RFC-0000:C-CLAUSE-DEF", + "RFC-0001:C-RFC-PHASE", + "RFC-0002:C-CRUD-VERBS", + "RFC-0002:C-LIFECYCLE-VERBS", +] + +[content] +context = """ +Before this decision, the RFC lifecycle treated `spec` as an open authoring candidate and assigned new Clauses to that current version, while version-changing bump validation still permitted another bump from `spec` when candidate content differed from the stored signature. The abandoned candidate then remained in changelog history, and Clauses first created there retained its version in `since` even if no implementation baseline was ever sealed. + +The pre-amendment deletion rules also prohibited Clause deletion from every normative RFC regardless of phase. A Clause created accidentally in an open `spec` candidate therefore could not be removed, which encouraged artificial deprecation or supersession records solely to correct authoring mistakes. + +The lifecycle needed one unambiguous version boundary without making `since` directly editable or adding persistent candidate-version state.""" +decision = """ +We will refine [[ADR-0016]] and [[ADR-0051]] by treating `spec` as the single open authoring candidate for the RFC's current version. Normal version-changing bumps open a new lifecycle only from a version that has already entered `impl`, `test`, or `stable` and has a sealed amendment signature. Entry from `spec` to `impl` establishes that signature; a missing signature in a later phase is rejected as an untrustworthy baseline rather than inferred by bumping. + +A Clause first introduced in the open current candidate may be removed before sealing when no other artifact references it. Clauses inherited from an earlier version continue to use deprecation or supersession so established history remains intact. Direct `since` editing and automatic rewriting across ordinary bumps remain outside the model. + +This option was selected because: + +1. The existing `phase`, RFC `version`, and Clause `since` fields identify the open candidate without another persistent state model. +2. One bump boundary per version prevents unsealed intermediate candidates from appearing as compatibility history. +3. Candidate-only deletion corrects authoring mistakes without weakening the history of Clauses inherited from sealed versions. + +If changing an open candidate's semantic-version level becomes a recurring need, it will be considered separately rather than overloading bump semantics now.""" +consequences = """ +### Positive + +- Each version has one authoring interval and one implementation baseline. +- Clause `since` values cannot become stranded on an abandoned intermediate candidate through normal lifecycle commands. +- Accidental current-candidate Clauses can be removed without creating false deprecation or supersession history. +- Existing status, phase, version, signature, changelog, and Clause metadata remain sufficient. + +### Negative + +- A user cannot issue another version-changing bump while an RFC is already in `spec`. Candidate scope must remain within the selected compatibility level; incompatible additions can be deferred to the next version. +- A semantic-version level chosen too early cannot be changed through ordinary lifecycle commands. An unpublished candidate can be corrected in version control before it becomes shared history. +- Clause deletion must distinguish candidate-only Clauses from inherited Clauses. The existing `phase == spec` and `since == current version` condition provides that boundary and requires focused regression coverage. +- A normative RFC missing its sealed signature after `spec` cannot bump or advance. The repository must restore that baseline through its migration or version-control history rather than guessing it from possibly amended content. + +### Neutral + +- Changelog-only correction remains independent of the version lifecycle. +- `since` remains lifecycle-owned and unavailable through generic editing.""" + +[[content.alternatives]] +text = "Keep spec-phase rebumping and record abandoned candidates as historical versions" +status = "rejected" +pros = ["Requires no command or storage changes"] +cons = [ + "Leaves unsealed candidates indistinguishable from implemented version history", + "Encourages artificial Clause supersession to repair authoring attribution", +] +rejection_reason = "Unsealed candidates are authoring state, not useful compatibility history, and synthetic supersession records misrepresent requirement evolution" + +[[content.alternatives]] +text = "Treat spec as one open candidate and permit removal of Clauses introduced only in that candidate" +status = "accepted" +pros = [ + "Uses the existing phase and since fields", + "Keeps one bump boundary per version lifecycle", +] +cons = ["Version level cannot be changed in place after the candidate is opened"] + +[[content.alternatives]] +text = "Add explicit candidate retargeting or next-version state" +status = "rejected" +pros = ["Allows an open candidate version level to change without abandoned versions"] +cons = [ + "Adds lifecycle commands and metadata for an uncommon correction", + "Weakens the simple meaning of lifecycle-owned version fields", +] +rejection_reason = "The additional state and command surface is disproportionate when candidate scope can be completed before selecting the next version" diff --git a/gov/rfc/RFC-0000/clauses/C-CLAUSE-DEF.toml b/gov/rfc/RFC-0000/clauses/C-CLAUSE-DEF.toml index 45722fc0..e3318275 100644 --- a/gov/rfc/RFC-0000/clauses/C-CLAUSE-DEF.toml +++ b/gov/rfc/RFC-0000/clauses/C-CLAUSE-DEF.toml @@ -30,7 +30,7 @@ Clause kinds: Deprecation is a lifecycle state (in `status`), not a document kind. -The `since` field identifies the first RFC version that contains the clause. A clause whose `since` field is absent is a pending clause. +The `since` field identifies the first RFC version that contains the clause. A clause whose `since` field is absent is a pending clause. `since` is lifecycle-owned: generic editing MUST NOT set, change, or remove it. Finalization and version bumps MUST assign `since` only to pending clauses and MUST preserve every non-pending clause's existing `since` value. Clause creation and version assignment MUST follow these rules: - A new clause in a draft RFC MUST be pending. @@ -40,7 +40,9 @@ Clause creation and version assignment MUST follow these rules: - A version bump of a normative RFC MUST assign the new RFC version to every pending clause. - Creating a clause in a deprecated RFC MUST be rejected. +Permanent clause deletion MUST be allowed when the containing RFC is draft. Permanent clause deletion MUST also be allowed when the containing RFC is normative, its phase is `spec`, and the clause `since` equals the RFC current version. Permanent deletion MUST be rejected for every other RFC status, phase, or clause-version combination. Permanent deletion MUST reject a clause referenced by any other artifact and MUST report the referencing artifact IDs. + Advancing a normative RFC from `spec` to `impl` MUST reject every pending clause. That transition MUST NOT assign clause version metadata. That transition MUST NOT rewrite existing clause version metadata. **Rationale:** -A clause can record its version immediately only after that target version is known. Initial draft clauses receive a version only at finalization, amendments created outside `spec` receive a version at the next normative RFC bump, and clauses added to an already-open normative `spec` version can safely use the current version. This preserves version provenance without making phase advancement own clause metadata.""" +A clause can record its version immediately only after that target version is known. Initial draft clauses receive a version only at finalization, amendments created outside `spec` receive a version at the next normative RFC bump, and clauses added to an already-open normative `spec` version can safely use the current version. This preserves version provenance without making phase advancement own clause metadata. Lifecycle-only assignment and preservation make `since == current version` reliable evidence that a Clause belongs only to the open candidate, while the reference gate prevents dangling governance links.""" diff --git a/gov/rfc/RFC-0000/clauses/C-PHASE-LIFECYCLE.toml b/gov/rfc/RFC-0000/clauses/C-PHASE-LIFECYCLE.toml index 50df0e94..c6bc8556 100644 --- a/gov/rfc/RFC-0000/clauses/C-PHASE-LIFECYCLE.toml +++ b/gov/rfc/RFC-0000/clauses/C-PHASE-LIFECYCLE.toml @@ -30,20 +30,22 @@ Phase rules: - Phase transitions MUST NOT skip a phase. - Each phase transition MUST satisfy the preceding phase gate. - `stable` MUST be terminal for one RFC version. -- A later version bump MAY start another phase lifecycle for the RFC. +- A version-changing bump MUST be rejected while the current version remains in `spec`. +- A later version-changing bump MAY start another phase lifecycle only when the current version is in `impl`, `test`, or `stable`. - A version bump that releases a detected content amendment MUST start the new version at `spec`. - Only a normative RFC MAY advance from `spec` to `impl`. - While phase is `spec`, RFC and clause content belongs to the current version candidate and MAY change without another version bump. -- The `spec` -> `impl` transition MUST replace the stored amendment signature with a signature of the current RFC and clause content. +- The `spec` -> `impl` transition MUST set or replace the stored amendment signature with a signature of the current RFC and clause content. - The `spec` -> `impl` transition MUST reject pending clauses as defined by [[RFC-0000:C-CLAUSE-DEF]]. - Changelog-only updates MUST preserve phase. - Changelog-only updates MUST NOT change the amendment signature. - In `impl`, `test`, or `stable`, content that differs from the stored amendment signature is an unversioned amendment. - An unversioned amendment MUST be released by a version bump before further phase progression. +- A normative RFC in `impl`, `test`, or `stable` MUST have a stored amendment signature. A version-changing bump or later phase progression MUST reject a missing signature without mutation and MUST leave pending clauses unchanged. - A draft RFC MUST NOT enter `stable`. - A deprecated RFC MUST NOT enter `impl` or `test`. For amendment detection, RFC content is the canonical RFC and clause data other than RFC `version`, `phase`, `changelog`, and `signature` fields. Those four fields are lifecycle or changelog bookkeeping and MUST be excluded from amendment comparison. A stored signature is the sealed content baseline for the current version after it enters `impl`. While the version remains in `spec`, establishing or replacing that baseline MUST NOT be treated as a content amendment. While the version remains in `spec`, establishing or replacing that baseline MUST NOT require another version bump. **Rationale:** -Scoping phase to one version lets later amendments reuse the existing ordered gates. The `spec` phase is the mutable authoring boundary for that version, and entry into `impl` seals the exact contract that implementation follows. Defining the comparison surface prevents phase progression and changelog bookkeeping from recursively appearing as new content amendments.""" +Scoping phase to one version lets later amendments reuse the existing ordered gates. The `spec` phase is the mutable authoring boundary for that version, and entry into `impl` seals the exact contract that implementation follows. Defining the comparison surface prevents phase progression and changelog bookkeeping from recursively appearing as new content amendments. Rejecting another version-changing bump from `spec` prevents an unsealed candidate from becoming compatibility history. A missing signature outside `spec` cannot establish a trustworthy amendment baseline through a version change; it requires migration or restoration of the sealed baseline.""" diff --git a/gov/rfc/RFC-0000/rfc.toml b/gov/rfc/RFC-0000/rfc.toml index a4bd16b2..32dd8323 100644 --- a/gov/rfc/RFC-0000/rfc.toml +++ b/gov/rfc/RFC-0000/rfc.toml @@ -3,19 +3,19 @@ [govctl] id = "RFC-0000" title = "govctl Governance Framework" -version = "1.6.1" +version = "1.7.0" status = "normative" phase = "stable" owners = ["@govctl-org"] created = "2026-01-17" -updated = "2026-07-16" +updated = "2026-07-20" tags = [ "core", "schema", "validation", "lifecycle", ] -signature = "8225ff5173cd4af351f8ff01bff00baa3bda2a239b0c41f76799c68b1f210048" +signature = "1bdf74804a9478ae32ca5df2367944699dae3aa11dc1e2ec7d8c8a6a9838ff63" [[sections]] title = "Summary" @@ -53,6 +53,15 @@ clauses = ["clauses/C-RELEASE-DEF.toml"] title = "Verification Guard Specification" clauses = ["clauses/C-GUARD-DEF.toml"] +[[changelog]] +version = "1.7.0" +date = "2026-07-20" +notes = "Define single-candidate RFC version boundaries" +changed = [ + "Restrict version-changing bumps to sealed RFC versions", + "Allow deletion of unreferenced Clauses introduced in the current spec candidate", +] + [[changelog]] version = "1.6.1" date = "2026-07-16" diff --git a/gov/rfc/RFC-0001/clauses/C-RFC-PHASE.toml b/gov/rfc/RFC-0001/clauses/C-RFC-PHASE.toml index e1f92758..f1fce190 100644 --- a/gov/rfc/RFC-0001/clauses/C-RFC-PHASE.toml +++ b/gov/rfc/RFC-0001/clauses/C-RFC-PHASE.toml @@ -27,11 +27,11 @@ Within one RFC version, the following transitions MUST be rejected: - Any skip (for example, spec -> test or spec -> stable) - stable -> any (`stable` is terminal for that version) -A version bump that releases a detected RFC or clause content amendment MUST start the new RFC version in `spec`, regardless of the prior version's phase. This starts a new version lifecycle and is not a backward transition within one version. +A version bump that releases a detected RFC or clause content amendment MUST start the new RFC version in `spec`, regardless of the prior version's phase. This starts a new version lifecycle and is not a backward transition within one version. Version-changing bump eligibility MUST follow [[RFC-0000:C-PHASE-LIFECYCLE]]. Only a normative RFC MAY advance from `spec` to `impl`. A normative RFC's current `spec` content is an authoring candidate rather than an implementation conformance baseline. The most recently sealed version remains the implementation baseline until the current candidate enters `impl`. -RFC and clause content MAY change while the current version remains in `spec`. Such changes belong to the current version. Such changes MUST NOT require another version bump. Advancing from `spec` to `impl` MUST seal the current amendment content as the version's stored signature. The transition MUST reject every pending clause defined by [[RFC-0000:C-CLAUSE-DEF]]. +RFC and clause content MAY change while the current version remains in `spec`. Such changes belong to the current version. Such changes MUST NOT require another version bump. Advancing from `spec` to `impl` MUST set or replace the current version's stored signature with the current amendment content. The transition MUST reject every pending clause defined by [[RFC-0000:C-CLAUSE-DEF]]. After the current version enters `impl`, amendment content that differs from the stored signature MUST be treated as an unversioned amendment. Phase progression MUST reject that amendment until a version bump releases it and starts the next version in `spec`. diff --git a/gov/rfc/RFC-0001/rfc.toml b/gov/rfc/RFC-0001/rfc.toml index 83dd1a44..451f6c05 100644 --- a/gov/rfc/RFC-0001/rfc.toml +++ b/gov/rfc/RFC-0001/rfc.toml @@ -3,17 +3,17 @@ [govctl] id = "RFC-0001" title = "Lifecycle State Machines" -version = "0.6.0" +version = "0.7.0" status = "normative" phase = "stable" owners = ["@govctl-org"] created = "2026-01-17" -updated = "2026-07-16" +updated = "2026-07-20" tags = [ "core", "lifecycle", ] -signature = "bb15b23a30af637d2a939a84afba34636b7af115de50afcc658ff19f70c0e44a" +signature = "e67a234786417d9c81abf240a46875efdc6e0852c4be3c4ef5e914223450646a" [[sections]] title = "Summary" @@ -30,6 +30,12 @@ clauses = [ "clauses/C-GATE-CONDITIONS.toml", ] +[[changelog]] +version = "0.7.0" +date = "2026-07-20" +notes = "Clarify RFC candidate phase transitions" +changed = ["Bind version bumps and signature sealing to explicit phase boundaries"] + [[changelog]] version = "0.6.0" date = "2026-07-16" diff --git a/gov/rfc/RFC-0002/clauses/C-CRUD-VERBS.toml b/gov/rfc/RFC-0002/clauses/C-CRUD-VERBS.toml index c57f46dc..cd10cfe8 100644 --- a/gov/rfc/RFC-0002/clauses/C-CRUD-VERBS.toml +++ b/gov/rfc/RFC-0002/clauses/C-CRUD-VERBS.toml @@ -94,9 +94,8 @@ For guards: - MUST error with list of referencing artifacts/config if any references exist For clauses: -- MUST only allow deletion if containing RFC is in `draft` status -- MUST verify no other artifacts reference the clause ID before deletion -- MUST error with list of referencing artifacts if any references exist +- MUST enforce the deletion eligibility and referential-integrity requirements in [[RFC-0000:C-CLAUSE-DEF]] +- MUST error with the list of referencing artifact IDs when references prevent deletion For Work Items: - MUST only allow deletion if status is `queue` diff --git a/gov/rfc/RFC-0002/clauses/C-LIFECYCLE-VERBS.toml b/gov/rfc/RFC-0002/clauses/C-LIFECYCLE-VERBS.toml index b1e1b968..b84a28f2 100644 --- a/gov/rfc/RFC-0002/clauses/C-LIFECYCLE-VERBS.toml +++ b/gov/rfc/RFC-0002/clauses/C-LIFECYCLE-VERBS.toml @@ -30,12 +30,14 @@ Resource-specific lifecycle and constrained-mutation operations implement state - A `spec` -> `impl` transition MUST require normative RFC status. - A `spec` -> `impl` transition MUST reject every pending clause. - A `spec` -> `impl` transition MUST NOT assign clause version metadata. - - A `spec` -> `impl` transition MUST replace the stored amendment signature with a signature of the current amendment content. + - A `spec` -> `impl` transition MUST set or replace the stored amendment signature with a signature of the current amendment content. - A later phase transition MUST reject amendment content that differs from the stored signature. 4. `govctl rfc bump -m ` - The version-changing form MUST require normative RFC status. - - Rejection for draft or deprecated RFC status MUST occur without mutation. + - The version-changing form MUST enforce the source-phase and stored-signature eligibility in [[RFC-0000:C-PHASE-LIFECYCLE]]. + - Rejection for draft or deprecated RFC status, phase `spec`, or a missing sealed signature MUST occur without mutation. + - Missing-signature rejection outside `spec` MUST leave pending clauses unchanged and MUST instruct the user to migrate the repository or restore its sealed baseline. - Version bumping MUST follow semantic versioning. - A version bump MUST update the changelog automatically. - A version bump MAY include `--change ` for additional changelog entries. @@ -43,10 +45,8 @@ Resource-specific lifecycle and constrained-mutation operations implement state - Amendment comparison MUST exclude RFC `version`, `phase`, `changelog`, and `signature` fields. - A bump that releases detected RFC or clause content MUST set phase to `spec`. - A bump MUST assign its new version to pending clauses as defined by [[RFC-0000:C-CLAUSE-DEF]]. - - A bump MAY establish a baseline amendment signature for an RFC that does not yet have one. - - Establishing a baseline without a detected content amendment MUST preserve phase. - `--change` without a bump level MUST remain a changelog-only operation. - - The normative-status requirement for version-changing bumps MUST NOT apply to `--change` without a bump level. + - The normative-status and source-phase requirements for version-changing bumps MUST NOT apply to `--change` without a bump level. - `--change` without a bump level MUST resolve the unique current changelog entry by matching the entry version to the RFC version. - `--change` without a bump level MUST reject zero or multiple current entries without mutation. - `--change` without a bump level MUST preserve the RFC version. diff --git a/gov/rfc/RFC-0002/rfc.toml b/gov/rfc/RFC-0002/rfc.toml index 92ad27b6..925314f0 100644 --- a/gov/rfc/RFC-0002/rfc.toml +++ b/gov/rfc/RFC-0002/rfc.toml @@ -3,12 +3,12 @@ [govctl] id = "RFC-0002" title = "CLI Resource Model and Command Architecture" -version = "0.13.2" +version = "0.14.0" status = "normative" phase = "stable" owners = ["@govctl-org"] created = "2026-01-19" -updated = "2026-07-16" +updated = "2026-07-20" tags = [ "cli", "editing", @@ -16,7 +16,7 @@ tags = [ "validation", "release", ] -signature = "a0a0d520c414eb9a74584ca0d5ca954a2928e4330316b35771151641c7fe98c0" +signature = "d76a4b3a12eaa15379eba7ca63c3ccb532d15528ca6d3a39dbf06cc14700d02f" [[sections]] title = "Summary" @@ -36,6 +36,15 @@ clauses = [ "clauses/C-SEARCH-COMMAND.toml", ] +[[changelog]] +version = "0.14.0" +date = "2026-07-20" +notes = "Align lifecycle commands with sealed RFC versions" +changed = [ + "Enforce candidate bump and signature baseline rules", + "Permit deletion of unreferenced current-candidate Clauses", +] + [[changelog]] version = "0.13.2" date = "2026-07-16" diff --git a/gov/work/2026-07-20-close-rfc-candidate-version-lifecycle-gaps.toml b/gov/work/2026-07-20-close-rfc-candidate-version-lifecycle-gaps.toml new file mode 100644 index 00000000..2e96f1fa --- /dev/null +++ b/gov/work/2026-07-20-close-rfc-candidate-version-lifecycle-gaps.toml @@ -0,0 +1,53 @@ +#:schema ../schema/work.schema.json + +[govctl] +id = "WI-2026-07-20-001" +title = "Close RFC candidate-version lifecycle gaps" +status = "done" +created = "2026-07-20" +started = "2026-07-20" +completed = "2026-07-20" +refs = [ + "ADR-0054", + "RFC-0000", + "RFC-0001", + "RFC-0002", + "RFC-0000:C-PHASE-LIFECYCLE", + "RFC-0000:C-CLAUSE-DEF", + "RFC-0001:C-RFC-PHASE", + "RFC-0002:C-CRUD-VERBS", + "RFC-0002:C-LIFECYCLE-VERBS", +] + +[content] +description = "Align `RFC` candidate authoring with one version boundary and add safe correction for newly introduced candidate `Clause` artifacts, preventing stranded candidate-version metadata and synthetic supersession records while updating implementation, tests, and agent-facing guidance." + +[[content.acceptance_criteria]] +text = "Lifecycle RFC clauses define `spec` as one open candidate and the deletion boundary for unreferenced Clauses introduced in the current version, including inherited-Clause rejection" +status = "done" +category = "changed" + +[[content.acceptance_criteria]] +text = "Version-changing rfc bump rejects spec-phase RFCs and later-phase RFCs without a sealed signature without mutation; later phase progression likewise rejects a missing signature" +status = "done" +category = "changed" + +[[content.acceptance_criteria]] +text = "Normative `spec` RFCs allow deletion only for unreferenced Clauses whose `since` equals the current RFC version" +status = "done" +category = "changed" + +[[content.acceptance_criteria]] +text = "AGENTS.md, `gov`, `spec`, and `rfc-writer` skills, CLI help, and `docs/guide/rfcs.md` describe the corrected lifecycle" +status = "done" +category = "chore" + +[[content.acceptance_criteria]] +text = "Focused lifecycle tests, `cargo test`, `govctl check`, and `just pre-commit` pass" +status = "done" +category = "chore" + +[[content.acceptance_criteria]] +text = "[[ADR-0054]] is accepted and [[RFC-0000]], [[RFC-0001]], and [[RFC-0002]] complete their spec, impl, test, and stable gates" +status = "done" +category = "chore" diff --git a/src/cli/resources/clause.rs b/src/cli/resources/clause.rs index 8b4db2b3..41126a65 100644 --- a/src/cli/resources/clause.rs +++ b/src/cli/resources/clause.rs @@ -143,6 +143,11 @@ Use dedicated verbs instead of `set` for: EXAMPLES: govctl clause delete RFC-0001:C-SCOPE govctl clause delete RFC-0001:C-SCOPE --force + +NOTES: + - Draft Clauses may be deleted when unreferenced. + - A normative spec Clause may be deleted only when its since version equals the RFC's current version. + - Inherited or sealed Clauses must be deprecated or superseded instead. ")] Delete(CommonDeleteArgs), /// Deprecate clause diff --git a/src/cli/resources/rfc.rs b/src/cli/resources/rfc.rs index 759fb4fb..f68c7d49 100644 --- a/src/cli/resources/rfc.rs +++ b/src/cli/resources/rfc.rs @@ -131,7 +131,8 @@ EXAMPLES: govctl rfc bump RFC-0001 -c \"fix: Correct current-version wording\" NOTES: - - Version-changing bumps require normative RFC status; finalize a draft first. + - Version-changing bumps require a normative RFC in impl, test, or stable with a sealed signature. + - While an RFC is in spec, continue authoring the current version candidate instead of bumping again. - Choose one of `--patch`, `--minor`, or `--major` when releasing a content amendment. - `--change` without a bump level updates the current changelog entry without changing version. - Use `-m/--summary` for a release summary and `-c/--change` for detailed entries. diff --git a/src/cmd/edit/delete.rs b/src/cmd/edit/delete.rs index 3c2d7ddf..36b63fd6 100644 --- a/src/cmd/edit/delete.rs +++ b/src/cmd/edit/delete.rs @@ -1,12 +1,12 @@ -use super::adapter::{DocAdapter, RfcTomlAdapter, TomlAdapter, WorkTomlAdapter}; +use super::adapter::{ClauseTomlAdapter, DocAdapter, RfcTomlAdapter, TomlAdapter, WorkTomlAdapter}; use super::delete_referrers::{clause_deletion_referrers, work_item_deletion_referrers}; use crate::cmd::confirmation::confirm_destructive_action; use crate::config::Config; use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticResult}; use crate::load::split_clause_id; -use crate::model::RfcStatus; +use crate::model::{RfcPhase, RfcStatus}; use crate::ui; -use crate::write::{WriteOp, delete_file}; +use crate::write::{WriteOp, delete_file, with_file_transaction}; use std::path::Path; pub fn delete_clause( @@ -24,25 +24,28 @@ pub fn delete_clause( })?; let mut rfc_loaded = RfcTomlAdapter::load(config, rfc_id)?; - if rfc_loaded.data.status != RfcStatus::Draft { + let clause_loaded = ClauseTomlAdapter::load(config, clause_id)?; + // [[RFC-0000:C-CLAUSE-DEF]] limits normative deletion to the open candidate. + let is_current_candidate_clause = rfc_loaded.data.status == RfcStatus::Normative + && rfc_loaded.data.phase == RfcPhase::Spec + && clause_loaded.data.since.as_deref() == Some(rfc_loaded.data.version.as_str()); + if rfc_loaded.data.status != RfcStatus::Draft && !is_current_candidate_clause { + let since = clause_loaded.data.since.as_deref().unwrap_or("pending"); return Err(Diagnostic::new( - DiagnosticCode::E0110RfcInvalidId, + DiagnosticCode::E0104RfcInvalidTransition, format!( - "Cannot delete clause: {} is {}. Only draft RFCs allow clause deletion.", + "Cannot delete clause from {} while status={}, phase={}, version={}, and clause since={}. Clause deletion is limited to draft RFCs or Clauses introduced in the current normative spec candidate.", rfc_id, - rfc_loaded.data.status.as_ref() + rfc_loaded.data.status.as_ref(), + rfc_loaded.data.phase.as_ref(), + rfc_loaded.data.version, + since, ), clause_id, )); } - let clause_path = crate::load::find_clause_toml(config, clause_id).ok_or_else(|| { - Diagnostic::new( - DiagnosticCode::E0202ClauseNotFound, - format!("Clause not found: {}", clause_id), - clause_id, - ) - })?; + let clause_path = clause_loaded.path; let clause_file_name = clause_path .file_name() @@ -79,15 +82,20 @@ pub fn delete_clause( )); } - crate::write::write_rfc( - &rfc_loaded.path, - &rfc_loaded.data, + with_file_transaction( + &[rfc_loaded.path.as_path(), clause_path.as_path()], op, - Some(&config.display_path(&rfc_loaded.path)), + || { + crate::write::write_rfc( + &rfc_loaded.path, + &rfc_loaded.data, + op, + Some(&config.display_path(&rfc_loaded.path)), + )?; + delete_file(&clause_path, op, Some(&config.display_path(&clause_path))) + }, )?; - delete_file(&clause_path, op, Some(&config.display_path(&clause_path)))?; - if !op.is_preview() { ui::success(format!("Deleted clause {}", clause_id)); } diff --git a/src/cmd/edit/delete_referrers.rs b/src/cmd/edit/delete_referrers.rs index db005fc1..f8faeb69 100644 --- a/src/cmd/edit/delete_referrers.rs +++ b/src/cmd/edit/delete_referrers.rs @@ -1,6 +1,7 @@ use crate::config::Config; -use crate::diagnostic::DiagnosticResult; +use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticResult}; use crate::model::ProjectIndex; +use regex::Regex; pub(super) fn clause_deletion_referrers( config: &Config, @@ -8,12 +9,117 @@ pub(super) fn clause_deletion_referrers( clause_id: &str, ) -> DiagnosticResult> { let mut referrers = project_referrers(index, clause_id, None, false); + referrers.extend(clause_supersession_referrers(index, clause_id)); + let inline_re = Regex::new(&config.source_scan.pattern).map_err(|err| { + Diagnostic::new( + DiagnosticCode::E0501ConfigInvalid, + format!("Invalid source_scan.pattern regex: {err}"), + config + .display_path(&config.gov_root.join("config.toml")) + .display() + .to_string(), + ) + })?; + referrers.extend(inline_clause_referrers(index, &inline_re, clause_id)); referrers.extend(guard_referrers(config, clause_id)?); referrers.sort(); referrers.dedup(); Ok(referrers) } +fn clause_supersession_referrers(index: &ProjectIndex, target_id: &str) -> Vec { + index + .rfcs + .iter() + .flat_map(|rfc| { + rfc.clauses.iter().filter_map(move |clause| { + let replacement = clause.spec.superseded_by.as_deref()?; + let qualified = if replacement.contains(':') { + replacement.to_string() + } else { + format!("{}:{replacement}", rfc.rfc.rfc_id) + }; + (qualified == target_id) + .then(|| format!("{}:{}", rfc.rfc.rfc_id, clause.spec.clause_id)) + }) + }) + .collect() +} + +fn inline_clause_referrers( + index: &ProjectIndex, + inline_re: &Regex, + target_id: &str, +) -> Vec { + let mut referrers = Vec::new(); + + for rfc in &index.rfcs { + for clause in &rfc.clauses { + let source_id = format!("{}:{}", rfc.rfc.rfc_id, clause.spec.clause_id); + if source_id != target_id && text_references(inline_re, &clause.spec.text, target_id) { + referrers.push(source_id); + } + } + if rfc.rfc.changelog.iter().any(|entry| { + entry + .notes + .as_deref() + .is_some_and(|text| text_references(inline_re, text, target_id)) + || [ + &entry.added, + &entry.changed, + &entry.deprecated, + &entry.removed, + &entry.fixed, + &entry.security, + ] + .into_iter() + .flatten() + .any(|text| text_references(inline_re, text, target_id)) + }) { + referrers.push(rfc.rfc.rfc_id.clone()); + } + } + + for adr in &index.adrs { + let content = &adr.spec.content; + let direct_content = [&content.context, &content.decision, &content.consequences]; + let alternative_content = content.alternatives.iter().flat_map(|alternative| { + std::iter::once(&alternative.text) + .chain(alternative.pros.iter()) + .chain(alternative.cons.iter()) + .chain(alternative.rejection_reason.iter()) + }); + if direct_content + .into_iter() + .chain(alternative_content) + .any(|text| text_references(inline_re, text, target_id)) + { + referrers.push(adr.spec.govctl.id.clone()); + } + } + + for work in &index.work_items { + let content = &work.spec.content; + let mut direct_content = std::iter::once(&content.description) + .chain(content.acceptance_criteria.iter().map(|item| &item.text)) + .chain(content.notes.iter()) + .chain(content.journal.iter().map(|entry| &entry.content)); + if direct_content.any(|text| text_references(inline_re, text, target_id)) { + referrers.push(work.spec.govctl.id.clone()); + } + } + + referrers +} + +fn text_references(inline_re: &Regex, text: &str, target_id: &str) -> bool { + inline_re + .captures_iter(text) + .filter_map(|captures| captures.get(1)) + .any(|target| target.as_str() == target_id) +} + pub(super) fn work_item_deletion_referrers(index: &ProjectIndex, work_id: &str) -> Vec { project_referrers(index, work_id, Some(work_id), true) } diff --git a/src/cmd/lifecycle/rfc.rs b/src/cmd/lifecycle/rfc.rs index d60e7684..cd90bd41 100644 --- a/src/cmd/lifecycle/rfc.rs +++ b/src/cmd/lifecycle/rfc.rs @@ -27,7 +27,7 @@ pub fn bump( let rfc_path = require_rfc_toml_path(config, rfc_id)?; let mut rfc = read_rfc(config, &rfc_path)?; - let refresh_signature_after_write = match (level, summary, changes.is_empty()) { + match (level, summary, changes.is_empty()) { (Some(lvl), Some(sum), _) => { // [[RFC-0002:C-LIFECYCLE-VERBS]] reserves version-changing bumps for // RFC lineages that have crossed the normative publication boundary. @@ -42,14 +42,25 @@ pub fn bump( )); } - let releases_content_amendment = - ensure_rfc_has_content_amendment(config, &rfc_path, rfc_id)?; + // [[RFC-0000:C-PHASE-LIFECYCLE]] permits a version-changing bump only + // after the current version has been sealed with a stored signature. + if rfc.phase == RfcPhase::Spec { + return Err(Diagnostic::new( + DiagnosticCode::E0104RfcInvalidTransition, + "Cannot bump RFC version while phase=spec. Continue authoring the current version candidate, then advance it to impl before opening another version.", + rfc_id, + )); + } - let new_version = bump_rfc_version(&mut rfc, lvl, sum)?; - if releases_content_amendment { - rfc.phase = RfcPhase::Spec; + if rfc.signature.is_none() { + return Err(missing_sealed_signature(rfc_id, "bump RFC version")); } + ensure_rfc_has_content_amendment(config, &rfc_path, rfc_id)?; + + let new_version = bump_rfc_version(&mut rfc, lvl, sum)?; + rfc.phase = RfcPhase::Spec; + for change in changes { add_changelog_change(&mut rfc, change)?; } @@ -60,7 +71,6 @@ pub fn bump( write_lifecycle_rfc(config, &rfc_path, &rfc, op)?; let updated_clause_ids = fill_pending_clause_versions(config, &rfc_path, &new_version, op)?; - refresh_rfc_signature_best_effort(config, &rfc_path, &mut rfc, op)?; Ok(updated_clause_ids) })?; @@ -95,7 +105,6 @@ pub fn bump( for change in changes { add_changelog_change(&mut rfc, change)?; } - false } (None, None, true) => { return Err(Diagnostic::new( @@ -104,14 +113,10 @@ pub fn bump( rfc_id, )); } - }; + } with_file_transaction(&[rfc_path.as_path()], op, || { - write_lifecycle_rfc(config, &rfc_path, &rfc, op)?; - if refresh_signature_after_write { - refresh_rfc_signature_best_effort(config, &rfc_path, &mut rfc, op)?; - } - Ok(()) + write_lifecycle_rfc(config, &rfc_path, &rfc, op) })?; if !op.is_preview() { for change in changes { @@ -255,17 +260,32 @@ pub fn advance( } } + // [[RFC-0000:C-PHASE-LIFECYCLE]] requires every post-spec phase to retain + // the sealed signature established by the spec -> impl transition. + if !seals_current_version && rfc.signature.is_none() { + return Err(missing_sealed_signature(rfc_id, "advance RFC phase")); + } + let rfc_index = crate::load::load_rfc(config, &rfc_path)?; let current_signature = crate::signature::compute_rfc_content_signature(&rfc_index)?; let next_signature = if seals_current_version { Some(current_signature) - } else if let Some(stored_signature) = &rfc_index.rfc.signature { + } else { + let stored_signature = rfc_index + .rfc + .signature + .as_ref() + .ok_or_else(|| missing_sealed_signature(rfc_id, "advance RFC phase"))?; if stored_signature == ¤t_signature { None } else if crate::signature::compute_rfc_signature(&rfc_index) .is_ok_and(|legacy| stored_signature == &legacy) { - Some(current_signature) + return Err(Diagnostic::new( + DiagnosticCode::E0505MigrationRequired, + "Cannot advance phase while the RFC has a legacy amendment signature. Run `govctl migrate` before advancing.", + rfc_id, + )); } else { return Err(Diagnostic::new( DiagnosticCode::E0114RfcPendingAmendment, @@ -273,8 +293,6 @@ pub fn advance( rfc_id, )); } - } else { - None }; let mut updated_rfc = rfc; @@ -333,36 +351,21 @@ fn write_lifecycle_rfc( crate::write::write_rfc(rfc_path, rfc, op, Some(&config.display_path(rfc_path))) } -fn refresh_rfc_signature_best_effort( - config: &Config, - rfc_path: &Path, - rfc: &mut RfcSpec, - op: WriteOp, -) -> DiagnosticResult<()> { - if let Ok(rfc_index) = crate::load::load_rfc(config, rfc_path) - && let Ok(sig) = crate::signature::compute_rfc_content_signature(&rfc_index) - { - rfc.signature = Some(sig); - write_lifecycle_rfc(config, rfc_path, rfc, op)?; - } - Ok(()) -} - fn ensure_rfc_has_content_amendment( config: &Config, rfc_path: &Path, rfc_id: &str, -) -> DiagnosticResult { +) -> DiagnosticResult<()> { if !pending_clause_ids(config, rfc_path)?.is_empty() { - return Ok(true); + return Ok(()); } let rfc_index = crate::load::load_rfc(config, rfc_path)?; if rfc_index.rfc.signature.is_none() { - return Ok(false); + return Err(missing_sealed_signature(rfc_id, "bump RFC version")); } if crate::signature::is_rfc_amended(&rfc_index) { - return Ok(true); + return Ok(()); } Err(Diagnostic::new( @@ -372,6 +375,16 @@ fn ensure_rfc_has_content_amendment( )) } +fn missing_sealed_signature(rfc_id: &str, action: &str) -> Diagnostic { + Diagnostic::new( + DiagnosticCode::E0505MigrationRequired, + format!( + "Cannot {action} without a sealed RFC content signature. Run `govctl migrate` or restore the sealed signature baseline from version-control history." + ), + rfc_id, + ) +} + pub(crate) fn require_changelog_update_ready( config: &Config, rfc_path: &Path, diff --git a/src/cmd/new/artifacts/rfc.rs b/src/cmd/new/artifacts/rfc.rs index aee7edd3..cb80d3f9 100644 --- a/src/cmd/new/artifacts/rfc.rs +++ b/src/cmd/new/artifacts/rfc.rs @@ -98,7 +98,7 @@ pub(super) fn create( fixed: vec![], security: vec![], }], - signature: None, // Will be set on first bump per [[ADR-0016]] + signature: None, // Sealed when the RFC first advances from spec to impl. }; let rfc_toml = config.rfc_source_path(&rfc_id, "toml"); diff --git a/src/model/rfc.rs b/src/model/rfc.rs index 6adf5b18..a1313de6 100644 --- a/src/model/rfc.rs +++ b/src/model/rfc.rs @@ -24,8 +24,8 @@ pub struct RfcSpec { pub sections: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub changelog: Vec, - /// Content signature for amendment detection per [[ADR-0016]] - /// SHA-256 hash of canonical RFC content at last released version + /// Sealed content signature for amendment detection per [[ADR-0016]]. + /// Set when the current version advances from spec to impl. #[serde(default, skip_serializing_if = "Option::is_none")] pub signature: Option, } diff --git a/src/signature/mod.rs b/src/signature/mod.rs index 01fc6755..8b7a5542 100644 --- a/src/signature/mod.rs +++ b/src/signature/mod.rs @@ -194,15 +194,20 @@ fn hex_encode(bytes: &[u8]) -> String { hex } -/// Check if an RFC has been amended since its last release. +/// Check if a sealed RFC version has an unversioned content amendment. /// /// Returns `true` if the current content signature differs from the stored signature, -/// indicating the RFC has been modified but not yet bumped to a new version. +/// indicating an RFC in `impl`, `test`, or `stable` has been modified but not yet +/// bumped to a new version. Content changes in `spec` belong to the open candidate. /// /// Returns `false` if signatures match (clean state) or if no signature is stored. /// Stored signatures created before content-only signatures are accepted as a /// legacy clean baseline when the full rendered-projection signature still matches. pub fn is_rfc_amended(rfc: &RfcIndex) -> bool { + if rfc.rfc.phase == crate::model::RfcPhase::Spec { + return false; + } + let Some(stored_sig) = &rfc.rfc.signature else { return false; }; diff --git a/src/signature/tests.rs b/src/signature/tests.rs index ebee3714..3393e065 100644 --- a/src/signature/tests.rs +++ b/src/signature/tests.rs @@ -101,6 +101,17 @@ fn test_migrated_legacy_signature_ignores_later_phase_changes() -> Result<(), Di Ok(()) } +#[test] +fn test_rfc_amended_ignores_open_spec_candidate_changes() -> Result<(), Diagnostic> { + let mut rfc = test_rfc_index(); + rfc.rfc.signature = Some(compute_rfc_content_signature(&rfc)?); + rfc.rfc.phase = RfcPhase::Spec; + rfc.rfc.title = "Open candidate title".to_string(); + + assert!(!is_rfc_amended(&rfc)); + Ok(()) +} + fn test_rfc_index() -> RfcIndex { RfcIndex { rfc: RfcSpec { diff --git a/tests/delete_tests/clause.rs b/tests/delete_tests/clause.rs index d851d542..bcffaf5f 100644 --- a/tests/delete_tests/clause.rs +++ b/tests/delete_tests/clause.rs @@ -140,6 +140,75 @@ fn test_delete_clause_success_draft() -> TestResult { Ok(()) } +#[test] +fn test_delete_clause_success_current_normative_spec_candidate() -> TestResult { + let (temp_dir, date) = init_project_with_date()?; + + write_rfc_fixture( + temp_dir.path(), + "Normative spec RFC", + "1.1.0", + "normative", + "spec", + &[ + ("C-KEEP", "Keep This Clause", "This clause will remain."), + ( + "C-DELETE", + "Delete This Clause", + "This candidate Clause references [[RFC-0001:C-DELETE]] itself.", + ), + ], + r#"added = ["Current candidate"]"#, + )?; + + let output = run_commands( + temp_dir.path(), + &[ + &["clause", "delete", "RFC-0001:C-DELETE", "-f"], + &["clause", "list", "RFC-0001"], + &["check"], + ], + )?; + assert_delete_snapshot!(normalize_output(&output, temp_dir.path(), &date)?); + Ok(()) +} + +#[test] +fn test_delete_clause_rejects_inherited_normative_spec_clause() -> TestResult { + let (temp_dir, _) = init_project_with_date()?; + let rfc_dir = write_rfc_fixture( + temp_dir.path(), + "Normative spec RFC", + "1.1.0", + "normative", + "spec", + &[( + "C-INHERITED", + "Inherited Clause", + "This clause was introduced in an earlier version.", + )], + r#"added = ["Current candidate"]"#, + )?; + let clause_path = rfc_dir.join("clauses/C-INHERITED.toml"); + let mut clause: toml::Value = toml::from_str(&fs::read_to_string(&clause_path)?)?; + clause["govctl"]["since"] = toml::Value::String("1.0.0".to_string()); + fs::write(&clause_path, toml::to_string_pretty(&clause)?)?; + let rfc_before = fs::read(rfc_dir.join("rfc.toml"))?; + let clause_before = fs::read(&clause_path)?; + + let output = run_commands( + temp_dir.path(), + &[&["clause", "delete", "RFC-0001:C-INHERITED", "-f"]], + )?; + + assert!(output.contains("error[E0104]"), "output: {output}"); + assert!(output.contains("version=1.1.0"), "output: {output}"); + assert!(output.contains("clause since=1.0.0"), "output: {output}"); + assert_eq!(fs::read(rfc_dir.join("rfc.toml"))?, rfc_before); + assert_eq!(fs::read(&clause_path)?, clause_before); + Ok(()) +} + #[test] fn test_delete_clause_safeguard_referenced_by_artifacts() -> TestResult { let (temp_dir, date) = init_project_with_date()?; @@ -147,16 +216,16 @@ fn test_delete_clause_safeguard_referenced_by_artifacts() -> TestResult { let rfc_dir = write_rfc_fixture( temp_dir.path(), - "Draft RFC", + "Normative spec RFC", "0.1.0", - "draft", + "normative", "spec", &[( "C-DELETE", "Referenced Clause", "This clause is still referenced.", )], - r#"notes = "Initial draft""#, + r#"added = ["Current candidate"]"#, )?; let commands: Vec> = vec![ @@ -191,3 +260,94 @@ fn test_delete_clause_safeguard_referenced_by_artifacts() -> TestResult { Ok(()) } + +#[test] +fn test_delete_clause_reports_inline_and_supersession_referrers() -> TestResult { + let (temp_dir, date) = init_project_with_date()?; + let work_id = first_work_id(&date); + + let rfc_dir = write_rfc_fixture( + temp_dir.path(), + "Normative spec RFC", + "0.1.0", + "normative", + "spec", + &[( + "C-DELETE", + "Referenced Clause", + "This clause is still referenced.", + )], + r#"added = ["Current candidate"]"#, + )?; + + let commands: Vec> = vec![ + command(&[ + "clause", + "new", + "RFC-0001:C-OLD", + "Old Clause", + "-s", + "Specification", + "-k", + "normative", + ]), + command(&[ + "clause", + "supersede", + "RFC-0001:C-OLD", + "--by", + "C-DELETE", + "--force", + ]), + command(&["rfc", "new", "Inline RFC"]), + command(&[ + "clause", + "new", + "RFC-0002:C-INLINE", + "Inline Clause", + "-s", + "Specification", + "-k", + "normative", + ]), + command(&[ + "clause", + "edit", + "RFC-0002:C-INLINE", + "text", + "--set", + "Uses [[RFC-0001:C-DELETE]].", + ]), + command(&["adr", "new", "Inline ADR"]), + command(&[ + "adr", + "edit", + "ADR-0001", + "content.context", + "--set", + "Uses [[RFC-0001:C-DELETE]].", + ]), + work_new("Inline work item"), + command(&[ + "work", + "edit", + &work_id, + "description", + "--set", + "Uses [[RFC-0001:C-DELETE]].", + ]), + command(&["clause", "delete", "RFC-0001:C-DELETE", "-f"]), + ]; + + let output = run_dynamic_commands(temp_dir.path(), &commands)?; + assert!(output.contains("error[E0211]"), "output: {output}"); + assert!(output.contains("RFC-0001:C-OLD"), "output: {output}"); + assert!(output.contains("RFC-0002:C-INLINE"), "output: {output}"); + assert!(output.contains("ADR-0001"), "output: {output}"); + assert!(output.contains(&work_id), "output: {output}"); + assert!( + rfc_dir.join("clauses/C-DELETE.toml").exists(), + "referenced clause should remain on disk" + ); + Ok(()) +} diff --git a/tests/edit_tests/rfc.rs b/tests/edit_tests/rfc.rs index b9c5dea3..d08f5783 100644 --- a/tests/edit_tests/rfc.rs +++ b/tests/edit_tests/rfc.rs @@ -461,7 +461,8 @@ fn test_rfc_changelog_edits_reject_legacy_signature_without_false_amendment() -> 3, "output: {output}" ); - assert!(output.contains("error[E0113]"), "output: {output}"); + assert!(output.contains("error[E0104]"), "output: {output}"); + assert!(output.contains("phase=spec"), "output: {output}"); assert!( output.contains("$ govctl rfc get RFC-0001 phase\nimpl"), "output: {output}" diff --git a/tests/lifecycle_tests/rfc_cases/advance.rs b/tests/lifecycle_tests/rfc_cases/advance.rs index 1bcdf654..afa099f5 100644 --- a/tests/lifecycle_tests/rfc_cases/advance.rs +++ b/tests/lifecycle_tests/rfc_cases/advance.rs @@ -143,20 +143,16 @@ fn test_advance_seals_content_edits_made_during_spec() -> common::TestResult { "Original normative behavior.", ], &["rfc", "finalize", "RFC-0001", "normative"], - &[ - "rfc", - "bump", - "RFC-0001", - "--patch", - "--summary", - "Establish baseline", - ], ], )?; let rfc_path = temp_dir.path().join("gov/rfc/RFC-0001/rfc.toml"); let before: toml::Value = toml::from_str(&fs::read_to_string(&rfc_path)?)?; - let baseline_signature = before["govctl"]["signature"].as_str().map(str::to_string); + let baseline_signature = before["govctl"] + .get("signature") + .and_then(toml::Value::as_str) + .map(str::to_string); + assert!(baseline_signature.is_none()); let output = run_commands( temp_dir.path(), @@ -199,6 +195,56 @@ fn test_advance_seals_content_edits_made_during_spec() -> common::TestResult { Ok(()) } +#[test] +fn test_advance_after_spec_rejects_missing_signature_without_mutation() -> common::TestResult { + let temp_dir = init_project()?; + run_commands( + temp_dir.path(), + &[ + &["rfc", "new", "Missing signature RFC"], + &["rfc", "finalize", "RFC-0001", "normative"], + &["rfc", "advance", "RFC-0001", "impl"], + &[ + "clause", + "new", + "RFC-0001:C-PENDING", + "Pending Clause", + "-s", + "Specification", + "-k", + "normative", + ], + ], + )?; + + let rfc_path = temp_dir.path().join("gov/rfc/RFC-0001/rfc.toml"); + let clause_path = temp_dir + .path() + .join("gov/rfc/RFC-0001/clauses/C-PENDING.toml"); + let mut rfc: toml::Value = toml::from_str(&fs::read_to_string(&rfc_path)?)?; + rfc["govctl"] + .as_table_mut() + .ok_or("RFC metadata is not a table")? + .remove("signature"); + fs::write(&rfc_path, toml::to_string_pretty(&rfc)?)?; + let rfc_before = fs::read(&rfc_path)?; + let clause_before = fs::read(&clause_path)?; + + let output = run_commands(temp_dir.path(), &[&["rfc", "advance", "RFC-0001", "test"]])?; + + assert!(output.contains("error[E0505]"), "output: {output}"); + assert!( + output.contains("sealed RFC content signature"), + "output: {output}" + ); + assert!(output.contains("govctl migrate"), "output: {output}"); + assert_eq!(fs::read(&rfc_path)?, rfc_before); + assert_eq!(fs::read(&clause_path)?, clause_before); + let clause: toml::Value = toml::from_str(&fs::read_to_string(&clause_path)?)?; + assert!(clause["govctl"].get("since").is_none()); + Ok(()) +} + #[test] fn test_advance_after_impl_rejects_unversioned_content_amendment() -> common::TestResult { let temp_dir = init_project()?; @@ -286,7 +332,7 @@ fn test_advance_to_impl_rejects_pending_clause_versions_without_mutation() -> co } #[test] -fn test_advance_migrates_legacy_signature_before_phase_changes() -> common::TestResult { +fn test_advance_to_impl_replaces_legacy_signature() -> common::TestResult { let temp_dir = init_project()?; run_commands( temp_dir.path(), @@ -334,6 +380,46 @@ fn test_advance_migrates_legacy_signature_before_phase_changes() -> common::Test Ok(()) } +#[test] +fn test_later_phase_advance_rejects_legacy_signature_without_mutation() -> common::TestResult { + let temp_dir = init_project()?; + run_commands( + temp_dir.path(), + &[ + &["rfc", "new", "Legacy signature RFC"], + &["rfc", "finalize", "RFC-0001", "normative"], + &["rfc", "advance", "RFC-0001", "impl"], + &["rfc", "render", "RFC-0001"], + ], + )?; + + let rendered = fs::read_to_string(temp_dir.path().join("docs/rfc/RFC-0001.md"))?; + let legacy_signature = rendered + .lines() + .find_map(|line| { + line.trim() + .strip_prefix("")) + }) + .ok_or("missing rendered RFC signature")?; + let rfc_path = temp_dir.path().join("gov/rfc/RFC-0001/rfc.toml"); + let mut rfc: toml::Value = toml::from_str(&fs::read_to_string(&rfc_path)?)?; + rfc["govctl"]["signature"] = toml::Value::String(legacy_signature.to_string()); + fs::write(&rfc_path, toml::to_string_pretty(&rfc)?)?; + let before = fs::read(&rfc_path)?; + + let output = run_commands(temp_dir.path(), &[&["rfc", "advance", "RFC-0001", "test"]])?; + + assert!(output.contains("error[E0505]"), "output: {output}"); + assert!( + output.contains("legacy amendment signature"), + "output: {output}" + ); + assert!(output.contains("govctl migrate"), "output: {output}"); + assert_eq!(fs::read(&rfc_path)?, before); + Ok(()) +} + #[test] fn test_advance_rejects_deprecated_rfc_entering_impl() -> common::TestResult { let temp_dir = init_project()?; diff --git a/tests/lifecycle_tests/rfc_cases/bump.rs b/tests/lifecycle_tests/rfc_cases/bump.rs index 123975dc..7fe7dc40 100644 --- a/tests/lifecycle_tests/rfc_cases/bump.rs +++ b/tests/lifecycle_tests/rfc_cases/bump.rs @@ -13,6 +13,17 @@ fn test_bump_patch_version() -> common::TestResult { &[ &["rfc", "new", "Test RFC"], &["rfc", "finalize", "RFC-0001", "normative"], + &["rfc", "advance", "RFC-0001", "impl"], + &["rfc", "advance", "RFC-0001", "test"], + &["rfc", "advance", "RFC-0001", "stable"], + &[ + "rfc", + "edit", + "RFC-0001", + "title", + "--set", + "Test RFC patch amendment", + ], &[ "rfc", "bump", @@ -37,6 +48,17 @@ fn test_bump_minor_version() -> common::TestResult { &[ &["rfc", "new", "Test RFC"], &["rfc", "finalize", "RFC-0001", "normative"], + &["rfc", "advance", "RFC-0001", "impl"], + &["rfc", "advance", "RFC-0001", "test"], + &["rfc", "advance", "RFC-0001", "stable"], + &[ + "rfc", + "edit", + "RFC-0001", + "title", + "--set", + "Test RFC minor amendment", + ], &[ "rfc", "bump", @@ -61,6 +83,17 @@ fn test_bump_major_version() -> common::TestResult { &[ &["rfc", "new", "Test RFC"], &["rfc", "finalize", "RFC-0001", "normative"], + &["rfc", "advance", "RFC-0001", "impl"], + &["rfc", "advance", "RFC-0001", "test"], + &["rfc", "advance", "RFC-0001", "stable"], + &[ + "rfc", + "edit", + "RFC-0001", + "title", + "--set", + "Test RFC major amendment", + ], &[ "rfc", "bump", @@ -162,6 +195,66 @@ fn test_version_bump_rejects_deprecated_without_mutation() -> common::TestResult Ok(()) } +#[test] +fn test_version_bump_rejects_spec_without_mutation_but_allows_changelog_change() +-> common::TestResult { + let temp_dir = init_project()?; + run_commands( + temp_dir.path(), + &[ + &["rfc", "new", "Spec RFC"], + &["rfc", "finalize", "RFC-0001", "normative"], + &[ + "rfc", + "edit", + "RFC-0001", + "title", + "--set", + "Amended spec candidate", + ], + ], + )?; + + let rfc_path = temp_dir.path().join("gov/rfc/RFC-0001/rfc.toml"); + let before = fs::read(&rfc_path)?; + let output = run_commands( + temp_dir.path(), + &[&[ + "rfc", + "bump", + "RFC-0001", + "--minor", + "--summary", + "Must be rejected", + ]], + )?; + + assert!(output.contains("error[E0104]"), "output: {output}"); + assert!(output.contains("phase=spec"), "output: {output}"); + assert!(!output.contains("Bumped RFC-0001"), "output: {output}"); + assert_eq!(fs::read(&rfc_path)?, before); + + let output = run_commands( + temp_dir.path(), + &[ + &[ + "rfc", + "bump", + "RFC-0001", + "--change", + "fix: Correct candidate changelog", + ], + &["rfc", "get", "RFC-0001", "version"], + ], + )?; + assert!(!output.contains("error["), "output: {output}"); + assert!( + output.contains("$ govctl rfc get RFC-0001 version\n0.1.0"), + "output: {output}" + ); + Ok(()) +} + #[test] fn test_changelog_only_bump_remains_available_for_non_normative_rfc() -> common::TestResult { let temp_dir = init_project()?; @@ -227,14 +320,6 @@ fn test_content_bump_restarts_stable_rfc_at_spec() -> common::TestResult { "Original normative behavior.", ], &["rfc", "finalize", "RFC-0001", "normative"], - &[ - "rfc", - "bump", - "RFC-0001", - "--patch", - "--summary", - "Establish baseline", - ], &["rfc", "advance", "RFC-0001", "impl"], &["rfc", "advance", "RFC-0001", "test"], &["rfc", "advance", "RFC-0001", "stable"], @@ -265,10 +350,9 @@ fn test_content_bump_restarts_stable_rfc_at_spec() -> common::TestResult { } #[test] -fn test_signature_baseline_and_changelog_only_update_preserve_stable_phase() -> common::TestResult { +fn test_bump_preserves_signature_until_new_candidate_advances_to_impl() -> common::TestResult { let temp_dir = init_project()?; - - let output = run_commands( + run_commands( temp_dir.path(), &[ &["rfc", "new", "Test RFC"], @@ -276,14 +360,70 @@ fn test_signature_baseline_and_changelog_only_update_preserve_stable_phase() -> &["rfc", "advance", "RFC-0001", "impl"], &["rfc", "advance", "RFC-0001", "test"], &["rfc", "advance", "RFC-0001", "stable"], + ], + )?; + + let rfc_path = temp_dir.path().join("gov/rfc/RFC-0001/rfc.toml"); + let sealed: toml::Value = toml::from_str(&fs::read_to_string(&rfc_path)?)?; + let sealed_signature = sealed["govctl"]["signature"] + .as_str() + .ok_or("missing sealed signature")? + .to_string(); + + run_commands( + temp_dir.path(), + &[ + &[ + "rfc", + "edit", + "RFC-0001", + "title", + "--set", + "Amended Test RFC", + ], &[ "rfc", "bump", "RFC-0001", "--patch", "--summary", - "Establish baseline", + "Release amendment", ], + ], + )?; + + let candidate: toml::Value = toml::from_str(&fs::read_to_string(&rfc_path)?)?; + assert_eq!(candidate["govctl"]["phase"].as_str(), Some("spec")); + assert_eq!( + candidate["govctl"]["signature"].as_str(), + Some(sealed_signature.as_str()) + ); + + run_commands( + temp_dir.path(), + &[["rfc", "advance", "RFC-0001", "impl"].as_slice()], + )?; + let implemented: toml::Value = toml::from_str(&fs::read_to_string(&rfc_path)?)?; + assert_eq!(implemented["govctl"]["phase"].as_str(), Some("impl")); + assert_ne!( + implemented["govctl"]["signature"].as_str(), + Some(sealed_signature.as_str()) + ); + Ok(()) +} + +#[test] +fn test_changelog_only_update_preserves_stable_phase() -> common::TestResult { + let temp_dir = init_project()?; + + let output = run_commands( + temp_dir.path(), + &[ + &["rfc", "new", "Test RFC"], + &["rfc", "finalize", "RFC-0001", "normative"], + &["rfc", "advance", "RFC-0001", "impl"], + &["rfc", "advance", "RFC-0001", "test"], + &["rfc", "advance", "RFC-0001", "stable"], &["rfc", "get", "RFC-0001", "phase"], &[ "rfc", @@ -486,7 +626,8 @@ fn test_bump_change_rejects_legacy_signature_without_mutation() -> common::TestR } #[test] -fn test_baseline_bump_with_pending_clause_restarts_at_spec() -> common::TestResult { +fn test_version_bump_rejects_missing_signature_and_preserves_pending_clause() -> common::TestResult +{ let temp_dir = init_project()?; run_commands( temp_dir.path(), @@ -518,30 +659,36 @@ fn test_baseline_bump_with_pending_clause_restarts_at_spec() -> common::TestResu ]], )?; + let clause_path = temp_dir + .path() + .join("gov/rfc/RFC-0001/clauses/C-PENDING.toml"); + let rfc_before = fs::read(&rfc_path)?; + let clause_before = fs::read(&clause_path)?; + let output = run_commands( temp_dir.path(), - &[ - &[ - "rfc", - "bump", - "RFC-0001", - "--patch", - "--summary", - "Release pending clause", - ], - &["rfc", "get", "RFC-0001", "phase"], - &["clause", "get", "RFC-0001:C-PENDING", "since"], - ], + &[&[ + "rfc", + "bump", + "RFC-0001", + "--patch", + "--summary", + "Release pending clause", + ]], )?; + assert!(output.contains("error[E0505]"), "output: {output}"); assert!( - output.contains("$ govctl rfc get RFC-0001 phase\nspec"), - "output: {output}" - ); - assert!( - output.contains("$ govctl clause get RFC-0001:C-PENDING since\n0.1.1"), + output.contains("sealed RFC content signature"), "output: {output}" ); + assert!(output.contains("govctl migrate"), "output: {output}"); + assert!(!output.contains("Bumped RFC-0001"), "output: {output}"); + assert!(!output.contains("Set C-PENDING.since"), "output: {output}"); + assert_eq!(fs::read(&rfc_path)?, rfc_before); + assert_eq!(fs::read(&clause_path)?, clause_before); + let clause: toml::Value = toml::from_str(&fs::read_to_string(&clause_path)?)?; + assert!(clause["govctl"].get("since").is_none()); Ok(()) } @@ -554,14 +701,9 @@ fn test_bump_rejects_empty_bump_after_signature_baseline() -> common::TestResult &[ &["rfc", "new", "Test RFC"], &["rfc", "finalize", "RFC-0001", "normative"], - &[ - "rfc", - "bump", - "RFC-0001", - "--patch", - "--summary", - "Establish baseline", - ], + &["rfc", "advance", "RFC-0001", "impl"], + &["rfc", "advance", "RFC-0001", "test"], + &["rfc", "advance", "RFC-0001", "stable"], &[ "rfc", "bump", @@ -586,14 +728,9 @@ fn test_bump_rejects_changelog_only_after_signature_baseline() -> common::TestRe &[ &["rfc", "new", "Test RFC"], &["rfc", "finalize", "RFC-0001", "normative"], - &[ - "rfc", - "bump", - "RFC-0001", - "--patch", - "--summary", - "Establish baseline", - ], + &["rfc", "advance", "RFC-0001", "impl"], + &["rfc", "advance", "RFC-0001", "test"], + &["rfc", "advance", "RFC-0001", "stable"], &[ "rfc", "bump", @@ -642,14 +779,9 @@ fn test_bump_change_does_not_clear_pending_amendment() -> common::TestResult { "Original normative behavior.", ], &["rfc", "finalize", "RFC-0001", "normative"], - &[ - "rfc", - "bump", - "RFC-0001", - "--patch", - "--summary", - "Establish baseline", - ], + &["rfc", "advance", "RFC-0001", "impl"], + &["rfc", "advance", "RFC-0001", "test"], + &["rfc", "advance", "RFC-0001", "stable"], &[ "clause", "edit", @@ -694,6 +826,9 @@ fn test_bump_rejects_malformed_unlisted_clause_without_mutation() -> common::Tes &[ &["rfc", "new", "Test RFC"], &["rfc", "finalize", "RFC-0001", "normative"], + &["rfc", "advance", "RFC-0001", "impl"], + &["rfc", "advance", "RFC-0001", "test"], + &["rfc", "advance", "RFC-0001", "stable"], ], )?; diff --git a/tests/snapshots/test_delete__delete_clause_safeguard_normative.snap b/tests/snapshots/test_delete__delete_clause_safeguard_normative.snap index b0c607fc..3c1c2fcf 100644 --- a/tests/snapshots/test_delete__delete_clause_safeguard_normative.snap +++ b/tests/snapshots/test_delete__delete_clause_safeguard_normative.snap @@ -1,7 +1,7 @@ --- -source: tests/test_delete.rs -expression: "normalize_output(&output, temp_dir.path(), &date)" +source: tests/delete_tests/clause.rs +expression: value --- $ govctl clause delete RFC-0001:C-LOCKED -f -error[E0110]: Cannot delete clause: RFC-0001 is normative. Only draft RFCs allow clause deletion. (RFC-0001:C-LOCKED) +error[E0104]: Cannot delete clause from RFC-0001 while status=normative, phase=stable, version=1.0.0, and clause since=1.0.0. Clause deletion is limited to draft RFCs or Clauses introduced in the current normative spec candidate. (RFC-0001:C-LOCKED) exit: 1 diff --git a/tests/snapshots/test_delete__delete_clause_success_current_normative_spec_candidate.snap b/tests/snapshots/test_delete__delete_clause_success_current_normative_spec_candidate.snap new file mode 100644 index 00000000..1ae6b350 --- /dev/null +++ b/tests/snapshots/test_delete__delete_clause_success_current_normative_spec_candidate.snap @@ -0,0 +1,26 @@ +--- +source: tests/delete_tests/clause.rs +expression: value +--- +$ govctl clause delete RFC-0001:C-DELETE -f +✓ Deleted clause RFC-0001:C-DELETE +exit: 0 + +$ govctl clause list RFC-0001 +┌────────┬──────────┬───────────┬────────┬──────────────────┐ +│ Clause ┆ RFC ┆ Kind ┆ Status ┆ Title │ +╞════════╪══════════╪═══════════╪════════╪══════════════════╡ +│ C-KEEP ┆ RFC-0001 ┆ normative ┆ active ┆ Keep This Clause │ +└────────┴──────────┴───────────┴────────┴──────────────────┘ +exit: 0 + +$ govctl check +Checked: + 1 RFCs + 1 clauses + 0 ADRs + 0 work items + 0 verification guards + +✓ All checks passed +exit: 0 diff --git a/tests/snapshots/test_help__rfc_bump_help.snap b/tests/snapshots/test_help__rfc_bump_help.snap index 629b6e79..44647780 100644 --- a/tests/snapshots/test_help__rfc_bump_help.snap +++ b/tests/snapshots/test_help__rfc_bump_help.snap @@ -26,7 +26,8 @@ EXAMPLES: govctl rfc bump RFC-0001 -c "fix: Correct current-version wording" NOTES: - - Version-changing bumps require normative RFC status; finalize a draft first. + - Version-changing bumps require a normative RFC in impl, test, or stable with a sealed signature. + - While an RFC is in spec, continue authoring the current version candidate instead of bumping again. - Choose one of `--patch`, `--minor`, or `--major` when releasing a content amendment. - `--change` without a bump level updates the current changelog entry without changing version. - Use `-m/--summary` for a release summary and `-c/--change` for detailed entries. diff --git a/tests/snapshots/test_lifecycle__bump_change_does_not_clear_pending_amendment.snap b/tests/snapshots/test_lifecycle__bump_change_does_not_clear_pending_amendment.snap index 4c192981..27551bbf 100644 --- a/tests/snapshots/test_lifecycle__bump_change_does_not_clear_pending_amendment.snap +++ b/tests/snapshots/test_lifecycle__bump_change_does_not_clear_pending_amendment.snap @@ -21,8 +21,16 @@ $ govctl rfc finalize RFC-0001 normative Finalized RFC-0001 to status: normative exit: 0 -$ govctl rfc bump RFC-0001 --patch --summary Establish baseline -Bumped RFC-0001 to 0.1.1 +$ govctl rfc advance RFC-0001 impl +Advanced RFC-0001 to phase: impl +exit: 0 + +$ govctl rfc advance RFC-0001 test +Advanced RFC-0001 to phase: test +exit: 0 + +$ govctl rfc advance RFC-0001 stable +Advanced RFC-0001 to phase: stable exit: 0 $ govctl clause edit RFC-0001:C-TEST --text Updated normative behavior. @@ -30,25 +38,25 @@ Updated clause: RFC-0001:C-TEST exit: 0 $ govctl rfc bump RFC-0001 --change Added release note -Added change to RFC-0001 v0.1.1: Added release note +Added change to RFC-0001 v0.1.0: Added release note exit: 0 $ govctl rfc list -┌───────────┬─────────┬───────────┬───────┬──────────┐ -│ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ -╞═══════════╪═════════╪═══════════╪═══════╪══════════╡ -│ RFC-0001* ┆ 0.1.1 ┆ normative ┆ spec ┆ Test RFC │ -└───────────┴─────────┴───────────┴───────┴──────────┘ +┌───────────┬─────────┬───────────┬────────┬──────────┐ +│ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ +╞═══════════╪═════════╪═══════════╪════════╪══════════╡ +│ RFC-0001* ┆ 0.1.0 ┆ normative ┆ stable ┆ Test RFC │ +└───────────┴─────────┴───────────┴────────┴──────────┘ exit: 0 $ govctl rfc bump RFC-0001 --patch --summary Release amendment -Bumped RFC-0001 to 0.1.2 +Bumped RFC-0001 to 0.1.1 exit: 0 $ govctl rfc list ┌──────────┬─────────┬───────────┬───────┬──────────┐ │ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ ╞══════════╪═════════╪═══════════╪═══════╪══════════╡ -│ RFC-0001 ┆ 0.1.2 ┆ normative ┆ spec ┆ Test RFC │ +│ RFC-0001 ┆ 0.1.1 ┆ normative ┆ spec ┆ Test RFC │ └──────────┴─────────┴───────────┴───────┴──────────┘ exit: 0 diff --git a/tests/snapshots/test_lifecycle__bump_major_version.snap b/tests/snapshots/test_lifecycle__bump_major_version.snap index 943ec4ed..d190ca6c 100644 --- a/tests/snapshots/test_lifecycle__bump_major_version.snap +++ b/tests/snapshots/test_lifecycle__bump_major_version.snap @@ -1,6 +1,6 @@ --- -source: tests/test_lifecycle.rs -expression: "normalize_output(&output, temp_dir.path(), &date)" +source: tests/lifecycle_tests/rfc_cases/bump.rs +expression: snapshot --- $ govctl rfc new Test RFC Created RFC: gov/rfc/RFC-0001/rfc.toml @@ -11,14 +11,30 @@ $ govctl rfc finalize RFC-0001 normative Finalized RFC-0001 to status: normative exit: 0 +$ govctl rfc advance RFC-0001 impl +Advanced RFC-0001 to phase: impl +exit: 0 + +$ govctl rfc advance RFC-0001 test +Advanced RFC-0001 to phase: test +exit: 0 + +$ govctl rfc advance RFC-0001 stable +Advanced RFC-0001 to phase: stable +exit: 0 + +$ govctl rfc edit RFC-0001 title --set Test RFC major amendment +Set RFC-0001.title = Test RFC major amendment +exit: 0 + $ govctl rfc bump RFC-0001 --major --summary Breaking change Bumped RFC-0001 to 1.0.0 exit: 0 $ govctl rfc list -┌──────────┬─────────┬───────────┬───────┬──────────┐ -│ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ -╞══════════╪═════════╪═══════════╪═══════╪══════════╡ -│ RFC-0001 ┆ 1.0.0 ┆ normative ┆ spec ┆ Test RFC │ -└──────────┴─────────┴───────────┴───────┴──────────┘ +┌──────────┬─────────┬───────────┬───────┬──────────────────────────┐ +│ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ +╞══════════╪═════════╪═══════════╪═══════╪══════════════════════════╡ +│ RFC-0001 ┆ 1.0.0 ┆ normative ┆ spec ┆ Test RFC major amendment │ +└──────────┴─────────┴───────────┴───────┴──────────────────────────┘ exit: 0 diff --git a/tests/snapshots/test_lifecycle__bump_minor_version.snap b/tests/snapshots/test_lifecycle__bump_minor_version.snap index eb30f75e..48531667 100644 --- a/tests/snapshots/test_lifecycle__bump_minor_version.snap +++ b/tests/snapshots/test_lifecycle__bump_minor_version.snap @@ -1,6 +1,6 @@ --- -source: tests/test_lifecycle.rs -expression: "normalize_output(&output, temp_dir.path(), &date)" +source: tests/lifecycle_tests/rfc_cases/bump.rs +expression: snapshot --- $ govctl rfc new Test RFC Created RFC: gov/rfc/RFC-0001/rfc.toml @@ -11,14 +11,30 @@ $ govctl rfc finalize RFC-0001 normative Finalized RFC-0001 to status: normative exit: 0 +$ govctl rfc advance RFC-0001 impl +Advanced RFC-0001 to phase: impl +exit: 0 + +$ govctl rfc advance RFC-0001 test +Advanced RFC-0001 to phase: test +exit: 0 + +$ govctl rfc advance RFC-0001 stable +Advanced RFC-0001 to phase: stable +exit: 0 + +$ govctl rfc edit RFC-0001 title --set Test RFC minor amendment +Set RFC-0001.title = Test RFC minor amendment +exit: 0 + $ govctl rfc bump RFC-0001 --minor --summary New feature Bumped RFC-0001 to 0.2.0 exit: 0 $ govctl rfc list -┌──────────┬─────────┬───────────┬───────┬──────────┐ -│ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ -╞══════════╪═════════╪═══════════╪═══════╪══════════╡ -│ RFC-0001 ┆ 0.2.0 ┆ normative ┆ spec ┆ Test RFC │ -└──────────┴─────────┴───────────┴───────┴──────────┘ +┌──────────┬─────────┬───────────┬───────┬──────────────────────────┐ +│ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ +╞══════════╪═════════╪═══════════╪═══════╪══════════════════════════╡ +│ RFC-0001 ┆ 0.2.0 ┆ normative ┆ spec ┆ Test RFC minor amendment │ +└──────────┴─────────┴───────────┴───────┴──────────────────────────┘ exit: 0 diff --git a/tests/snapshots/test_lifecycle__bump_patch_version.snap b/tests/snapshots/test_lifecycle__bump_patch_version.snap index 3a721d72..3f550d4e 100644 --- a/tests/snapshots/test_lifecycle__bump_patch_version.snap +++ b/tests/snapshots/test_lifecycle__bump_patch_version.snap @@ -1,6 +1,6 @@ --- -source: tests/test_lifecycle.rs -expression: "normalize_output(&output, temp_dir.path(), &date)" +source: tests/lifecycle_tests/rfc_cases/bump.rs +expression: snapshot --- $ govctl rfc new Test RFC Created RFC: gov/rfc/RFC-0001/rfc.toml @@ -11,14 +11,30 @@ $ govctl rfc finalize RFC-0001 normative Finalized RFC-0001 to status: normative exit: 0 +$ govctl rfc advance RFC-0001 impl +Advanced RFC-0001 to phase: impl +exit: 0 + +$ govctl rfc advance RFC-0001 test +Advanced RFC-0001 to phase: test +exit: 0 + +$ govctl rfc advance RFC-0001 stable +Advanced RFC-0001 to phase: stable +exit: 0 + +$ govctl rfc edit RFC-0001 title --set Test RFC patch amendment +Set RFC-0001.title = Test RFC patch amendment +exit: 0 + $ govctl rfc bump RFC-0001 --patch --summary Minor fix Bumped RFC-0001 to 0.1.1 exit: 0 $ govctl rfc list -┌──────────┬─────────┬───────────┬───────┬──────────┐ -│ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ -╞══════════╪═════════╪═══════════╪═══════╪══════════╡ -│ RFC-0001 ┆ 0.1.1 ┆ normative ┆ spec ┆ Test RFC │ -└──────────┴─────────┴───────────┴───────┴──────────┘ +┌──────────┬─────────┬───────────┬───────┬──────────────────────────┐ +│ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ +╞══════════╪═════════╪═══════════╪═══════╪══════════════════════════╡ +│ RFC-0001 ┆ 0.1.1 ┆ normative ┆ spec ┆ Test RFC patch amendment │ +└──────────┴─────────┴───────────┴───────┴──────────────────────────┘ exit: 0 diff --git a/tests/snapshots/test_lifecycle__bump_rejects_changelog_only_after_signature_baseline.snap b/tests/snapshots/test_lifecycle__bump_rejects_changelog_only_after_signature_baseline.snap index 5a016077..a572bae5 100644 --- a/tests/snapshots/test_lifecycle__bump_rejects_changelog_only_after_signature_baseline.snap +++ b/tests/snapshots/test_lifecycle__bump_rejects_changelog_only_after_signature_baseline.snap @@ -11,12 +11,20 @@ $ govctl rfc finalize RFC-0001 normative Finalized RFC-0001 to status: normative exit: 0 -$ govctl rfc bump RFC-0001 --patch --summary Establish baseline -Bumped RFC-0001 to 0.1.1 +$ govctl rfc advance RFC-0001 impl +Advanced RFC-0001 to phase: impl +exit: 0 + +$ govctl rfc advance RFC-0001 test +Advanced RFC-0001 to phase: test +exit: 0 + +$ govctl rfc advance RFC-0001 stable +Advanced RFC-0001 to phase: stable exit: 0 $ govctl rfc bump RFC-0001 --change Added changelog note -Added change to RFC-0001 v0.1.1: Added changelog note +Added change to RFC-0001 v0.1.0: Added changelog note exit: 0 $ govctl rfc bump RFC-0001 --patch --summary Changelog-only bump @@ -24,9 +32,9 @@ error[E0113]: RFC version bump requires RFC or clause content changes since the exit: 1 $ govctl rfc list -┌──────────┬─────────┬───────────┬───────┬──────────┐ -│ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ -╞══════════╪═════════╪═══════════╪═══════╪══════════╡ -│ RFC-0001 ┆ 0.1.1 ┆ normative ┆ spec ┆ Test RFC │ -└──────────┴─────────┴───────────┴───────┴──────────┘ +┌──────────┬─────────┬───────────┬────────┬──────────┐ +│ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ +╞══════════╪═════════╪═══════════╪════════╪══════════╡ +│ RFC-0001 ┆ 0.1.0 ┆ normative ┆ stable ┆ Test RFC │ +└──────────┴─────────┴───────────┴────────┴──────────┘ exit: 0 diff --git a/tests/snapshots/test_lifecycle__bump_rejects_empty_bump_after_signature_baseline.snap b/tests/snapshots/test_lifecycle__bump_rejects_empty_bump_after_signature_baseline.snap index b4852797..a1027df4 100644 --- a/tests/snapshots/test_lifecycle__bump_rejects_empty_bump_after_signature_baseline.snap +++ b/tests/snapshots/test_lifecycle__bump_rejects_empty_bump_after_signature_baseline.snap @@ -11,8 +11,16 @@ $ govctl rfc finalize RFC-0001 normative Finalized RFC-0001 to status: normative exit: 0 -$ govctl rfc bump RFC-0001 --patch --summary Establish baseline -Bumped RFC-0001 to 0.1.1 +$ govctl rfc advance RFC-0001 impl +Advanced RFC-0001 to phase: impl +exit: 0 + +$ govctl rfc advance RFC-0001 test +Advanced RFC-0001 to phase: test +exit: 0 + +$ govctl rfc advance RFC-0001 stable +Advanced RFC-0001 to phase: stable exit: 0 $ govctl rfc bump RFC-0001 --patch --summary Empty bump @@ -20,9 +28,9 @@ error[E0113]: RFC version bump requires RFC or clause content changes since the exit: 1 $ govctl rfc list -┌──────────┬─────────┬───────────┬───────┬──────────┐ -│ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ -╞══════════╪═════════╪═══════════╪═══════╪══════════╡ -│ RFC-0001 ┆ 0.1.1 ┆ normative ┆ spec ┆ Test RFC │ -└──────────┴─────────┴───────────┴───────┴──────────┘ +┌──────────┬─────────┬───────────┬────────┬──────────┐ +│ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ +╞══════════╪═════════╪═══════════╪════════╪══════════╡ +│ RFC-0001 ┆ 0.1.0 ┆ normative ┆ stable ┆ Test RFC │ +└──────────┴─────────┴───────────┴────────┴──────────┘ exit: 0 diff --git a/tests/snapshots/test_rfc_lifecycle__rfc_amendment_tracking.snap b/tests/snapshots/test_rfc_lifecycle__rfc_amendment_tracking.snap index 1c301dcd..d780c53c 100644 --- a/tests/snapshots/test_rfc_lifecycle__rfc_amendment_tracking.snap +++ b/tests/snapshots/test_rfc_lifecycle__rfc_amendment_tracking.snap @@ -1,6 +1,6 @@ --- source: tests/test_rfc_lifecycle.rs -expression: "normalize_output(&combined, temp_dir.path(), &date)" +expression: normalized --- $ govctl rfc new Amendment Test RFC Created RFC: gov/rfc/RFC-0001/rfc.toml @@ -21,16 +21,24 @@ $ govctl rfc finalize RFC-0001 normative Finalized RFC-0001 to status: normative exit: 0 -$ govctl rfc bump RFC-0001 --minor --summary Establish baseline with signature -Bumped RFC-0001 to 0.2.0 +$ govctl rfc advance RFC-0001 impl +Advanced RFC-0001 to phase: impl +exit: 0 + +$ govctl rfc advance RFC-0001 test +Advanced RFC-0001 to phase: test +exit: 0 + +$ govctl rfc advance RFC-0001 stable +Advanced RFC-0001 to phase: stable exit: 0 $ govctl rfc list -┌──────────┬─────────┬───────────┬───────┬────────────────────┐ -│ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ -╞══════════╪═════════╪═══════════╪═══════╪════════════════════╡ -│ RFC-0001 ┆ 0.2.0 ┆ normative ┆ spec ┆ Amendment Test RFC │ -└──────────┴─────────┴───────────┴───────┴────────────────────┘ +┌──────────┬─────────┬───────────┬────────┬────────────────────┐ +│ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ +╞══════════╪═════════╪═══════════╪════════╪════════════════════╡ +│ RFC-0001 ┆ 0.1.0 ┆ normative ┆ stable ┆ Amendment Test RFC │ +└──────────┴─────────┴───────────┴────────┴────────────────────┘ exit: 0 $ govctl clause edit RFC-0001:C-AMEND-TEST --text AMENDED text - content changed. @@ -38,21 +46,21 @@ Updated clause: RFC-0001:C-AMEND-TEST exit: 0 $ govctl rfc list -┌───────────┬─────────┬───────────┬───────┬────────────────────┐ -│ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ -╞═══════════╪═════════╪═══════════╪═══════╪════════════════════╡ -│ RFC-0001* ┆ 0.2.0 ┆ normative ┆ spec ┆ Amendment Test RFC │ -└───────────┴─────────┴───────────┴───────┴────────────────────┘ +┌───────────┬─────────┬───────────┬────────┬────────────────────┐ +│ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ +╞═══════════╪═════════╪═══════════╪════════╪════════════════════╡ +│ RFC-0001* ┆ 0.1.0 ┆ normative ┆ stable ┆ Amendment Test RFC │ +└───────────┴─────────┴───────────┴────────┴────────────────────┘ exit: 0 $ govctl rfc bump RFC-0001 --patch --summary Release amendment -Bumped RFC-0001 to 0.2.1 +Bumped RFC-0001 to 0.1.1 exit: 0 $ govctl rfc list ┌──────────┬─────────┬───────────┬───────┬────────────────────┐ │ RFC ┆ Version ┆ Status ┆ Phase ┆ Title │ ╞══════════╪═════════╪═══════════╪═══════╪════════════════════╡ -│ RFC-0001 ┆ 0.2.1 ┆ normative ┆ spec ┆ Amendment Test RFC │ +│ RFC-0001 ┆ 0.1.1 ┆ normative ┆ spec ┆ Amendment Test RFC │ └──────────┴─────────┴───────────┴───────┴────────────────────┘ exit: 0 diff --git a/tests/test_rfc_lifecycle.rs b/tests/test_rfc_lifecycle.rs index 4148a624..dee96e61 100644 --- a/tests/test_rfc_lifecycle.rs +++ b/tests/test_rfc_lifecycle.rs @@ -33,19 +33,14 @@ fn test_rfc_amendment_tracking() -> common::TestResult { ], )?; - // Finalize and bump to set baseline signature + // Enter implementation to seal the initial version baseline. let baseline = run_commands( temp_dir.path(), &[ &["rfc", "finalize", "RFC-0001", "normative"], - &[ - "rfc", - "bump", - "RFC-0001", - "--minor", - "--summary", - "Establish baseline with signature", - ], + &["rfc", "advance", "RFC-0001", "impl"], + &["rfc", "advance", "RFC-0001", "test"], + &["rfc", "advance", "RFC-0001", "stable"], &["rfc", "list"], ], )?;