From 07efb73bb4122f3e3d809ee4d926bf3a4841f8a2 Mon Sep 17 00:00:00 2001 From: Gabriel Wu <13583761+lucifer1004@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:17:25 +0800 Subject: [PATCH 1/2] feat(rfc): support in-version spec authoring --- .claude/skills/gov/SKILL.md | 14 +- .claude/skills/rfc-writer/SKILL.md | 21 ++ .claude/skills/spec/SKILL.md | 24 +- AGENTS.md | 19 +- CHANGELOG.md | 22 ++ docs/guide/rfcs.md | 49 ++- docs/rfc/RFC-0000.md | 87 +++-- docs/rfc/RFC-0001.md | 73 +++-- docs/rfc/RFC-0002.md | 158 +++++---- gov/rfc/RFC-0000/clauses/C-CLAUSE-DEF.toml | 14 +- .../RFC-0000/clauses/C-PHASE-LIFECYCLE.toml | 40 ++- gov/rfc/RFC-0000/clauses/C-RFC-DEF.toml | 8 +- .../RFC-0000/clauses/C-STATUS-LIFECYCLE.toml | 18 +- gov/rfc/RFC-0000/rfc.toml | 12 +- .../RFC-0001/clauses/C-GATE-CONDITIONS.toml | 34 +- gov/rfc/RFC-0001/clauses/C-RFC-PHASE.toml | 32 +- gov/rfc/RFC-0001/rfc.toml | 12 +- .../RFC-0002/clauses/C-LIFECYCLE-VERBS.toml | 146 ++++++--- gov/rfc/RFC-0002/rfc.toml | 12 +- gov/schema/edit-ops.json | 75 ++++- gov/schema/edit-ops.schema.json | 2 +- ...-in-version-rfc-authoring-during-spec.toml | 80 +++++ src/cli/resources/rfc.rs | 17 +- src/cmd/edit/add.rs | 26 +- src/cmd/edit/doc_target/mod.rs | 1 + src/cmd/edit/doc_target/rfc_changelog.rs | 173 ++++++++++ src/cmd/edit/doc_target/set.rs | 5 + src/cmd/edit/get.rs | 26 +- src/cmd/edit/remove.rs | 26 +- src/cmd/edit/rules.rs | 22 +- src/cmd/lifecycle/mod.rs | 1 + src/cmd/lifecycle/rfc.rs | 87 +++-- src/cmd/lifecycle/rfc_clause_versions.rs | 18 +- src/cmd/new/artifacts/clause.rs | 63 ++-- src/diagnostic/code/metadata.rs | 1 + src/diagnostic/code/mod.rs | 3 + src/validate/rfc.rs | 20 +- src/write/changelog/mod.rs | 77 +++-- src/write/changelog/tests.rs | 58 +++- src/write/mod.rs | 5 +- tests/common/fixtures.rs | 5 + tests/edit_tests/rfc.rs | 301 ++++++++++++++++++ tests/error_tests/rfc_clause_cases/check.rs | 60 ++++ .../rfc_clause_cases/wire_format.rs | 20 ++ tests/lifecycle_tests/clause.rs | 156 +++++++++ tests/lifecycle_tests/rfc_cases/advance.rs | 119 ++++++- tests/lifecycle_tests/rfc_cases/bump.rs | 206 ++++++++++++ .../lifecycle_tests/rfc_cases/deprecation.rs | 40 +++ ...ors__legacy_flat_clause_toml_accepted.snap | 5 +- ...errors__legacy_flat_rfc_toml_accepted.snap | 6 +- ...errors__valid_clause_toml_wire_format.snap | 6 +- ...st_errors__valid_rfc_toml_wire_format.snap | 6 +- tests/snapshots/test_help__rfc_bump_help.snap | 32 ++ tests/snapshots/test_help__rfc_edit_help.snap | 71 +++++ tests/snapshots/test_help__rfc_get_help.snap | 5 +- tests/test_help.rs | 10 + 56 files changed, 2263 insertions(+), 366 deletions(-) create mode 100644 gov/work/2026-07-16-allow-in-version-rfc-authoring-during-spec.toml create mode 100644 src/cmd/edit/doc_target/rfc_changelog.rs create mode 100644 tests/snapshots/test_help__rfc_bump_help.snap create mode 100644 tests/snapshots/test_help__rfc_edit_help.snap diff --git a/.claude/skills/gov/SKILL.md b/.claude/skills/gov/SKILL.md index 9356443b..e01ec3ac 100644 --- a/.claude/skills/gov/SKILL.md +++ b/.claude/skills/gov/SKILL.md @@ -200,11 +200,19 @@ govctl check --has-active For RFC-governed work, verify the RFC state: - `draft/spec`: ask permission, then finalize and advance to `impl` -- `normative/spec`: ask permission, then advance to `impl` -- `normative/impl+`: proceed +- `normative/spec`: continue authoring the current version without another bump; ask permission before advancing to `impl`, which seals its RFC and Clause content +- `normative/impl+`: proceed against the sealed current-version baseline - `deprecated`: stop -If implementation reveals a spec bug, do not silently deviate. Amend the RFC per [[ADR-0016]] or stop and ask. +If implementation reveals a spec bug, do not silently deviate: + +- A code-only implementation defect does not require an RFC edit or bump. +- Editing RFC or Clause content after entry to `impl` creates an unversioned amendment. Do not advance to `test` while it exists. +- Release that amendment 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. + +Ask before the lifecycle-owned bump unless the user already granted full authority. Implementation rules: diff --git a/.claude/skills/rfc-writer/SKILL.md b/.claude/skills/rfc-writer/SKILL.md index 8857b6ef..aec74697 100644 --- a/.claude/skills/rfc-writer/SKILL.md +++ b/.claude/skills/rfc-writer/SKILL.md @@ -137,6 +137,27 @@ Only include representation details when they are themselves the external contra | Specification | normative | MUST/SHOULD/MAY requirements | | Rationale | informative | Extended explanation | +## Phase-Aware Authoring + +Before editing an existing normative RFC, inspect its phase: + +- In `spec`, RFC and Clause edits refine the current version candidate and do not require another version bump. +- In `impl`, `test`, or `stable`, RFC or Clause edits create an unversioned amendment. The `/spec` or `/gov` workflow must release it 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. + +This helper writes specification content but does not perform lifecycle verbs. +Use `/spec` or `/gov` for bump and advance decisions. + +## Clause Version Assignment + +Clause `since` is lifecycle-owned; do not write it into clause text or try to set +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. +- A deprecated RFC does not accept new Clauses. + ## Rendering Rules The renderer auto-generates structural elements. **Do NOT include these in clause `text`:** diff --git a/.claude/skills/spec/SKILL.md b/.claude/skills/spec/SKILL.md index 5d60438f..f7a4db96 100644 --- a/.claude/skills/spec/SKILL.md +++ b/.claude/skills/spec/SKILL.md @@ -26,6 +26,7 @@ Use this workflow for spec-only governance work: refine or accept ADRs, clarify 7. Validate with `govctl check`, and run `govctl render` when rendered docs should change. 8. Use `/commit` for raw VCS operations. This workflow defines what to record, not how to invoke VCS directly. 9. Do not let RFCs absorb implementation structure or let ADRs absorb work-item execution details. Preserve artifact roles while editing. +10. Do not bump an RFC again merely because its current version is already in `spec`; edits there belong to that version candidate. ## Quick Reference @@ -47,6 +48,8 @@ govctl adr accept govctl clause edit :C- text --stdin <<'EOF' ... EOF govctl rfc bump --patch -m "Clarify clause wording" +govctl rfc get changelog +govctl rfc edit changelog.summary --set "Clarify current version" govctl rfc finalize normative govctl check @@ -81,6 +84,10 @@ govctl rfc show govctl adr show ``` +Before editing a normative RFC, inspect its current phase. The phase determines +whether the edit remains in the current version candidate or must be released as +a new version. + For artifact editing conventions, follow the appropriate writer skill: - RFC changes -> **rfc-writer** @@ -99,7 +106,9 @@ For RFC work: - Edit clauses with `govctl clause edit` - If the RFC is draft and ready to become normative, ask permission before `govctl rfc finalize normative` -- If amending an existing normative RFC, ask permission before `govctl rfc bump` +- 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` +- Use `govctl rfc edit changelog.*` for current-version changelog corrections; these do not change version, phase, or the sealed content signature Semver guidance for RFC amendments: @@ -107,7 +116,9 @@ Semver guidance for RFC amendments: - `--minor`: additive requirement or newly specified behavior - `--major`: breaking or incompatible requirement change -Every RFC bump must include a changelog summary via `-m`. +Every version-changing RFC bump must include a changelog summary via `-m`. +`--change` without a bump level is changelog-only and cannot release an RFC or +Clause content amendment. ### 4. Review and validate @@ -157,10 +168,11 @@ If the task grows into implementation work, stop here and hand off to `/gov`. ### Clarify an RFC without changing behavior -1. Edit the clause text with `govctl clause edit` -2. Run **rfc-reviewer** -3. Ask permission, then run `govctl rfc bump --patch -m "Clarify wording"` -4. Run `govctl check` and `govctl render` +1. Inspect the RFC phase with `govctl rfc show ` +2. Edit the clause text with `govctl clause edit` +3. Run **rfc-reviewer** +4. If the RFC was already in `spec`, keep the same version; otherwise ask permission, then run `govctl rfc bump --patch -m "Clarify wording"` +5. Run `govctl check` and `govctl render` ### Prepare a deprecation plan without implementation diff --git a/AGENTS.md b/AGENTS.md index 4b310d2c..cabd08d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,7 +50,8 @@ The `gov/` directory is authoritative for governance artifacts. Rendered project 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 RFCs MAY be amended: version bump + changelog per [[ADR-0016]] +- 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 - Cite RFC clauses when implementing invariants ### Law 2: Phase Discipline @@ -83,7 +84,7 @@ draft → normative → deprecated ``` - **draft**: Under discussion. Implementation MUST NOT depend on draft RFCs. -- **normative**: Binding. Implementation MUST conform to current version. Spec MAY evolve via version bumps with changelog entries per [[ADR-0016]]. +- **normative**: Binding. Content in `spec` is the current version candidate; entry to `impl` seals that version as the implementation baseline. Later amendments start a new version lifecycle through a version bump per [[ADR-0016]]. - **deprecated**: Superseded. No new work permitted. Reverse transitions are forbidden. @@ -92,13 +93,21 @@ Reverse transitions are forbidden. | Status \ Phase | spec | impl | test | stable | | -------------- | ---- | ---- | ---- | ------ | -| draft | ✅ | ⚠️ | ⚠️ | ❌ | +| draft | ✅ | ❌ | ❌ | ❌ | | normative | ✅ | ✅ | ✅ | ✅ | | deprecated | ✅ | ❌ | ❌ | ✅ | -- ⚠️ = experimental, gates are soft warnings - ❌ = forbidden +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. + +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. + --- ## 4. Decision Tree @@ -167,6 +176,8 @@ govctl loop replan LOOP-YYYY-MM-DD-NNN # Viewing artifacts (styled markdown to stdout) govctl rfc show RFC-0001 # Show RFC +govctl rfc get RFC-0001 changelog +govctl rfc edit RFC-0001 changelog.summary --set "Clarify current version" govctl adr show ADR-0001 # Show ADR govctl work show WI-ID # Show work item govctl clause show RFC-0001:C-X # Show clause diff --git a/CHANGELOG.md b/CHANGELOG.md index 88d6c407..c5ff8285 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,22 @@ Release entries are curated summaries for readers. Work item traceability remain ## [Unreleased] +### Added + +- RFC edit and get paths expose only the changelog entry matching the current RFC version, with summary and category mutations (WI-2026-07-16-001) + +### Changed + +- Lifecycle RFC clauses document current-version spec authoring, implementation-entry sealing, and current-changelog correction boundaries (WI-2026-07-16-001) +- Change-only rfc bump --change uses current-version resolution and CLI help documents its no-version-bump behavior (WI-2026-07-16-001) + +### Fixed + +- RFC and Clause edits made in spec can advance to impl without another version bump (WI-2026-07-16-001) +- RFC changelog editing rejects historical entries and lifecycle-owned version and date fields (WI-2026-07-16-001) +- Clause creation assigns since immediately only for normative spec RFCs, while draft and non-spec amendments remain pending for finalize or bump (WI-2026-07-16-001) +- Entering impl seals the current content signature and rejects unresolved pending Clause versions without rewriting them (WI-2026-07-16-001) + ## [0.11.0] - 2026-07-16 0.11.0 makes lifecycle corrections explicit and failure-safe. RFC amendments @@ -48,6 +64,12 @@ accidental latest local release cut can be undone without rewriting history. - ADR placeholder context now uses its own `W0113` warning instead of the missing-reference warning code. +### Known Limitations + +- In 0.11.0, changing RFC or Clause content after a version bump has entered + `spec` requires another bump before advancing to `impl`. Later releases seal + the final current-version content during the `spec` to `impl` transition. + ## [0.10.2] - 2026-07-05 0.10.2 restores compatibility between the bundled Agent Skills and GitHub diff --git a/docs/guide/rfcs.md b/docs/guide/rfcs.md index 03454596..ea9cc489 100644 --- a/docs/guide/rfcs.md +++ b/docs/guide/rfcs.md @@ -56,13 +56,21 @@ govctl rfc list --tag caching,api ## Canonical Edit Surface -All RFC and clause fields are accessible through path-based editing: +Editable RFC and Clause fields use path-based operations. Lifecycle-owned fields +such as RFC versions, changelog dates, and Clause `since` values use dedicated +commands or automatic assignment instead: ```bash # Lifecycle-managed fields use dedicated verbs govctl rfc bump RFC-0010 --minor -m "Add new clause for edge case" govctl rfc finalize RFC-0010 normative +# Correct metadata for the current RFC version only +govctl rfc get RFC-0010 changelog +govctl rfc edit RFC-0010 changelog.summary --set "Clarify edge-case behavior" +govctl rfc edit RFC-0010 changelog.fixed --add "Correct timeout wording" +govctl rfc edit RFC-0010 changelog.fixed[0] --remove + # Add to array fields govctl rfc edit RFC-0010 refs --add RFC-0001 govctl rfc edit RFC-0010 owners --add "@co-maintainer" @@ -107,6 +115,12 @@ text = """ The system MUST validate all inputs.""" ``` +`since` is assigned when the target version is known. Clauses created in a +draft RFC remain pending until finalization. Clauses created in a normative RFC +already in `spec` receive the current RFC version immediately. Clauses created +in `impl`, `test`, or `stable` remain pending until the content amendment is +released by an RFC version bump. + ### Edit Clause Text ```bash @@ -133,7 +147,7 @@ govctl clause delete RFC-0010:C-MISTAKE -f **Safety:** Deletion is only allowed when: -- The RFC status is `draft` (normative RFCs are immutable) +- The RFC status is `draft` - No other artifacts reference the clause For normative RFCs, use `govctl clause deprecate RFC-0010:C-OLD` instead. @@ -168,14 +182,16 @@ When the spec is complete and approved: govctl rfc finalize RFC-0010 normative ``` -This makes the RFC binding — implementation must conform to it. +This ratifies the RFC lineage. While its current version remains in `spec`, that +content is still an authoring candidate. The version becomes the implementation +baseline when it advances to `impl`. ### Deprecate When an RFC is superseded or obsolete: ```bash -govctl rfc finalize RFC-0010 deprecated +govctl rfc deprecate RFC-0010 ``` ## Phase Lifecycle @@ -197,8 +213,17 @@ govctl rfc advance RFC-0010 stable # Tested, ready for production 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 - Each phase has invariants that must be satisfied +The sealed signature is a content baseline, not a file lock. A code-only defect +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. + ## Versioning RFCs use semantic versioning: @@ -210,6 +235,22 @@ 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. + +Use change-only bump syntax to append categorized metadata without changing the +RFC version: + +```bash +govctl rfc bump RFC-0010 --change "fix: Correct current-version wording" +``` + +Current-version changelog correction always resolves the entry whose version +matches the RFC version, regardless of array order. Historical entries and all +RFC/changelog version and date fields are not editable through the resource +edit surface. + ## Listing and Viewing ```bash diff --git a/docs/rfc/RFC-0000.md b/docs/rfc/RFC-0000.md index e3bde6d9..a823ad3d 100644 --- a/docs/rfc/RFC-0000.md +++ b/docs/rfc/RFC-0000.md @@ -1,9 +1,9 @@ - + # RFC-0000: govctl Governance Framework -> **Version:** 1.5.0 | **Status:** normative | **Phase:** stable +> **Version:** 1.6.0 | **Status:** normative | **Phase:** stable --- @@ -46,10 +46,16 @@ Format evolution is tracked by the project-level `[schema] version` in `gov/conf Entries in `sections[].clauses` MUST reference clause TOML files by relative path (for example `clauses/C-EXAMPLE.toml`). +The RFC `version` field is lifecycle-owned. Resource editing operations MUST NOT modify the RFC `version` field. + +An RFC MUST have exactly one current changelog entry whose `version` equals the RFC's current version. Other changelog entries are historical versions. Validation MUST reject an RFC with zero current changelog entries. Validation MUST reject an RFC with multiple current changelog entries. Current-changelog field operations MUST reject either invalid state without mutation. + +The current entry's summary and categorized changes MAY be corrected without changing the RFC version or phase. Changelog `version` fields are lifecycle-owned. Changelog `date` fields are lifecycle-owned. Resource editing operations MUST NOT modify either lifecycle-owned field. Resource editing operations MUST NOT modify any historical changelog entry. + Implementations MUST reject legacy RFC JSON storage files (`rfc.json`) during normal operations. The diagnostic MUST instruct users to migrate those repositories with a govctl version earlier than 0.9 before upgrading to a TOML-only govctl release. **Rationale:** -This removes the last storage-format split from RFC handling while preserving a clear upgrade path for repositories that still contain legacy JSON sources. +The current changelog entry describes the current RFC version and may need refinement while that version is authored. Matching by version avoids relying on array position. Keeping version identity, dates, and historical entries lifecycle-owned preserves version provenance. > **Tags:** `core`, `schema` @@ -59,18 +65,21 @@ This removes the last storage-format split from RFC handling while preserving a RFC status follows this lifecycle: - draft → normative → deprecated + draft -> normative -> deprecated **draft**: Under discussion. Implementation MUST NOT depend on draft RFCs. -**normative**: Frozen and ratified. Implementation MUST conform to normative RFCs. +**normative**: The RFC lineage is ratified. A normative version in `impl`, `test`, or `stable` is sealed, and implementation MUST conform to that sealed version. A normative version in `spec` is a controlled authoring candidate and MUST NOT be used as an implementation conformance baseline. The most recently sealed version remains the implementation baseline until the current `spec` version enters `impl`. **deprecated**: Superseded or obsolete. Implementation SHOULD migrate away. Transition rules: -- draft → normative: Requires explicit finalization -- normative → deprecated: Requires a superseding RFC or explicit deprecation -- Reverse transitions are FORBIDDEN +- The draft -> normative transition MUST require explicit finalization. +- The normative -> deprecated transition MUST require a superseding RFC or explicit deprecation. +- Reverse status transitions MUST be rejected. + +**Rationale:** +Normative status ratifies the RFC lineage, while phase identifies whether its current version is still being authored or has been sealed for implementation. This distinction permits controlled current-version authoring without presenting mutable `spec` content as a binding implementation contract. > **Tags:** `core`, `lifecycle` @@ -80,30 +89,38 @@ Transition rules: RFC phase describes the current RFC version and follows this lifecycle within that version: - spec → impl → test → stable + spec -> impl -> test -> stable -**spec**: Defining the current version. No implementation work permitted. +**spec**: Authoring the current version candidate. Implementation work against that candidate MUST NOT begin. -**impl**: Building what the current version specifies. +**impl**: Building what the sealed current version specifies. -**test**: Verifying implementation against the current version. +**test**: Verifying implementation against the sealed current version. -**stable**: Implementation and tests for the current version are complete. +**stable**: Implementation and tests for the sealed current version are complete. Phase rules: -- Phases within one version MUST proceed in order; skipping is FORBIDDEN -- Each phase transition requires the previous phase gate to pass -- `stable` is terminal for one RFC version, not for the RFC across later version bumps -- A version bump that releases a detected content amendment MUST start the new version at `spec` -- Changelog-only updates and signature-baseline bookkeeping without a detected content amendment MUST preserve phase -- Phase progression MUST be rejected while a detected content amendment remains unversioned -- draft + stable is FORBIDDEN (cannot stabilize unratified work) -- deprecated + impl/test is FORBIDDEN (no new work on deprecated specs) - -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 bump bookkeeping and MUST be excluded from amendment comparison. An RFC without a stored amendment signature has no comparison baseline; establishing its first baseline MUST NOT by itself be treated as a detected content amendment. +- Phases within one version MUST proceed in order. +- 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 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 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 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. Defining the comparison surface prevents phase progression and release 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. > **Tags:** `core`, `lifecycle` @@ -177,8 +194,20 @@ 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. + +Clause creation and version assignment MUST follow these rules: +- A new clause in a draft RFC MUST be pending. +- Finalizing a draft RFC as normative MUST assign the RFC's current version to every pending clause. +- A new clause in a normative RFC whose phase is `spec` MUST set `since` to the RFC's current version. +- A new clause in a normative RFC whose phase is `impl`, `test`, or `stable` MUST be pending. +- A version bump MUST assign the new RFC version to every pending clause that it releases. +- Creating a clause in a deprecated RFC MUST be rejected. + +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:** -This aligns clause storage with RFC, ADR, Work Item, and Release storage while removing the legacy JSON compatibility path from normal operation. +A clause can record its version immediately only after that target version is known. Initial draft clauses receive a version at finalization, amendments created outside `spec` receive a version at 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. > **Tags:** `core`, `schema` @@ -401,6 +430,14 @@ A guard check MUST pass only when the command exits successfully. If `pattern` i ## Changelog +### v1.6.0 (2026-07-16) + +Define current-version RFC authoring and changelog invariants + +#### Changed + +- Defined spec-phase sealing, Clause version assignment, and current changelog ownership + ### v1.5.0 (2026-07-15) Define guarded correction for local release cuts diff --git a/docs/rfc/RFC-0001.md b/docs/rfc/RFC-0001.md index c78602a9..5dd6ff7c 100644 --- a/docs/rfc/RFC-0001.md +++ b/docs/rfc/RFC-0001.md @@ -1,9 +1,9 @@ - + # RFC-0001: Lifecycle State Machines -> **Version:** 0.5.0 | **Status:** normative | **Phase:** stable +> **Version:** 0.6.0 | **Status:** normative | **Phase:** stable --- @@ -47,29 +47,35 @@ Invalid transitions (MUST be rejected): An RFC MUST have exactly one of the following phase values. The phase describes conformance work for the RFC's current version: -1. **spec** — Specification phase. The RFC text is being written. No implementation work. -2. **impl** — Implementation phase. Code is being written to conform to the current RFC version. -3. **test** — Testing phase. Implementation is complete; tests are being written and validated against the current RFC version. -4. **stable** — Stable phase. Implementation and tests for the current RFC version are complete. +1. **spec** - Specification phase. The current version candidate is being written. Implementation work against that candidate is not permitted. +2. **impl** - Implementation phase. Code is being written to conform to the sealed current version. +3. **test** - Testing phase. Implementation is complete; tests are being written and validated against the sealed current version. +4. **stable** - Stable phase. Implementation and tests for the sealed current version are complete. Within one RFC version, valid transitions are forward only via the `advance` command: -- spec → impl -- impl → test -- test → stable +- spec -> impl +- impl -> test +- test -> stable Within one RFC version, the following transitions MUST be rejected: -- Any backward transition (e.g., impl → spec) -- Any skip (e.g., spec → test, spec → stable) -- stable → any (`stable` is terminal for that version) +- Any backward transition (for example, impl -> spec) +- 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 changelog-only update MUST preserve the current phase. Establishing an amendment signature baseline without a detected content amendment MUST also preserve the current phase. +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`. -Amendment content and bookkeeping fields are defined by [RFC-0000:C-PHASE-LIFECYCLE](../rfc/RFC-0000.md#rfc-0000c-phase-lifecycle). When an RFC has a stored amendment signature and its amendment content differs from that signature, `advance` MUST reject phase progression until the amendment is released by a version bump. +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). + +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`. + +Changelog-only updates MUST preserve the current phase. Changelog-only updates MUST preserve the current signature. Establishing or replacing the signature while sealing a `spec` version MUST preserve the selected RFC version. + +Amendment content and bookkeeping fields are defined by [RFC-0000:C-PHASE-LIFECYCLE](../rfc/RFC-0000.md#rfc-0000c-phase-lifecycle). **Rationale:** -Scoping phase to the current RFC version preserves ordered specification, implementation, and testing gates while allowing the RFC itself to evolve through versioned amendments. +Scoping phase to the current RFC version preserves ordered specification, implementation, and testing gates while allowing the RFC itself to evolve through versioned amendments. Treating `spec` as the current version's mutable authoring boundary avoids version inflation, while sealing at `impl` gives implementation a precise contract. > **Tags:** `lifecycle` @@ -198,29 +204,30 @@ Transition-time checks ensure that a newly selected replacement is in effect. Pe Certain transitions have additional gate conditions beyond the state machine rules. -**Work Item → done:** -1. The work item MUST have at least one acceptance criterion defined -2. All acceptance criteria MUST have status "done" or "cancelled" (no "pending") -3. Every guard named in the work item's `verification.required_guards` MUST pass or be explicitly waived with a reason -4. If project verification is enabled, every project-level default guard MUST also pass or be explicitly waived with a reason +**Work Item -> done:** +1. The work item MUST have at least one acceptance criterion defined. +2. All acceptance criteria MUST have status `done` or `cancelled`. +3. Every guard named in the work item's `verification.required_guards` MUST pass or be explicitly waived with a reason. +4. If project verification is enabled, every project-level default guard MUST also pass or be explicitly waived with a reason. The effective required verification guards are the union of the work item's `verification.required_guards` and, only when project verification is enabled, the project's configured default guards. Only guards covered by explicit waivers are removed from that set. Rationale: Prevents marking work as complete without defined success criteria or executable completion proof. -**RFC draft → normative:** -- No additional gates (policy decision, not structural) +**RFC draft -> normative:** +- No additional gates (policy decision, not structural). -**RFC phase spec → impl:** -- RFC status SHOULD be normative (warning if draft) +**RFC phase spec -> impl:** +- RFC status MUST be normative. +- Every clause MUST have a resolved `since` version under [RFC-0000:C-CLAUSE-DEF](../rfc/RFC-0000.md#rfc-0000c-clause-def). -Rationale: Implementation should follow finalized specification. +Rationale: Implementation must follow a ratified and version-resolved specification. -**RFC phase impl → test:** -- No additional gates +**RFC phase impl -> test:** +- No additional gates. -**RFC phase test → stable:** -- No additional gates (assumes tests are passing externally) +**RFC phase test -> stable:** +- No additional gates (assumes tests are passing externally). Future gates MAY be added via RFC amendment, but MUST NOT break existing valid workflows. Project verification MUST default to disabled when the project config does not opt into it. @@ -232,6 +239,14 @@ Future gates MAY be added via RFC amendment, but MUST NOT break existing valid w ## Changelog +### v0.6.0 (2026-07-16) + +Align RFC phase gates with current-version sealing + +#### Changed + +- Required normative status and resolved Clause versions before implementation entry + ### v0.5.0 (2026-07-15) Scope terminal states to versions and releases diff --git a/docs/rfc/RFC-0002.md b/docs/rfc/RFC-0002.md index a82f4996..697dd9a1 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.12.1 | **Status:** normative | **Phase:** stable +> **Version:** 0.13.0 | **Status:** normative | **Phase:** stable --- @@ -279,57 +279,94 @@ Resource-specific lifecycle and constrained-mutation operations implement state **RFC Lifecycle Operations:** 1. `govctl rfc finalize normative` - - Implements the draft → normative transition in [RFC-0001:C-RFC-STATUS](../rfc/RFC-0001.md#rfc-0001c-rfc-status) + - Implements the draft -> normative transition in [RFC-0001:C-RFC-STATUS](../rfc/RFC-0001.md#rfc-0001c-rfc-status). + - Assigns the RFC's current version to pending clauses as defined by [RFC-0000:C-CLAUSE-DEF](../rfc/RFC-0000.md#rfc-0000c-clause-def). 2. `govctl rfc deprecate ` - - Implements the normative → deprecated transition in [RFC-0001:C-RFC-STATUS](../rfc/RFC-0001.md#rfc-0001c-rfc-status) + - Implements the normative -> deprecated transition in [RFC-0001:C-RFC-STATUS](../rfc/RFC-0001.md#rfc-0001c-rfc-status). 3. `govctl rfc advance ` - - Implements [RFC-0001:C-RFC-PHASE](../rfc/RFC-0001.md#rfc-0001c-rfc-phase) transitions - - Forward-only phase progression within the current RFC version - - MUST reject phase progression while a stored amendment signature differs from current amendment content as defined by [RFC-0000:C-PHASE-LIFECYCLE](../rfc/RFC-0000.md#rfc-0000c-phase-lifecycle) + - Implements [RFC-0001:C-RFC-PHASE](../rfc/RFC-0001.md#rfc-0001c-rfc-phase) transitions. + - Phase progression MUST be forward-only within the current RFC version. + - 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 later phase transition MUST reject amendment content that differs from the stored signature. 4. `govctl rfc bump -m ` - - Version bumping per semantic versioning - - Updates changelog automatically - - Optional: `--change ` for additional changelog entries - - MUST reject a version bump when the RFC has a stored amendment signature and no amendment content has changed since that signature - - MUST exclude RFC `version`, `phase`, `changelog`, and `signature` fields from amendment comparison - - MUST set phase to `spec` when the bump releases a detected RFC or clause content amendment - - MAY establish a baseline amendment signature for RFCs that do not yet have one - - MUST preserve phase when establishing a baseline amendment signature without a detected content amendment - - `--change` alone MUST remain changelog-only - - `--change` alone MUST preserve phase - - `--change` alone MUST NOT make a later version bump valid - -5. `govctl rfc supersede --by ` - - Marks RFC as superseded by another RFC - - Both RFCs must exist + - Version bumping MUST follow semantic versioning. + - A version bump MUST update the changelog automatically. + - A version bump MAY include `--change ` for additional changelog entries. + - A version bump MUST be rejected when the RFC has a stored amendment signature and no amendment content has changed since that signature. + - 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. + - `--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. + - `--change` without a bump level MUST preserve the current changelog date. + - `--change` without a bump level MUST preserve phase. + - `--change` without a bump level MUST preserve the amendment signature. + - `--change` without a bump level MUST NOT make a later version bump valid. + - A change prefixed by `add:`, `change:`, `deprecate:`, `remove:`, `fix:`, or `security:` MUST be appended to the corresponding changelog category. + - A change without a category prefix MUST be appended to `added`. + - An unknown category prefix MUST be rejected. + +5. RFC current-version changelog access + - `govctl rfc get changelog` MUST expose only the unique changelog entry whose version matches the RFC's current version. + - The field result MUST be one object rather than an array. + - The object MUST contain `version`, `date`, `summary`, `added`, `changed`, `deprecated`, `removed`, `fixed`, and `security` fields. + - `summary` MAY be a string or null. + - Each categorized field MUST be an array of strings. + - Full-resource retrieval and RFC rendering MAY include historical changelog entries. + - Indexed changelog get paths MUST be rejected. + - Nested changelog get paths MUST be rejected. + - `govctl rfc edit changelog.summary --set ` MUST update the current entry's summary. + - `govctl rfc edit changelog. --add ` MUST append to one of `added`, `changed`, `deprecated`, `removed`, `fixed`, or `security`. + - `govctl rfc edit changelog.[] --remove` MUST remove an indexed item from the current category. + - Current-entry resolution MUST match by version. + - Current-entry resolution MUST NOT depend on changelog array position. + - Current-changelog operations MUST reject zero or multiple current entries without mutation. + - Changelog editing MUST reject historical entry selection. + - Changelog editing MUST reject changelog `version` and `date` paths. + - RFC editing MUST reject the top-level RFC `version` path. + - Changelog editing MUST preserve the RFC version. + - Changelog editing MUST preserve phase. + - Changelog editing MUST preserve the amendment signature. + - Changelog editing MUST preserve the current changelog date. + +6. `govctl rfc supersede --by ` + - Marks RFC as superseded by another RFC. + - Both RFCs must exist. **ADR Lifecycle Operations:** 1. `govctl adr accept ` - - Implements [RFC-0001:C-ADR-STATUS](../rfc/RFC-0001.md#rfc-0001c-adr-status) proposed → accepted + - Implements [RFC-0001:C-ADR-STATUS](../rfc/RFC-0001.md#rfc-0001c-adr-status) proposed -> accepted. 2. `govctl adr reject ` - - Implements [RFC-0001:C-ADR-STATUS](../rfc/RFC-0001.md#rfc-0001c-adr-status) proposed → rejected + - Implements [RFC-0001:C-ADR-STATUS](../rfc/RFC-0001.md#rfc-0001c-adr-status) proposed -> rejected. 3. `govctl adr supersede --by ` - - Implements [RFC-0001:C-ADR-STATUS](../rfc/RFC-0001.md#rfc-0001c-adr-status) accepted → superseded - - Both ADRs must exist + - Implements [RFC-0001:C-ADR-STATUS](../rfc/RFC-0001.md#rfc-0001c-adr-status) accepted -> superseded. + - Both ADRs must exist. **Work Lifecycle Operations:** 1. `govctl work move ` - - Implements [RFC-0001:C-WORK-STATUS](../rfc/RFC-0001.md#rfc-0001c-work-status) transitions - - Validates gate conditions when entering `done` - - MUST reject done → active when any release references the Work Item - - queue → active MUST set `started` when it is absent - - active → done MUST set `completed` - - active → cancelled MUST set `completed` - - done → active MUST preserve `started` - - done → active MUST remove `completed` - - done → active MUST NOT mutate loop state or round artifacts governed by [RFC-0006:C-WORK-ITEM-INTERACTION](../rfc/RFC-0006.md#rfc-0006c-work-item-interaction) + - Implements [RFC-0001:C-WORK-STATUS](../rfc/RFC-0001.md#rfc-0001c-work-status) transitions. + - Validates gate conditions when entering `done`. + - MUST reject done -> active when any release references the Work Item. + - queue -> active MUST set `started` when it is absent. + - active -> done MUST set `completed`. + - active -> cancelled MUST set `completed`. + - done -> active MUST preserve `started`. + - done -> active MUST remove `completed`. + - done -> active MUST NOT mutate loop state or round artifacts governed by [RFC-0006:C-WORK-ITEM-INTERACTION](../rfc/RFC-0006.md#rfc-0006c-work-item-interaction). **Release Lifecycle Operations:** @@ -339,34 +376,37 @@ Resource-specific lifecycle and constrained-mutation operations implement state **Clause Lifecycle Operations:** -1. `govctl clause deprecate ` - - Implements [RFC-0001:C-CLAUSE-STATUS](../rfc/RFC-0001.md#rfc-0001c-clause-status) active → deprecated +1. `govctl clause new ` + - MUST assign or defer `since` according to the parent RFC status and phase rules in [RFC-0000:C-CLAUSE-DEF](../rfc/RFC-0000.md#rfc-0000c-clause-def). -2. `govctl clause supersede <id> --by <replacement-id>` - - Implements [RFC-0001:C-CLAUSE-STATUS](../rfc/RFC-0001.md#rfc-0001c-clause-status) active/deprecated → superseded - - Both clauses must exist +2. `govctl clause deprecate <id>` + - Implements [RFC-0001:C-CLAUSE-STATUS](../rfc/RFC-0001.md#rfc-0001c-clause-status) active -> deprecated. + +3. `govctl clause supersede <id> --by <replacement-id>` + - Implements [RFC-0001:C-CLAUSE-STATUS](../rfc/RFC-0001.md#rfc-0001c-clause-status) active/deprecated -> superseded. + - Both clauses must exist. **Consistency Requirements:** -1. All lifecycle operations MUST validate transitions per [RFC-0001](../rfc/RFC-0001.md) or their resource definition -2. All lifecycle operations MUST update timestamp fields where applicable -3. Except for the rollback-failure path in requirement 4, a lifecycle invocation that handles an error and completes through govctl's normal error path with a non-zero exit status MUST restore every governed artifact changed by that invocation byte-for-byte before returning -4. If an I/O or storage error prevents complete restoration, the invocation MUST return a non-zero exit status -5. The rollback-failure diagnostic MUST contain the term `rollback` -6. The rollback-failure diagnostic MUST state that governed-artifact restoration may be incomplete -7. The restoration baseline is the artifact content observed after the invocation acquires its exclusive write scope under [RFC-0004:C-SCOPE](../rfc/RFC-0004.md#rfc-0004c-scope) and before its first governed-artifact mutation -8. Unexpected process termination before normal error completion, host failure, and the rollback-failure path in requirement 4 are outside the lifecycle atomicity guarantee -9. All lifecycle operations MUST support global `--dry-run` flag -10. Invalid transitions MUST error with clear explanation of valid transitions +1. All lifecycle operations MUST validate transitions per [RFC-0001](../rfc/RFC-0001.md) or their resource definition. +2. All lifecycle operations MUST update timestamp fields where applicable. +3. Except for the rollback-failure path in requirement 4, a lifecycle invocation that handles an error and completes through govctl's normal error path with a non-zero exit status MUST restore every governed artifact changed by that invocation byte-for-byte before returning. +4. If an I/O or storage error prevents complete restoration, the invocation MUST return a non-zero exit status. +5. The rollback-failure diagnostic MUST contain the term `rollback`. +6. The rollback-failure diagnostic MUST state that governed-artifact restoration may be incomplete. +7. The restoration baseline is the artifact content observed after the invocation acquires its exclusive write scope under [RFC-0004:C-SCOPE](../rfc/RFC-0004.md#rfc-0004c-scope) and before its first governed-artifact mutation. +8. Unexpected process termination before normal error completion, host failure, and the rollback-failure path in requirement 4 are outside the lifecycle atomicity guarantee. +9. All lifecycle operations MUST support global `--dry-run` flag. +10. Invalid transitions MUST error with clear explanation of valid transitions. **Rationale:** Lifecycle operations are resource-specific because: -- RFCs have status AND phase (two dimensions of state) -- ADRs can be rejected (not applicable to RFCs) -- Work Items have gate conditions (acceptance criteria) -- Release correction has a newest-entry and expected-version boundary -- Each resource has different valid transitions +- RFCs have status AND phase (two dimensions of state). +- ADRs can be rejected (not applicable to RFCs). +- Work Items have gate conditions (acceptance criteria). +- Release correction has a newest-entry and expected-version boundary. +- Each resource has different valid transitions. Scoping these operations to resources makes their applicability explicit and prevents confusion. Lifecycle atomicity defines the byte content observed when an invocation completes through its normal error path; it does not require a crash-recovery subsystem for the flat-file artifact store. @@ -772,6 +812,14 @@ Search is discovery across the governance corpus, not a resource-specific CRUD o ## Changelog +### v0.13.0 (2026-07-16) + +Specify current-version changelog editing and RFC sealing + +#### Added + +- Defined current changelog access and in-version spec authoring lifecycle operations + ### v0.12.1 (2026-07-15) Restore stable release creation syntax diff --git a/gov/rfc/RFC-0000/clauses/C-CLAUSE-DEF.toml b/gov/rfc/RFC-0000/clauses/C-CLAUSE-DEF.toml index e548675c..83a2eae8 100644 --- a/gov/rfc/RFC-0000/clauses/C-CLAUSE-DEF.toml +++ b/gov/rfc/RFC-0000/clauses/C-CLAUSE-DEF.toml @@ -30,5 +30,17 @@ 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. + +Clause creation and version assignment MUST follow these rules: +- A new clause in a draft RFC MUST be pending. +- Finalizing a draft RFC as normative MUST assign the RFC's current version to every pending clause. +- A new clause in a normative RFC whose phase is `spec` MUST set `since` to the RFC's current version. +- A new clause in a normative RFC whose phase is `impl`, `test`, or `stable` MUST be pending. +- A version bump MUST assign the new RFC version to every pending clause that it releases. +- Creating a clause in a deprecated RFC MUST be rejected. + +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:** -This aligns clause storage with RFC, ADR, Work Item, and Release storage while removing the legacy JSON compatibility path from normal operation.""" +A clause can record its version immediately only after that target version is known. Initial draft clauses receive a version at finalization, amendments created outside `spec` receive a version at 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.""" diff --git a/gov/rfc/RFC-0000/clauses/C-PHASE-LIFECYCLE.toml b/gov/rfc/RFC-0000/clauses/C-PHASE-LIFECYCLE.toml index 185d57e7..50df0e94 100644 --- a/gov/rfc/RFC-0000/clauses/C-PHASE-LIFECYCLE.toml +++ b/gov/rfc/RFC-0000/clauses/C-PHASE-LIFECYCLE.toml @@ -15,27 +15,35 @@ tags = [ text = """ RFC phase describes the current RFC version and follows this lifecycle within that version: - spec → impl → test → stable + spec -> impl -> test -> stable -**spec**: Defining the current version. No implementation work permitted. +**spec**: Authoring the current version candidate. Implementation work against that candidate MUST NOT begin. -**impl**: Building what the current version specifies. +**impl**: Building what the sealed current version specifies. -**test**: Verifying implementation against the current version. +**test**: Verifying implementation against the sealed current version. -**stable**: Implementation and tests for the current version are complete. +**stable**: Implementation and tests for the sealed current version are complete. Phase rules: -- Phases within one version MUST proceed in order; skipping is FORBIDDEN -- Each phase transition requires the previous phase gate to pass -- `stable` is terminal for one RFC version, not for the RFC across later version bumps -- A version bump that releases a detected content amendment MUST start the new version at `spec` -- Changelog-only updates and signature-baseline bookkeeping without a detected content amendment MUST preserve phase -- Phase progression MUST be rejected while a detected content amendment remains unversioned -- draft + stable is FORBIDDEN (cannot stabilize unratified work) -- deprecated + impl/test is FORBIDDEN (no new work on deprecated specs) - -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 bump bookkeeping and MUST be excluded from amendment comparison. An RFC without a stored amendment signature has no comparison baseline; establishing its first baseline MUST NOT by itself be treated as a detected content amendment. +- Phases within one version MUST proceed in order. +- 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 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 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 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. Defining the comparison surface prevents phase progression and release 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.""" diff --git a/gov/rfc/RFC-0000/clauses/C-RFC-DEF.toml b/gov/rfc/RFC-0000/clauses/C-RFC-DEF.toml index ed20d4e7..212ee8be 100644 --- a/gov/rfc/RFC-0000/clauses/C-RFC-DEF.toml +++ b/gov/rfc/RFC-0000/clauses/C-RFC-DEF.toml @@ -27,7 +27,13 @@ Format evolution is tracked by the project-level `[schema] version` in `gov/conf Entries in `sections[].clauses` MUST reference clause TOML files by relative path (for example `clauses/C-EXAMPLE.toml`). +The RFC `version` field is lifecycle-owned. Resource editing operations MUST NOT modify the RFC `version` field. + +An RFC MUST have exactly one current changelog entry whose `version` equals the RFC's current version. Other changelog entries are historical versions. Validation MUST reject an RFC with zero current changelog entries. Validation MUST reject an RFC with multiple current changelog entries. Current-changelog field operations MUST reject either invalid state without mutation. + +The current entry's summary and categorized changes MAY be corrected without changing the RFC version or phase. Changelog `version` fields are lifecycle-owned. Changelog `date` fields are lifecycle-owned. Resource editing operations MUST NOT modify either lifecycle-owned field. Resource editing operations MUST NOT modify any historical changelog entry. + Implementations MUST reject legacy RFC JSON storage files (`rfc.json`) during normal operations. The diagnostic MUST instruct users to migrate those repositories with a govctl version earlier than 0.9 before upgrading to a TOML-only govctl release. **Rationale:** -This removes the last storage-format split from RFC handling while preserving a clear upgrade path for repositories that still contain legacy JSON sources.""" +The current changelog entry describes the current RFC version and may need refinement while that version is authored. Matching by version avoids relying on array position. Keeping version identity, dates, and historical entries lifecycle-owned preserves version provenance.""" diff --git a/gov/rfc/RFC-0000/clauses/C-STATUS-LIFECYCLE.toml b/gov/rfc/RFC-0000/clauses/C-STATUS-LIFECYCLE.toml index 5de962d9..600babed 100644 --- a/gov/rfc/RFC-0000/clauses/C-STATUS-LIFECYCLE.toml +++ b/gov/rfc/RFC-0000/clauses/C-STATUS-LIFECYCLE.toml @@ -6,21 +6,27 @@ title = "RFC Status Lifecycle" kind = "normative" status = "active" since = "1.0.0" -tags = ["core", "lifecycle"] +tags = [ + "core", + "lifecycle", +] [content] text = """ RFC status follows this lifecycle: - draft → normative → deprecated + draft -> normative -> deprecated **draft**: Under discussion. Implementation MUST NOT depend on draft RFCs. -**normative**: Frozen and ratified. Implementation MUST conform to normative RFCs. +**normative**: The RFC lineage is ratified. A normative version in `impl`, `test`, or `stable` is sealed, and implementation MUST conform to that sealed version. A normative version in `spec` is a controlled authoring candidate and MUST NOT be used as an implementation conformance baseline. The most recently sealed version remains the implementation baseline until the current `spec` version enters `impl`. **deprecated**: Superseded or obsolete. Implementation SHOULD migrate away. Transition rules: -- draft → normative: Requires explicit finalization -- normative → deprecated: Requires a superseding RFC or explicit deprecation -- Reverse transitions are FORBIDDEN""" +- The draft -> normative transition MUST require explicit finalization. +- The normative -> deprecated transition MUST require a superseding RFC or explicit deprecation. +- Reverse status transitions MUST be rejected. + +**Rationale:** +Normative status ratifies the RFC lineage, while phase identifies whether its current version is still being authored or has been sealed for implementation. This distinction permits controlled current-version authoring without presenting mutable `spec` content as a binding implementation contract.""" diff --git a/gov/rfc/RFC-0000/rfc.toml b/gov/rfc/RFC-0000/rfc.toml index 03b466d4..fdf78802 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.5.0" +version = "1.6.0" status = "normative" phase = "stable" owners = ["@govctl-org"] created = "2026-01-17" -updated = "2026-07-15" +updated = "2026-07-16" tags = [ "core", "schema", "validation", "lifecycle", ] -signature = "a8dbcc3587e2f2f2b548c3cf33a3a684c0f5b329c12eeb8433b4f491dc6541d7" +signature = "6af0805b1349bc7e13119ba6912cac5a7165cc024dc8fef8712f845e0831d7b1" [[sections]] title = "Summary" @@ -53,6 +53,12 @@ clauses = ["clauses/C-RELEASE-DEF.toml"] title = "Verification Guard Specification" clauses = ["clauses/C-GUARD-DEF.toml"] +[[changelog]] +version = "1.6.0" +date = "2026-07-16" +notes = "Define current-version RFC authoring and changelog invariants" +changed = ["Defined spec-phase sealing, Clause version assignment, and current changelog ownership"] + [[changelog]] version = "1.5.0" date = "2026-07-15" diff --git a/gov/rfc/RFC-0001/clauses/C-GATE-CONDITIONS.toml b/gov/rfc/RFC-0001/clauses/C-GATE-CONDITIONS.toml index ca1e057a..bf198827 100644 --- a/gov/rfc/RFC-0001/clauses/C-GATE-CONDITIONS.toml +++ b/gov/rfc/RFC-0001/clauses/C-GATE-CONDITIONS.toml @@ -6,34 +6,38 @@ title = "Transition Gate Conditions" kind = "normative" status = "active" since = "0.1.0" -tags = ["lifecycle", "validation"] +tags = [ + "lifecycle", + "validation", +] [content] text = """ Certain transitions have additional gate conditions beyond the state machine rules. -**Work Item → done:** -1. The work item MUST have at least one acceptance criterion defined -2. All acceptance criteria MUST have status "done" or "cancelled" (no "pending") -3. Every guard named in the work item's `verification.required_guards` MUST pass or be explicitly waived with a reason -4. If project verification is enabled, every project-level default guard MUST also pass or be explicitly waived with a reason +**Work Item -> done:** +1. The work item MUST have at least one acceptance criterion defined. +2. All acceptance criteria MUST have status `done` or `cancelled`. +3. Every guard named in the work item's `verification.required_guards` MUST pass or be explicitly waived with a reason. +4. If project verification is enabled, every project-level default guard MUST also pass or be explicitly waived with a reason. The effective required verification guards are the union of the work item's `verification.required_guards` and, only when project verification is enabled, the project's configured default guards. Only guards covered by explicit waivers are removed from that set. Rationale: Prevents marking work as complete without defined success criteria or executable completion proof. -**RFC draft → normative:** -- No additional gates (policy decision, not structural) +**RFC draft -> normative:** +- No additional gates (policy decision, not structural). -**RFC phase spec → impl:** -- RFC status SHOULD be normative (warning if draft) +**RFC phase spec -> impl:** +- RFC status MUST be normative. +- Every clause MUST have a resolved `since` version under [[RFC-0000:C-CLAUSE-DEF]]. -Rationale: Implementation should follow finalized specification. +Rationale: Implementation must follow a ratified and version-resolved specification. -**RFC phase impl → test:** -- No additional gates +**RFC phase impl -> test:** +- No additional gates. -**RFC phase test → stable:** -- No additional gates (assumes tests are passing externally) +**RFC phase test -> stable:** +- No additional gates (assumes tests are passing externally). Future gates MAY be added via RFC amendment, but MUST NOT break existing valid workflows. Project verification MUST default to disabled when the project config does not opt into it.""" diff --git a/gov/rfc/RFC-0001/clauses/C-RFC-PHASE.toml b/gov/rfc/RFC-0001/clauses/C-RFC-PHASE.toml index 0fc84a7d..e1f92758 100644 --- a/gov/rfc/RFC-0001/clauses/C-RFC-PHASE.toml +++ b/gov/rfc/RFC-0001/clauses/C-RFC-PHASE.toml @@ -12,26 +12,32 @@ tags = ["lifecycle"] text = """ An RFC MUST have exactly one of the following phase values. The phase describes conformance work for the RFC's current version: -1. **spec** — Specification phase. The RFC text is being written. No implementation work. -2. **impl** — Implementation phase. Code is being written to conform to the current RFC version. -3. **test** — Testing phase. Implementation is complete; tests are being written and validated against the current RFC version. -4. **stable** — Stable phase. Implementation and tests for the current RFC version are complete. +1. **spec** - Specification phase. The current version candidate is being written. Implementation work against that candidate is not permitted. +2. **impl** - Implementation phase. Code is being written to conform to the sealed current version. +3. **test** - Testing phase. Implementation is complete; tests are being written and validated against the sealed current version. +4. **stable** - Stable phase. Implementation and tests for the sealed current version are complete. Within one RFC version, valid transitions are forward only via the `advance` command: -- spec → impl -- impl → test -- test → stable +- spec -> impl +- impl -> test +- test -> stable Within one RFC version, the following transitions MUST be rejected: -- Any backward transition (e.g., impl → spec) -- Any skip (e.g., spec → test, spec → stable) -- stable → any (`stable` is terminal for that version) +- Any backward transition (for example, impl -> spec) +- 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 changelog-only update MUST preserve the current phase. Establishing an amendment signature baseline without a detected content amendment MUST also preserve the current phase. +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`. -Amendment content and bookkeeping fields are defined by [[RFC-0000:C-PHASE-LIFECYCLE]]. When an RFC has a stored amendment signature and its amendment content differs from that signature, `advance` MUST reject phase progression until the amendment is released by a version bump. +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]]. + +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`. + +Changelog-only updates MUST preserve the current phase. Changelog-only updates MUST preserve the current signature. Establishing or replacing the signature while sealing a `spec` version MUST preserve the selected RFC version. + +Amendment content and bookkeeping fields are defined by [[RFC-0000:C-PHASE-LIFECYCLE]]. **Rationale:** -Scoping phase to the current RFC version preserves ordered specification, implementation, and testing gates while allowing the RFC itself to evolve through versioned amendments.""" +Scoping phase to the current RFC version preserves ordered specification, implementation, and testing gates while allowing the RFC itself to evolve through versioned amendments. Treating `spec` as the current version's mutable authoring boundary avoids version inflation, while sealing at `impl` gives implementation a precise contract.""" diff --git a/gov/rfc/RFC-0001/rfc.toml b/gov/rfc/RFC-0001/rfc.toml index 365a13f4..83dd1a44 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.5.0" +version = "0.6.0" status = "normative" phase = "stable" owners = ["@govctl-org"] created = "2026-01-17" -updated = "2026-07-15" +updated = "2026-07-16" tags = [ "core", "lifecycle", ] -signature = "589db866e0b760a4885fce03ea0f2f0a01709ea75959eacdf09c32d105f7632f" +signature = "bb15b23a30af637d2a939a84afba34636b7af115de50afcc658ff19f70c0e44a" [[sections]] title = "Summary" @@ -30,6 +30,12 @@ clauses = [ "clauses/C-GATE-CONDITIONS.toml", ] +[[changelog]] +version = "0.6.0" +date = "2026-07-16" +notes = "Align RFC phase gates with current-version sealing" +changed = ["Required normative status and resolved Clause versions before implementation entry"] + [[changelog]] version = "0.5.0" date = "2026-07-15" diff --git a/gov/rfc/RFC-0002/clauses/C-LIFECYCLE-VERBS.toml b/gov/rfc/RFC-0002/clauses/C-LIFECYCLE-VERBS.toml index 3e61eb47..be502988 100644 --- a/gov/rfc/RFC-0002/clauses/C-LIFECYCLE-VERBS.toml +++ b/gov/rfc/RFC-0002/clauses/C-LIFECYCLE-VERBS.toml @@ -18,57 +18,94 @@ Resource-specific lifecycle and constrained-mutation operations implement state **RFC Lifecycle Operations:** 1. `govctl rfc finalize <id> normative` - - Implements the draft → normative transition in [[RFC-0001:C-RFC-STATUS]] + - Implements the draft -> normative transition in [[RFC-0001:C-RFC-STATUS]]. + - Assigns the RFC's current version to pending clauses as defined by [[RFC-0000:C-CLAUSE-DEF]]. 2. `govctl rfc deprecate <id>` - - Implements the normative → deprecated transition in [[RFC-0001:C-RFC-STATUS]] + - Implements the normative -> deprecated transition in [[RFC-0001:C-RFC-STATUS]]. 3. `govctl rfc advance <id> <spec|impl|test|stable>` - - Implements [[RFC-0001:C-RFC-PHASE]] transitions - - Forward-only phase progression within the current RFC version - - MUST reject phase progression while a stored amendment signature differs from current amendment content as defined by [[RFC-0000:C-PHASE-LIFECYCLE]] + - Implements [[RFC-0001:C-RFC-PHASE]] transitions. + - Phase progression MUST be forward-only within the current RFC version. + - 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 later phase transition MUST reject amendment content that differs from the stored signature. 4. `govctl rfc bump <id> <patch|minor|major> -m <message>` - - Version bumping per semantic versioning - - Updates changelog automatically - - Optional: `--change <description>` for additional changelog entries - - MUST reject a version bump when the RFC has a stored amendment signature and no amendment content has changed since that signature - - MUST exclude RFC `version`, `phase`, `changelog`, and `signature` fields from amendment comparison - - MUST set phase to `spec` when the bump releases a detected RFC or clause content amendment - - MAY establish a baseline amendment signature for RFCs that do not yet have one - - MUST preserve phase when establishing a baseline amendment signature without a detected content amendment - - `--change` alone MUST remain changelog-only - - `--change` alone MUST preserve phase - - `--change` alone MUST NOT make a later version bump valid - -5. `govctl rfc supersede <id> --by <replacement-id>` - - Marks RFC as superseded by another RFC - - Both RFCs must exist + - Version bumping MUST follow semantic versioning. + - A version bump MUST update the changelog automatically. + - A version bump MAY include `--change <description>` for additional changelog entries. + - A version bump MUST be rejected when the RFC has a stored amendment signature and no amendment content has changed since that signature. + - 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. + - `--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. + - `--change` without a bump level MUST preserve the current changelog date. + - `--change` without a bump level MUST preserve phase. + - `--change` without a bump level MUST preserve the amendment signature. + - `--change` without a bump level MUST NOT make a later version bump valid. + - A change prefixed by `add:`, `change:`, `deprecate:`, `remove:`, `fix:`, or `security:` MUST be appended to the corresponding changelog category. + - A change without a category prefix MUST be appended to `added`. + - An unknown category prefix MUST be rejected. + +5. RFC current-version changelog access + - `govctl rfc get <id> changelog` MUST expose only the unique changelog entry whose version matches the RFC's current version. + - The field result MUST be one object rather than an array. + - The object MUST contain `version`, `date`, `summary`, `added`, `changed`, `deprecated`, `removed`, `fixed`, and `security` fields. + - `summary` MAY be a string or null. + - Each categorized field MUST be an array of strings. + - Full-resource retrieval and RFC rendering MAY include historical changelog entries. + - Indexed changelog get paths MUST be rejected. + - Nested changelog get paths MUST be rejected. + - `govctl rfc edit <id> changelog.summary --set <value>` MUST update the current entry's summary. + - `govctl rfc edit <id> changelog.<category> --add <value>` MUST append to one of `added`, `changed`, `deprecated`, `removed`, `fixed`, or `security`. + - `govctl rfc edit <id> changelog.<category>[<index>] --remove` MUST remove an indexed item from the current category. + - Current-entry resolution MUST match by version. + - Current-entry resolution MUST NOT depend on changelog array position. + - Current-changelog operations MUST reject zero or multiple current entries without mutation. + - Changelog editing MUST reject historical entry selection. + - Changelog editing MUST reject changelog `version` and `date` paths. + - RFC editing MUST reject the top-level RFC `version` path. + - Changelog editing MUST preserve the RFC version. + - Changelog editing MUST preserve phase. + - Changelog editing MUST preserve the amendment signature. + - Changelog editing MUST preserve the current changelog date. + +6. `govctl rfc supersede <id> --by <replacement-id>` + - Marks RFC as superseded by another RFC. + - Both RFCs must exist. **ADR Lifecycle Operations:** 1. `govctl adr accept <id>` - - Implements [[RFC-0001:C-ADR-STATUS]] proposed → accepted + - Implements [[RFC-0001:C-ADR-STATUS]] proposed -> accepted. 2. `govctl adr reject <id>` - - Implements [[RFC-0001:C-ADR-STATUS]] proposed → rejected + - Implements [[RFC-0001:C-ADR-STATUS]] proposed -> rejected. 3. `govctl adr supersede <id> --by <replacement-id>` - - Implements [[RFC-0001:C-ADR-STATUS]] accepted → superseded - - Both ADRs must exist + - Implements [[RFC-0001:C-ADR-STATUS]] accepted -> superseded. + - Both ADRs must exist. **Work Lifecycle Operations:** 1. `govctl work move <id> <queue|active|done|cancelled>` - - Implements [[RFC-0001:C-WORK-STATUS]] transitions - - Validates gate conditions when entering `done` - - MUST reject done → active when any release references the Work Item - - queue → active MUST set `started` when it is absent - - active → done MUST set `completed` - - active → cancelled MUST set `completed` - - done → active MUST preserve `started` - - done → active MUST remove `completed` - - done → active MUST NOT mutate loop state or round artifacts governed by [[RFC-0006:C-WORK-ITEM-INTERACTION]] + - Implements [[RFC-0001:C-WORK-STATUS]] transitions. + - Validates gate conditions when entering `done`. + - MUST reject done -> active when any release references the Work Item. + - queue -> active MUST set `started` when it is absent. + - active -> done MUST set `completed`. + - active -> cancelled MUST set `completed`. + - done -> active MUST preserve `started`. + - done -> active MUST remove `completed`. + - done -> active MUST NOT mutate loop state or round artifacts governed by [[RFC-0006:C-WORK-ITEM-INTERACTION]]. **Release Lifecycle Operations:** @@ -78,33 +115,36 @@ Resource-specific lifecycle and constrained-mutation operations implement state **Clause Lifecycle Operations:** -1. `govctl clause deprecate <id>` - - Implements [[RFC-0001:C-CLAUSE-STATUS]] active → deprecated +1. `govctl clause new <id> <title>` + - MUST assign or defer `since` according to the parent RFC status and phase rules in [[RFC-0000:C-CLAUSE-DEF]]. -2. `govctl clause supersede <id> --by <replacement-id>` - - Implements [[RFC-0001:C-CLAUSE-STATUS]] active/deprecated → superseded - - Both clauses must exist +2. `govctl clause deprecate <id>` + - Implements [[RFC-0001:C-CLAUSE-STATUS]] active -> deprecated. + +3. `govctl clause supersede <id> --by <replacement-id>` + - Implements [[RFC-0001:C-CLAUSE-STATUS]] active/deprecated -> superseded. + - Both clauses must exist. **Consistency Requirements:** -1. All lifecycle operations MUST validate transitions per [[RFC-0001]] or their resource definition -2. All lifecycle operations MUST update timestamp fields where applicable -3. Except for the rollback-failure path in requirement 4, a lifecycle invocation that handles an error and completes through govctl's normal error path with a non-zero exit status MUST restore every governed artifact changed by that invocation byte-for-byte before returning -4. If an I/O or storage error prevents complete restoration, the invocation MUST return a non-zero exit status -5. The rollback-failure diagnostic MUST contain the term `rollback` -6. The rollback-failure diagnostic MUST state that governed-artifact restoration may be incomplete -7. The restoration baseline is the artifact content observed after the invocation acquires its exclusive write scope under [[RFC-0004:C-SCOPE]] and before its first governed-artifact mutation -8. Unexpected process termination before normal error completion, host failure, and the rollback-failure path in requirement 4 are outside the lifecycle atomicity guarantee -9. All lifecycle operations MUST support global `--dry-run` flag -10. Invalid transitions MUST error with clear explanation of valid transitions +1. All lifecycle operations MUST validate transitions per [[RFC-0001]] or their resource definition. +2. All lifecycle operations MUST update timestamp fields where applicable. +3. Except for the rollback-failure path in requirement 4, a lifecycle invocation that handles an error and completes through govctl's normal error path with a non-zero exit status MUST restore every governed artifact changed by that invocation byte-for-byte before returning. +4. If an I/O or storage error prevents complete restoration, the invocation MUST return a non-zero exit status. +5. The rollback-failure diagnostic MUST contain the term `rollback`. +6. The rollback-failure diagnostic MUST state that governed-artifact restoration may be incomplete. +7. The restoration baseline is the artifact content observed after the invocation acquires its exclusive write scope under [[RFC-0004:C-SCOPE]] and before its first governed-artifact mutation. +8. Unexpected process termination before normal error completion, host failure, and the rollback-failure path in requirement 4 are outside the lifecycle atomicity guarantee. +9. All lifecycle operations MUST support global `--dry-run` flag. +10. Invalid transitions MUST error with clear explanation of valid transitions. **Rationale:** Lifecycle operations are resource-specific because: -- RFCs have status AND phase (two dimensions of state) -- ADRs can be rejected (not applicable to RFCs) -- Work Items have gate conditions (acceptance criteria) -- Release correction has a newest-entry and expected-version boundary -- Each resource has different valid transitions +- RFCs have status AND phase (two dimensions of state). +- ADRs can be rejected (not applicable to RFCs). +- Work Items have gate conditions (acceptance criteria). +- Release correction has a newest-entry and expected-version boundary. +- Each resource has different valid transitions. Scoping these operations to resources makes their applicability explicit and prevents confusion. Lifecycle atomicity defines the byte content observed when an invocation completes through its normal error path; it does not require a crash-recovery subsystem for the flat-file artifact store.""" diff --git a/gov/rfc/RFC-0002/rfc.toml b/gov/rfc/RFC-0002/rfc.toml index 1932acae..2e1fb150 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.12.1" +version = "0.13.0" status = "normative" phase = "stable" owners = ["@govctl-org"] created = "2026-01-19" -updated = "2026-07-15" +updated = "2026-07-16" tags = [ "cli", "editing", @@ -16,7 +16,7 @@ tags = [ "validation", "release", ] -signature = "373fa81463fff75133a749849c8cbb8038507b84e55294bb8e29ebe56607dc82" +signature = "8242e784a13b698482afb54a215c799dc65bb36fb705f84fb33e563cb997b475" [[sections]] title = "Summary" @@ -36,6 +36,12 @@ clauses = [ "clauses/C-SEARCH-COMMAND.toml", ] +[[changelog]] +version = "0.13.0" +date = "2026-07-16" +notes = "Specify current-version changelog editing and RFC sealing" +added = ["Defined current changelog access and in-version spec authoring lifecycle operations"] + [[changelog]] version = "0.12.1" date = "2026-07-15" diff --git a/gov/schema/edit-ops.json b/gov/schema/edit-ops.json index b89c3b46..e3a42b88 100644 --- a/gov/schema/edit-ops.json +++ b/gov/schema/edit-ops.json @@ -1,5 +1,5 @@ { - "version": 2, + "version": 3, "aliases": [ { "alias": "ac", "canonical": "acceptance_criteria" }, { "alias": "alt", "canonical": "alternatives" }, @@ -653,6 +653,79 @@ } ], "nested_rules": [ + { + "artifact": "rfc", + "root": "changelog", + "content_path": ["changelog"], + "node": { + "kind": "object", + "verbs": ["get"], + "fields": [ + { + "name": "summary", + "node": { + "kind": "scalar", + "verbs": ["set"], + "set_mode": { "type": "string" } + } + }, + { + "name": "added", + "node": { + "kind": "list", + "verbs": ["add", "remove"], + "text_key": null, + "item": { "kind": "scalar", "verbs": ["remove"] } + } + }, + { + "name": "changed", + "node": { + "kind": "list", + "verbs": ["add", "remove"], + "text_key": null, + "item": { "kind": "scalar", "verbs": ["remove"] } + } + }, + { + "name": "deprecated", + "node": { + "kind": "list", + "verbs": ["add", "remove"], + "text_key": null, + "item": { "kind": "scalar", "verbs": ["remove"] } + } + }, + { + "name": "removed", + "node": { + "kind": "list", + "verbs": ["add", "remove"], + "text_key": null, + "item": { "kind": "scalar", "verbs": ["remove"] } + } + }, + { + "name": "fixed", + "node": { + "kind": "list", + "verbs": ["add", "remove"], + "text_key": null, + "item": { "kind": "scalar", "verbs": ["remove"] } + } + }, + { + "name": "security", + "node": { + "kind": "list", + "verbs": ["add", "remove"], + "text_key": null, + "item": { "kind": "scalar", "verbs": ["remove"] } + } + } + ] + } + }, { "artifact": "adr", "root": "alternatives", diff --git a/gov/schema/edit-ops.schema.json b/gov/schema/edit-ops.schema.json index 29951264..3e3f6459 100644 --- a/gov/schema/edit-ops.schema.json +++ b/gov/schema/edit-ops.schema.json @@ -89,7 +89,7 @@ "properties": { "artifact": { "type": "string", - "enum": ["adr", "work", "guard"] + "enum": ["rfc", "adr", "work", "guard"] }, "root": { "type": "string", diff --git a/gov/work/2026-07-16-allow-in-version-rfc-authoring-during-spec.toml b/gov/work/2026-07-16-allow-in-version-rfc-authoring-during-spec.toml new file mode 100644 index 00000000..9ac0ea5d --- /dev/null +++ b/gov/work/2026-07-16-allow-in-version-rfc-authoring-during-spec.toml @@ -0,0 +1,80 @@ +#:schema ../schema/work.schema.json + +[govctl] +id = "WI-2026-07-16-001" +title = "Allow in-version RFC authoring during spec" +status = "done" +created = "2026-07-16" +started = "2026-07-16" +completed = "2026-07-16" +refs = [ + "RFC-0000:C-RFC-DEF", + "RFC-0000:C-PHASE-LIFECYCLE", + "RFC-0001:C-RFC-PHASE", + "RFC-0002:C-LIFECYCLE-VERBS", + "ADR-0004", + "ADR-0016", + "ADR-0031", + "ADR-0051", + "RFC-0000:C-CLAUSE-DEF", + "RFC-0000:C-STATUS-LIFECYCLE", + "RFC-0001:C-GATE-CONDITIONS", +] +tags = [ + "lifecycle", + "editing", + "cli", +] + +[content] +description = "Amend the RFC lifecycle specification and implement the corresponding authoring flow for continuing current-version RFC work in `spec`, sealing version state on entry to `impl`, and maintaining current-version changelog metadata through `rfc edit`." + +[[content.acceptance_criteria]] +text = "Lifecycle RFC clauses document current-version spec authoring, implementation-entry sealing, and current-changelog correction boundaries" +status = "done" +category = "changed" + +[[content.acceptance_criteria]] +text = "RFC and Clause edits made in spec can advance to impl without another version bump" +status = "done" +category = "fixed" + +[[content.acceptance_criteria]] +text = "RFC edit and get paths expose only the changelog entry matching the current RFC version, with summary and category mutations" +status = "done" +category = "added" + +[[content.acceptance_criteria]] +text = "RFC changelog editing rejects historical entries and lifecycle-owned version and date fields" +status = "done" +category = "fixed" + +[[content.acceptance_criteria]] +text = "Change-only rfc bump --change uses current-version resolution and CLI help documents its no-version-bump behavior" +status = "done" +category = "changed" + +[[content.acceptance_criteria]] +text = "0.11.0 release notes document the current spec-phase rebump limitation" +status = "done" +category = "chore" + +[[content.acceptance_criteria]] +text = "Focused lifecycle and edit tests plus project verification guards pass" +status = "done" +category = "chore" + +[[content.acceptance_criteria]] +text = "Clause creation assigns since immediately only for normative spec RFCs, while draft and non-spec amendments remain pending for finalize or bump" +status = "done" +category = "fixed" + +[[content.acceptance_criteria]] +text = "Entering impl seals the current content signature and rejects unresolved pending Clause versions without rewriting them" +status = "done" +category = "fixed" + +[[content.acceptance_criteria]] +text = "AGENTS.md, the spec, gov, and rfc-writer skills, and docs/guide/rfcs.md reflect in-version spec authoring, sealed implementation baselines, and automatic Clause version assignment" +status = "done" +category = "chore" diff --git a/src/cli/resources/rfc.rs b/src/cli/resources/rfc.rs index 280dea1b..aa60b3eb 100644 --- a/src/cli/resources/rfc.rs +++ b/src/cli/resources/rfc.rs @@ -28,7 +28,7 @@ EXAMPLES: /// Get RFC metadata or specific field #[command(after_help = "\ VALID FIELDS: - - title, version, status, phase, owners, refs + - title, version, status, phase, owners, refs, changelog EXAMPLES: govctl rfc get RFC-0001 @@ -67,9 +67,14 @@ NOTES: /// Canonical path-first edit entrypoint #[command(after_help = "\ EXAMPLES: - govctl rfc edit RFC-0001 version --set 1.2.0 + govctl rfc edit RFC-0001 changelog.summary --set \"Clarify retry behavior\" + govctl rfc edit RFC-0001 changelog.fixed --add \"Correct timeout wording\" + govctl rfc edit RFC-0001 changelog.fixed[0] --remove govctl rfc edit RFC-0001 refs --add RFC-0002 - govctl rfc edit RFC-0001 refs[0] --remove + +NOTES: + - Changelog edits apply only to the entry matching the RFC's current version. + - RFC and changelog version/date fields are lifecycle-owned. ")] Edit(CommonEditArgs), /// Set RFC field value @@ -122,10 +127,12 @@ EXAMPLES: #[command(after_help = "\ EXAMPLES: govctl rfc bump RFC-0001 --patch -m \"Clarify examples\" - govctl rfc bump RFC-0001 --minor -c \"changed: New normative clause\" + govctl rfc bump RFC-0001 --minor -m \"Add a normative clause\" -c \"change: Define the new behavior\" + govctl rfc bump RFC-0001 -c \"fix: Correct current-version wording\" NOTES: - - Exactly one of `--patch`, `--minor`, or `--major` must be chosen. + - 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. ")] Bump { diff --git a/src/cmd/edit/add.rs b/src/cmd/edit/add.rs index e7ca5956..000e3cb9 100644 --- a/src/cmd/edit/add.rs +++ b/src/cmd/edit/add.rs @@ -2,7 +2,7 @@ use super::adapter::{ AdrTomlAdapter, ClauseTomlAdapter, GuardTomlAdapter, RfcTomlAdapter, TomlAdapter, WorkTomlAdapter, }; -use super::doc_target::add_doc_simple_list_field; +use super::doc_target::{add_doc_simple_list_field, rfc_changelog}; use super::engine as edit_engine; use super::refs::{is_refs_target, validate_ref_edit}; use super::rules as edit_rules; @@ -182,15 +182,21 @@ pub fn add_to_field(request: AddFieldRequest<'_>) -> DiagnosticResult<Vec<Diagno } WorkTomlAdapter::write(config, &entry, op)?; } - ArtifactType::Rfc => add_doc_simple_list_field::<RfcTomlAdapter>( - config, - id, - target, - value, - op, - ArtifactType::Rfc, - "RFC fields do not support nested paths for add", - )?, + ArtifactType::Rfc => { + if rfc_changelog::is_target(target) { + rfc_changelog::add(config, id, target, value, op)?; + } else { + add_doc_simple_list_field::<RfcTomlAdapter>( + config, + id, + target, + value, + op, + ArtifactType::Rfc, + "RFC fields do not support nested paths for add", + )?; + } + } ArtifactType::Clause => add_doc_simple_list_field::<ClauseTomlAdapter>( config, id, diff --git a/src/cmd/edit/doc_target/mod.rs b/src/cmd/edit/doc_target/mod.rs index a9dc9402..154df530 100644 --- a/src/cmd/edit/doc_target/mod.rs +++ b/src/cmd/edit/doc_target/mod.rs @@ -12,6 +12,7 @@ use crate::config::Config; use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticResult}; use crate::write::WriteOp; +pub(super) mod rfc_changelog; mod set; pub(super) use set::{set_clause_field, set_rfc_field}; diff --git a/src/cmd/edit/doc_target/rfc_changelog.rs b/src/cmd/edit/doc_target/rfc_changelog.rs new file mode 100644 index 00000000..0026955b --- /dev/null +++ b/src/cmd/edit/doc_target/rfc_changelog.rs @@ -0,0 +1,173 @@ +use super::super::adapter::{DocAdapter, RfcTomlAdapter}; +use super::super::engine as edit_engine; +use super::super::matching::MatchOptions; +use super::super::runtime as edit_runtime; +use super::super::target_doc::{NestedGetMode, add_to_target_doc, render_target_from_doc}; +use super::super::target_doc_remove::{notify_removed, remove_target_from_doc}; +use super::{ArtifactType, deserialize_edit_doc, serialize_edit_doc}; +use crate::cmd::output::print_json; +use crate::config::Config; +use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticResult}; +use crate::model::ChangelogEntry; +use crate::write::{WriteOp, current_changelog_entry, current_changelog_entry_mut}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +struct CurrentChangelogDoc { + changelog: CurrentChangelogView, +} + +#[derive(Debug, Serialize, Deserialize)] +struct CurrentChangelogView { + version: String, + date: String, + summary: Option<String>, + added: Vec<String>, + changed: Vec<String>, + deprecated: Vec<String>, + removed: Vec<String>, + fixed: Vec<String>, + security: Vec<String>, +} + +impl From<&ChangelogEntry> for CurrentChangelogView { + fn from(entry: &ChangelogEntry) -> Self { + Self { + version: entry.version.clone(), + date: entry.date.clone(), + summary: entry.notes.clone(), + added: entry.added.clone(), + changed: entry.changed.clone(), + deprecated: entry.deprecated.clone(), + removed: entry.removed.clone(), + fixed: entry.fixed.clone(), + security: entry.security.clone(), + } + } +} + +pub(in crate::cmd::edit) fn is_target(target: &edit_engine::ResolvedTarget) -> bool { + target + .path() + .segments + .first() + .is_some_and(|segment| segment.name == "changelog") +} + +pub(in crate::cmd::edit) fn get( + config: &Config, + id: &str, + target: &edit_engine::ResolvedTarget, +) -> DiagnosticResult<()> { + let loaded = RfcTomlAdapter::load(config, id)?; + let doc = current_changelog_doc(&loaded.data, id)?; + render_target_from_doc(ArtifactType::Rfc, &doc, target, id, NestedGetMode::Allow)?; + + let root_only = target.path().segments.len() == 1 && target.path().segments[0].index.is_none(); + if !root_only { + return Err(Diagnostic::new( + DiagnosticCode::E0804FieldNotEditable, + "RFC changelog get supports only the current-entry root path", + id, + )); + } + + print_json( + doc.get("changelog").unwrap_or(&serde_json::Value::Null), + DiagnosticCode::E0903UnexpectedError, + "Failed to serialize current RFC changelog entry", + id, + ) +} + +pub(in crate::cmd::edit) fn set( + config: &Config, + id: &str, + target: &edit_engine::ResolvedTarget, + value: &str, + op: WriteOp, +) -> DiagnosticResult<()> { + let mut loaded = RfcTomlAdapter::load(config, id)?; + crate::cmd::lifecycle::require_changelog_update_ready(config, &loaded.path, id)?; + let mut doc = current_changelog_doc(&loaded.data, id)?; + edit_runtime::set_nested_field(ArtifactType::Rfc, &mut doc, target.path(), value, id)?; + apply_current_changelog_doc(&mut loaded.data, doc, id)?; + RfcTomlAdapter::write(config, &loaded, op) +} + +pub(in crate::cmd::edit) fn add( + config: &Config, + id: &str, + target: &edit_engine::ResolvedTarget, + value: &str, + op: WriteOp, +) -> DiagnosticResult<()> { + let mut loaded = RfcTomlAdapter::load(config, id)?; + crate::cmd::lifecycle::require_changelog_update_ready(config, &loaded.path, id)?; + let mut doc = current_changelog_doc(&loaded.data, id)?; + add_to_target_doc(ArtifactType::Rfc, &mut doc, target, value, id)?; + apply_current_changelog_doc(&mut loaded.data, doc, id)?; + RfcTomlAdapter::write(config, &loaded, op) +} + +pub(in crate::cmd::edit) fn remove( + config: &Config, + id: &str, + target: &edit_engine::ResolvedTarget, + opts: &MatchOptions, + op: WriteOp, +) -> DiagnosticResult<()> { + if !matches!( + target, + edit_engine::ResolvedTarget::IndexedItem { + origin: edit_engine::TargetOrigin::Nested, + .. + } + ) { + return Err(Diagnostic::new( + DiagnosticCode::E0817PathTypeMismatch, + "RFC changelog removal requires an indexed current-category path", + id, + )); + } + + let mut loaded = RfcTomlAdapter::load(config, id)?; + crate::cmd::lifecycle::require_changelog_update_ready(config, &loaded.path, id)?; + let mut doc = current_changelog_doc(&loaded.data, id)?; + let (display_field, removed) = + remove_target_from_doc(ArtifactType::Rfc, &mut doc, id, target, opts)?; + apply_current_changelog_doc(&mut loaded.data, doc, id)?; + RfcTomlAdapter::write(config, &loaded, op)?; + notify_removed(id, &display_field, &removed, op); + Ok(()) +} + +fn current_changelog_doc( + rfc: &crate::model::RfcSpec, + id: &str, +) -> DiagnosticResult<serde_json::Value> { + let entry = current_changelog_entry(rfc)?; + serialize_edit_doc( + &CurrentChangelogDoc { + changelog: CurrentChangelogView::from(entry), + }, + id, + ) +} + +fn apply_current_changelog_doc( + rfc: &mut crate::model::RfcSpec, + doc: serde_json::Value, + id: &str, +) -> DiagnosticResult<()> { + let view: CurrentChangelogDoc = deserialize_edit_doc(doc, id)?; + let entry = current_changelog_entry_mut(rfc)?; + entry.notes = view.changelog.summary; + entry.added = view.changelog.added; + entry.changed = view.changelog.changed; + entry.deprecated = view.changelog.deprecated; + entry.removed = view.changelog.removed; + entry.fixed = view.changelog.fixed; + entry.security = view.changelog.security; + Ok(()) +} diff --git a/src/cmd/edit/doc_target/set.rs b/src/cmd/edit/doc_target/set.rs index d8cf1488..a936826d 100644 --- a/src/cmd/edit/doc_target/set.rs +++ b/src/cmd/edit/doc_target/set.rs @@ -2,6 +2,7 @@ use super::super::{ adapter::DocAdapter, deserialize_edit_doc, engine as edit_engine, refs, runtime as edit_runtime, serialize_edit_doc, unexpected_edit_state, }; +use super::rfc_changelog; use super::{DocTargetKind, SetDocRequest, require_simple_field}; use crate::config::Config; use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticResult}; @@ -18,6 +19,10 @@ pub(in crate::cmd::edit) fn set_rfc_field<A>( where A: DocAdapter<Data = crate::model::RfcSpec>, { + if rfc_changelog::is_target(target) { + return rfc_changelog::set(config, id, target, value, op); + } + let request = SetDocRequest { config, id, diff --git a/src/cmd/edit/get.rs b/src/cmd/edit/get.rs index 84c487a9..bda6bb2b 100644 --- a/src/cmd/edit/get.rs +++ b/src/cmd/edit/get.rs @@ -2,7 +2,7 @@ use super::ArtifactType; use super::adapter::{ AdrTomlAdapter, ClauseTomlAdapter, GuardTomlAdapter, RfcTomlAdapter, WorkTomlAdapter, }; -use super::doc_target::get_doc_field; +use super::doc_target::{get_doc_field, rfc_changelog}; use super::engine as edit_engine; use super::toml_target::get_toml_field; use crate::config::Config; @@ -24,13 +24,23 @@ pub fn get_field( plan.target.as_ref(), ArtifactType::WorkItem, )?, - ArtifactType::Rfc => get_doc_field::<RfcTomlAdapter>( - config, - id, - plan.target.as_ref(), - ArtifactType::Rfc, - "RFC fields do not support nested paths", - )?, + ArtifactType::Rfc => { + if let Some(target) = plan + .target + .as_ref() + .filter(|target| rfc_changelog::is_target(target)) + { + rfc_changelog::get(config, id, target)?; + } else { + get_doc_field::<RfcTomlAdapter>( + config, + id, + plan.target.as_ref(), + ArtifactType::Rfc, + "RFC fields do not support nested paths", + )?; + } + } ArtifactType::Clause => get_doc_field::<ClauseTomlAdapter>( config, id, diff --git a/src/cmd/edit/remove.rs b/src/cmd/edit/remove.rs index 2ca3f699..5b5cd696 100644 --- a/src/cmd/edit/remove.rs +++ b/src/cmd/edit/remove.rs @@ -1,7 +1,7 @@ use super::adapter::{ AdrTomlAdapter, ClauseTomlAdapter, GuardTomlAdapter, RfcTomlAdapter, WorkTomlAdapter, }; -use super::doc_target::remove_doc_simple_list_field; +use super::doc_target::{remove_doc_simple_list_field, rfc_changelog}; use super::matching::MatchOptions; use super::rules as edit_rules; use super::toml_target::remove_toml_field; @@ -34,15 +34,21 @@ pub fn remove_from_field( op, ArtifactType::WorkItem, )?, - ArtifactType::Rfc => remove_doc_simple_list_field::<RfcTomlAdapter>( - config, - id, - target, - opts, - op, - ArtifactType::Rfc, - "RFC fields do not support nested paths for remove", - )?, + ArtifactType::Rfc => { + if rfc_changelog::is_target(target) { + rfc_changelog::remove(config, id, target, opts, op)?; + } else { + remove_doc_simple_list_field::<RfcTomlAdapter>( + config, + id, + target, + opts, + op, + ArtifactType::Rfc, + "RFC fields do not support nested paths for remove", + )?; + } + } ArtifactType::Clause => remove_doc_simple_list_field::<ClauseTomlAdapter>( config, id, diff --git a/src/cmd/edit/rules.rs b/src/cmd/edit/rules.rs index 31ca2721..e0f10092 100644 --- a/src/cmd/edit/rules.rs +++ b/src/cmd/edit/rules.rs @@ -217,7 +217,7 @@ mod tests { fn test_nested_rule_lookup() -> Result<(), Box<dyn std::error::Error>> { let rule = nested_root_rule("adr", "alternatives").ok_or("rule should exist")?; assert_eq!(rule.node.kind, NestedNodeKind::List); - assert_eq!(EDIT_RULES_VERSION, 2); + assert_eq!(EDIT_RULES_VERSION, 3); Ok(()) } @@ -246,6 +246,26 @@ mod tests { Ok(()) } + #[test] + fn test_rfc_current_changelog_rule_lookup() -> Result<(), Box<dyn std::error::Error>> { + let rule = nested_root_rule("rfc", "changelog").ok_or("rule should exist")?; + assert_eq!(rule.node.kind, NestedNodeKind::Object); + assert!(rule.node.verbs.contains(&"get")); + assert!(nested_field_supports_verb( + "rfc", + "changelog", + "summary", + Verb::Set + )); + assert!(!nested_field_supports_verb( + "rfc", + "changelog", + "summary", + Verb::Get + )); + Ok(()) + } + #[test] fn test_nested_status_list_spec() -> Result<(), Box<dyn std::error::Error>> { let rule = nested_root_rule("adr", "alternatives").ok_or("rule should exist")?; diff --git a/src/cmd/lifecycle/mod.rs b/src/cmd/lifecycle/mod.rs index c270b4e6..65f13062 100644 --- a/src/cmd/lifecycle/mod.rs +++ b/src/cmd/lifecycle/mod.rs @@ -14,6 +14,7 @@ mod rfc_clause_versions; mod rfc_supersede; pub use adr::{accept_adr, reject_adr, validate_adr_completeness}; pub use release::{cut_release, undo_release}; +pub(crate) use rfc::require_changelog_update_ready; pub use rfc::{advance, bump, finalize}; /// Deprecate an artifact diff --git a/src/cmd/lifecycle/rfc.rs b/src/cmd/lifecycle/rfc.rs index 7713ffc0..c488b2d2 100644 --- a/src/cmd/lifecycle/rfc.rs +++ b/src/cmd/lifecycle/rfc.rs @@ -1,5 +1,7 @@ use super::paths::require_rfc_toml_path; -use super::rfc_clause_versions::{fill_pending_clause_versions, rfc_update_paths}; +use super::rfc_clause_versions::{ + fill_pending_clause_versions, pending_clause_ids, rfc_update_paths, +}; use crate::FinalizeStatus; use crate::cmd::edit; use crate::config::Config; @@ -68,21 +70,20 @@ pub fn bump( rfc_id, )); } - (None, _, false) => { - let refresh_signature_after_write = - should_refresh_signature_after_changelog_only(config, &rfc_path)?; - for change in changes { - add_changelog_change(&mut rfc, change)?; - } - refresh_signature_after_write - } - (None, Some(_), true) => { + (None, Some(_), _) => { return Err(Diagnostic::new( DiagnosticCode::E0108RfcBumpRequiresSummary, "Bump level (--patch/--minor/--major) required when providing --summary", rfc_id, )); } + (None, None, false) => { + require_changelog_update_ready(config, &rfc_path, rfc_id)?; + for change in changes { + add_changelog_change(&mut rfc, change)?; + } + false + } (None, None, true) => { return Err(Diagnostic::new( DiagnosticCode::E0801MissingRequiredArg, @@ -166,12 +167,17 @@ fn transition_rfc_status( )); } - let paths = rfc_update_paths(config, &rfc_path)?; - let path_refs: Vec<_> = paths.iter().map(std::path::PathBuf::as_path).collect(); - let updated_clause_ids = with_file_transaction(&path_refs, op, || { + let updated_clause_ids = if target_status == RfcStatus::Normative { + let paths = rfc_update_paths(config, &rfc_path)?; + let path_refs: Vec<_> = paths.iter().map(std::path::PathBuf::as_path).collect(); + with_file_transaction(&path_refs, op, || { + edit::set_field_direct(config, rfc_id, "status", target_status.as_ref(), op)?; + fill_pending_clause_versions(config, &rfc_path, &rfc.version, op) + })? + } else { edit::set_field_direct(config, rfc_id, "status", target_status.as_ref(), op)?; - fill_pending_clause_versions(config, &rfc_path, &rfc.version, op) - })?; + Vec::new() + }; if !op.is_preview() { for clause_id in updated_clause_ids { @@ -221,9 +227,26 @@ pub fn advance( )); } + let seals_current_version = rfc.phase == RfcPhase::Spec && phase == RfcPhase::Impl; + if seals_current_version { + let pending_clause_ids = pending_clause_ids(config, &rfc_path)?; + if !pending_clause_ids.is_empty() { + return Err(Diagnostic::new( + DiagnosticCode::E0104RfcInvalidTransition, + format!( + "Cannot advance to impl while Clause versions are pending: {}", + pending_clause_ids.join(", ") + ), + rfc_id, + )); + } + } + let rfc_index = crate::load::load_rfc(config, &rfc_path)?; - let migrated_signature = if let Some(stored_signature) = &rfc_index.rfc.signature { - let current_signature = crate::signature::compute_rfc_content_signature(&rfc_index)?; + 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 { if stored_signature == ¤t_signature { None } else if crate::signature::compute_rfc_signature(&rfc_index) @@ -242,7 +265,7 @@ pub fn advance( }; let mut updated_rfc = rfc; - if let Some(signature) = migrated_signature { + if let Some(signature) = next_signature { updated_rfc.signature = Some(signature); } updated_rfc.phase = phase; @@ -317,6 +340,10 @@ fn ensure_rfc_has_content_amendment( rfc_path: &Path, rfc_id: &str, ) -> DiagnosticResult<bool> { + if !pending_clause_ids(config, rfc_path)?.is_empty() { + return Ok(true); + } + let rfc_index = crate::load::load_rfc(config, rfc_path)?; if rfc_index.rfc.signature.is_none() { return Ok(false); @@ -332,10 +359,28 @@ fn ensure_rfc_has_content_amendment( )) } -fn should_refresh_signature_after_changelog_only( +pub(crate) fn require_changelog_update_ready( config: &Config, rfc_path: &Path, -) -> DiagnosticResult<bool> { + rfc_id: &str, +) -> DiagnosticResult<()> { + require_rfc_content_signature_schema(config, rfc_id)?; let rfc_index = crate::load::load_rfc(config, rfc_path)?; - Ok(rfc_index.rfc.signature.is_some() && !crate::signature::is_rfc_amended(&rfc_index)) + let Some(stored_signature) = &rfc_index.rfc.signature else { + return Ok(()); + }; + let content_signature = crate::signature::compute_rfc_content_signature(&rfc_index)?; + if stored_signature == &content_signature { + return Ok(()); + } + if crate::signature::compute_rfc_signature(&rfc_index) + .is_ok_and(|legacy| stored_signature == &legacy) + { + return Err(Diagnostic::new( + DiagnosticCode::E0505MigrationRequired, + "Cannot update the changelog while the RFC has a legacy amendment signature. Migrate the repository signature baseline first.", + rfc_id, + )); + } + Ok(()) } diff --git a/src/cmd/lifecycle/rfc_clause_versions.rs b/src/cmd/lifecycle/rfc_clause_versions.rs index 4bbea20e..7b68c897 100644 --- a/src/cmd/lifecycle/rfc_clause_versions.rs +++ b/src/cmd/lifecycle/rfc_clause_versions.rs @@ -14,6 +14,21 @@ pub(super) fn rfc_update_paths(config: &Config, rfc_path: &Path) -> DiagnosticRe Ok(paths) } +pub(super) fn pending_clause_ids( + config: &Config, + rfc_path: &Path, +) -> DiagnosticResult<Vec<String>> { + let mut clause_ids = Vec::new(); + for path in clause_toml_paths(rfc_path)? { + let clause = read_clause(config, &path)?; + if clause.since.is_none() { + clause_ids.push(clause.clause_id); + } + } + clause_ids.sort(); + Ok(clause_ids) +} + fn clause_toml_paths(rfc_path: &Path) -> DiagnosticResult<Vec<PathBuf>> { let clauses_dir = rfc_path .parent() @@ -59,8 +74,7 @@ fn clause_toml_paths(rfc_path: &Path) -> DiagnosticResult<Vec<PathBuf>> { /// Update pending clauses (since: null) with the given version. /// -/// Clauses are created with `since: None` and filled in when the RFC -/// is bumped or finalized. +/// Pending clauses are filled in when the RFC is bumped or finalized. pub(super) fn fill_pending_clause_versions( config: &Config, rfc_path: &Path, diff --git a/src/cmd/new/artifacts/clause.rs b/src/cmd/new/artifacts/clause.rs index a5307497..92487117 100644 --- a/src/cmd/new/artifacts/clause.rs +++ b/src/cmd/new/artifacts/clause.rs @@ -2,10 +2,12 @@ use super::write_new_artifact_toml; use crate::config::Config; use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticResult, Diagnostics}; use crate::load::split_clause_id; -use crate::model::{ClauseKind, ClauseSpec, ClauseStatus, ClauseWire, RfcWire, SectionSpec}; +use crate::model::{ + ClauseKind, ClauseSpec, ClauseStatus, ClauseWire, RfcPhase, RfcStatus, RfcWire, SectionSpec, +}; use crate::schema::ArtifactSchema; use crate::ui; -use crate::write::WriteOp; +use crate::write::{WriteOp, with_file_transaction}; pub(super) fn create( config: &Config, @@ -34,6 +36,19 @@ pub(super) fn create( let mut rfc = crate::write::read_rfc(config, &rfc_path)?; + if rfc.status == RfcStatus::Deprecated { + return Err(Diagnostic::new( + DiagnosticCode::E0104RfcInvalidTransition, + format!("Cannot create a clause in deprecated RFC: {rfc_id}"), + clause_id, + )); + } + + let since = match (rfc.status, rfc.phase) { + (RfcStatus::Normative, RfcPhase::Spec) => Some(rfc.version.clone()), + _ => None, + }; + let clause = ClauseSpec { clause_id: clause_name.to_string(), title: title.to_string(), @@ -42,22 +57,13 @@ pub(super) fn create( text: "TODO: Add clause text here.".to_string(), anchors: vec![], superseded_by: None, - since: None, // Will be set by rfc bump + since, tags: vec![], }; let clause_path = config.clause_source_path(rfc_id, clause_name, "toml"); - let wire: ClauseWire = clause.into(); - write_new_artifact_toml( - config, - &clause_path, - &wire, - ArtifactSchema::Clause, - DiagnosticCode::E0201ClauseSchemaInvalid, - "clause", - op, - )?; + let clause_wire: ClauseWire = clause.into(); let clause_rel_path = format!("clauses/{clause_name}.toml"); if let Some(sec) = rfc.sections.iter_mut().find(|s| s.title == section) { @@ -71,16 +77,27 @@ pub(super) fn create( }); } - let wire: RfcWire = rfc.into(); - write_new_artifact_toml( - config, - &rfc_path, - &wire, - ArtifactSchema::Rfc, - DiagnosticCode::E0101RfcSchemaInvalid, - "RFC", - op, - )?; + let rfc_wire: RfcWire = rfc.into(); + with_file_transaction(&[clause_path.as_path(), rfc_path.as_path()], op, || { + write_new_artifact_toml( + config, + &clause_path, + &clause_wire, + ArtifactSchema::Clause, + DiagnosticCode::E0201ClauseSchemaInvalid, + "clause", + op, + )?; + write_new_artifact_toml( + config, + &rfc_path, + &rfc_wire, + ArtifactSchema::Rfc, + DiagnosticCode::E0101RfcSchemaInvalid, + "RFC", + op, + ) + })?; if !op.is_preview() { ui::created("clause", &config.display_path(&clause_path)); diff --git a/src/diagnostic/code/metadata.rs b/src/diagnostic/code/metadata.rs index 2bd44a7c..6859b8d4 100644 --- a/src/diagnostic/code/metadata.rs +++ b/src/diagnostic/code/metadata.rs @@ -35,6 +35,7 @@ pub(super) fn code(code: &DiagnosticCode) -> &'static str { DiagnosticCode::E0112RfcReferenceHierarchy => "E0112", DiagnosticCode::E0113RfcBumpNoAmendment => "E0113", DiagnosticCode::E0114RfcPendingAmendment => "E0114", + DiagnosticCode::E0115RfcCurrentChangelogInvalid => "E0115", // E02xx - Clause DiagnosticCode::E0201ClauseSchemaInvalid => "E0201", DiagnosticCode::E0202ClauseNotFound => "E0202", diff --git a/src/diagnostic/code/mod.rs b/src/diagnostic/code/mod.rs index af714f29..2cc07b58 100644 --- a/src/diagnostic/code/mod.rs +++ b/src/diagnostic/code/mod.rs @@ -29,6 +29,7 @@ pub enum DiagnosticCode { E0112RfcReferenceHierarchy, E0113RfcBumpNoAmendment, E0114RfcPendingAmendment, + E0115RfcCurrentChangelogInvalid, // Clause errors (E02xx) E0201ClauseSchemaInvalid, @@ -151,6 +152,8 @@ pub enum DiagnosticCode { E0903UnexpectedError, // Warnings (W01xx) + /// Retained for compatibility with diagnostics emitted before current-entry validation. + #[allow(dead_code)] W0101RfcNoChangelog, W0102ClauseNoSince, W0103AdrNoRefs, diff --git a/src/validate/rfc.rs b/src/validate/rfc.rs index 3f8d8d26..b08d276f 100644 --- a/src/validate/rfc.rs +++ b/src/validate/rfc.rs @@ -32,10 +32,24 @@ pub(super) fn validate_rfc(rfc: &RfcIndex, config: &Config, result: &mut Validat )); } - if rfc.rfc.changelog.is_empty() { + let current_changelog_count = rfc + .rfc + .changelog + .iter() + .filter(|entry| entry.version == rfc.rfc.version) + .count(); + if current_changelog_count != 1 { + let code = if current_changelog_count == 0 { + DiagnosticCode::E0111RfcNoChangelog + } else { + DiagnosticCode::E0115RfcCurrentChangelogInvalid + }; result.diagnostics.push(Diagnostic::new( - DiagnosticCode::W0101RfcNoChangelog, - "RFC has no changelog entries (hint: run `govctl rfc bump`)", + code, + format!( + "RFC must contain exactly one changelog entry for current version {} (found {})", + rfc.rfc.version, current_changelog_count + ), rfc_path_display.clone(), )); } diff --git a/src/write/changelog/mod.rs b/src/write/changelog/mod.rs index b0c97097..54973dcc 100644 --- a/src/write/changelog/mod.rs +++ b/src/write/changelog/mod.rs @@ -132,32 +132,67 @@ pub fn bump_rfc_version( pub fn add_changelog_change(rfc: &mut RfcSpec, change: &str) -> DiagnosticResult<()> { let parsed = parse_changelog_change(change)?; - if let Some(entry) = rfc.changelog.first_mut() { - match parsed.category { - ChangelogCategory::Added => entry.added.push(parsed.message), - ChangelogCategory::Changed => entry.changed.push(parsed.message), - ChangelogCategory::Deprecated => entry.deprecated.push(parsed.message), - ChangelogCategory::Removed => entry.removed.push(parsed.message), - ChangelogCategory::Fixed => entry.fixed.push(parsed.message), - ChangelogCategory::Security => entry.security.push(parsed.message), - ChangelogCategory::Chore => { - return Err(Diagnostic::new( - DiagnosticCode::E0809ChoreNotAllowed, - "'chore:' category is not valid for RFC changelogs (use for work items only)", - "changelog", - )); - } + let entry = current_changelog_entry_mut(rfc)?; + match parsed.category { + ChangelogCategory::Added => entry.added.push(parsed.message), + ChangelogCategory::Changed => entry.changed.push(parsed.message), + ChangelogCategory::Deprecated => entry.deprecated.push(parsed.message), + ChangelogCategory::Removed => entry.removed.push(parsed.message), + ChangelogCategory::Fixed => entry.fixed.push(parsed.message), + ChangelogCategory::Security => entry.security.push(parsed.message), + ChangelogCategory::Chore => { + return Err(Diagnostic::new( + DiagnosticCode::E0809ChoreNotAllowed, + "'chore:' category is not valid for RFC changelogs (use for work items only)", + "changelog", + )); } - } else { - return Err(Diagnostic::new( - DiagnosticCode::E0111RfcNoChangelog, - "No changelog entry exists. Bump version first.", - "rfc", - )); } Ok(()) } +/// Resolve the unique changelog entry for the RFC's current version. +/// +/// Implements [[RFC-0000:C-RFC-DEF]]. +pub fn current_changelog_entry(rfc: &RfcSpec) -> DiagnosticResult<&ChangelogEntry> { + let index = current_changelog_index(rfc)?; + Ok(&rfc.changelog[index]) +} + +/// Resolve the unique mutable changelog entry for the RFC's current version. +pub fn current_changelog_entry_mut(rfc: &mut RfcSpec) -> DiagnosticResult<&mut ChangelogEntry> { + let index = current_changelog_index(rfc)?; + Ok(&mut rfc.changelog[index]) +} + +fn current_changelog_index(rfc: &RfcSpec) -> DiagnosticResult<usize> { + let matching: Vec<_> = rfc + .changelog + .iter() + .enumerate() + .filter_map(|(index, entry)| (entry.version == rfc.version).then_some(index)) + .collect(); + + if let [index] = matching.as_slice() { + return Ok(*index); + } + + let code = if matching.is_empty() { + DiagnosticCode::E0111RfcNoChangelog + } else { + DiagnosticCode::E0115RfcCurrentChangelogInvalid + }; + Err(Diagnostic::new( + code, + format!( + "RFC must contain exactly one changelog entry for current version {} (found {})", + rfc.version, + matching.len() + ), + &rfc.rfc_id, + )) +} + /// Get today's date in ISO format pub fn today() -> String { Local::now().format("%Y-%m-%d").to_string() diff --git a/src/write/changelog/tests.rs b/src/write/changelog/tests.rs index e235cd77..eb625698 100644 --- a/src/write/changelog/tests.rs +++ b/src/write/changelog/tests.rs @@ -1,5 +1,39 @@ use super::*; -use crate::model::ChangelogCategory; +use crate::diagnostic::DiagnosticCode; +use crate::model::{ChangelogCategory, ChangelogEntry, RfcPhase, RfcSpec, RfcStatus}; + +fn changelog_entry(version: &str) -> ChangelogEntry { + ChangelogEntry { + version: version.to_string(), + date: "2026-07-16".to_string(), + notes: Some(format!("Version {version}")), + added: vec![], + changed: vec![], + deprecated: vec![], + removed: vec![], + fixed: vec![], + security: vec![], + } +} + +fn test_rfc() -> RfcSpec { + RfcSpec { + rfc_id: "RFC-0001".to_string(), + title: "Test RFC".to_string(), + version: "0.2.0".to_string(), + status: RfcStatus::Normative, + phase: RfcPhase::Spec, + owners: vec![], + created: "2026-07-16".to_string(), + updated: None, + supersedes: None, + refs: vec![], + tags: vec![], + sections: vec![], + changelog: vec![changelog_entry("0.1.0"), changelog_entry("0.2.0")], + signature: None, + } +} #[test] fn test_parse_changelog_no_prefix() -> Result<(), Box<dyn std::error::Error>> { @@ -129,3 +163,25 @@ fn test_parse_changelog_conventional_commit_aliases() -> Result<(), Box<dyn std: assert_eq!(r.category, ChangelogCategory::Chore); Ok(()) } + +#[test] +fn add_change_resolves_current_entry_by_version() -> Result<(), Box<dyn std::error::Error>> { + let mut rfc = test_rfc(); + + add_changelog_change(&mut rfc, "fix: current version")?; + + assert!(rfc.changelog[0].fixed.is_empty()); + assert_eq!(rfc.changelog[1].fixed, ["current version"]); + Ok(()) +} + +#[test] +fn current_entry_resolution_rejects_duplicate_matches() { + let mut rfc = test_rfc(); + rfc.changelog.push(changelog_entry("0.2.0")); + + let error = add_changelog_change(&mut rfc, "fix: ambiguous").unwrap_err(); + + assert_eq!(error.code, DiagnosticCode::E0115RfcCurrentChangelogInvalid); + assert!(error.message.contains("found 2")); +} diff --git a/src/write/mod.rs b/src/write/mod.rs index 53dfb782..2522aac1 100644 --- a/src/write/mod.rs +++ b/src/write/mod.rs @@ -17,7 +17,10 @@ mod changelog; pub use artifact::{read_clause, read_rfc, write_clause, write_rfc}; pub use artifact_normalize::{normalize_clause_value, normalize_rfc_value}; -pub use changelog::{BumpLevel, ParsedChange, add_changelog_change, bump_rfc_version, today}; +pub use changelog::{ + BumpLevel, ParsedChange, add_changelog_change, bump_rfc_version, current_changelog_entry, + current_changelog_entry_mut, today, +}; pub fn parse_changelog_change(change: &str) -> DiagnosticResult<ParsedChange> { changelog::parse_changelog_change(change) diff --git a/tests/common/fixtures.rs b/tests/common/fixtures.rs index 42b9dcac..cebf4e72 100644 --- a/tests/common/fixtures.rs +++ b/tests/common/fixtures.rs @@ -144,6 +144,11 @@ created = "2026-01-01" [[sections]] title = "Specification" + +[[changelog]] +version = "0.1.0" +date = "2026-01-01" +notes = "Initial version" "# ); fs::write(rfc_dir.join("rfc.toml"), content)?; diff --git a/tests/edit_tests/rfc.rs b/tests/edit_tests/rfc.rs index 9178a143..b9c5dea3 100644 --- a/tests/edit_tests/rfc.rs +++ b/tests/edit_tests/rfc.rs @@ -290,3 +290,304 @@ fn test_rfc_get_nonexistent() -> common::TestResult { assert_edit_snapshot!(normalize_output(&output, temp_dir.path(), &date)?); Ok(()) } + +#[test] +fn test_rfc_current_changelog_edit_resolves_by_version_and_preserves_lifecycle_fields() +-> common::TestResult { + let temp_dir = init_project()?; + run_commands( + temp_dir.path(), + &[ + &["rfc", "new", "Test RFC"], + &["rfc", "finalize", "RFC-0001", "normative"], + &["rfc", "advance", "RFC-0001", "impl"], + ], + )?; + + let rfc_path = temp_dir.path().join("gov/rfc/RFC-0001/rfc.toml"); + let mut before: toml::Value = toml::from_str(&std::fs::read_to_string(&rfc_path)?)?; + let current = before["changelog"] + .as_array() + .and_then(|entries| entries.first()) + .cloned() + .ok_or("missing current changelog entry")?; + let mut historical = current; + historical["version"] = toml::Value::String("0.0.1".to_string()); + historical["date"] = toml::Value::String("2026-01-01".to_string()); + historical["notes"] = toml::Value::String("Historical summary".to_string()); + before["changelog"] + .as_array_mut() + .ok_or("changelog is not an array")? + .insert(0, historical); + std::fs::write(&rfc_path, toml::to_string_pretty(&before)?)?; + + let baseline: toml::Value = toml::from_str(&std::fs::read_to_string(&rfc_path)?)?; + let output = run_commands( + temp_dir.path(), + &[ + &[ + "rfc", + "edit", + "RFC-0001", + "changelog.summary", + "--set", + "Corrected summary", + ], + &[ + "rfc", + "edit", + "RFC-0001", + "changelog.fixed", + "--add", + "First fix", + ], + &[ + "rfc", + "edit", + "RFC-0001", + "changelog.fixed", + "--add", + "Second fix", + ], + &["rfc", "edit", "RFC-0001", "changelog.fixed[0]", "--remove"], + &["rfc", "get", "RFC-0001", "changelog"], + ], + )?; + + assert!(output.contains("\"summary\": \"Corrected summary\"")); + assert!(output.contains("\"Second fix\"")); + assert!(!output.contains("\"First fix\"")); + + let after: toml::Value = toml::from_str(&std::fs::read_to_string(&rfc_path)?)?; + for field in ["version", "phase", "signature"] { + assert_eq!(after["govctl"][field], baseline["govctl"][field]); + } + assert_eq!(after["changelog"][0], baseline["changelog"][0]); + assert_eq!( + after["changelog"][1]["date"], + baseline["changelog"][1]["date"] + ); + assert_eq!( + after["changelog"][1]["notes"].as_str(), + Some("Corrected summary") + ); + assert_eq!( + after["changelog"][1]["fixed"] + .as_array() + .and_then(|items| items.first()) + .and_then(toml::Value::as_str), + Some("Second fix") + ); + Ok(()) +} + +#[test] +fn test_rfc_changelog_edits_reject_legacy_signature_without_false_amendment() -> common::TestResult +{ + let temp_dir = init_project()?; + run_commands( + temp_dir.path(), + &[ + &["rfc", "new", "Legacy signature RFC"], + &[ + "rfc", + "edit", + "RFC-0001", + "changelog.fixed", + "--add", + "Existing fix", + ], + &["rfc", "finalize", "RFC-0001", "normative"], + &["rfc", "render", "RFC-0001"], + ], + )?; + + let rendered = std::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("<!-- SIGNATURE: sha256:") + .and_then(|value| value.strip_suffix(" -->")) + }) + .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(&std::fs::read_to_string(&rfc_path)?)?; + rfc["govctl"] + .as_table_mut() + .ok_or("RFC metadata is not a table")? + .insert( + "signature".to_string(), + toml::Value::String(legacy_signature.to_string()), + ); + std::fs::write(&rfc_path, toml::to_string_pretty(&rfc)?)?; + let before: toml::Value = toml::from_str(&std::fs::read_to_string(&rfc_path)?)?; + + let output = run_commands( + temp_dir.path(), + &[ + &[ + "rfc", + "edit", + "RFC-0001", + "changelog.summary", + "--set", + "Must not be written", + ], + &[ + "rfc", + "edit", + "RFC-0001", + "changelog.fixed", + "--add", + "Must not be added", + ], + &["rfc", "edit", "RFC-0001", "changelog.fixed[0]", "--remove"], + &[ + "rfc", + "bump", + "RFC-0001", + "--patch", + "--summary", + "False amendment", + ], + &["rfc", "advance", "RFC-0001", "impl"], + &["rfc", "get", "RFC-0001", "phase"], + ], + )?; + + assert_eq!( + output.matches("error[E0505]").count(), + 3, + "output: {output}" + ); + assert!(output.contains("error[E0113]"), "output: {output}"); + assert!( + output.contains("$ govctl rfc get RFC-0001 phase\nimpl"), + "output: {output}" + ); + let after: toml::Value = toml::from_str(&std::fs::read_to_string(&rfc_path)?)?; + assert_eq!(after["changelog"], before["changelog"]); + Ok(()) +} + +#[test] +fn test_rfc_changelog_edit_rejects_historical_and_lifecycle_owned_paths() -> common::TestResult { + let temp_dir = init_project()?; + run_commands(temp_dir.path(), &[&["rfc", "new", "Test RFC"]])?; + let rfc_path = temp_dir.path().join("gov/rfc/RFC-0001/rfc.toml"); + let before = std::fs::read(&rfc_path)?; + + let output = run_commands( + temp_dir.path(), + &[ + &[ + "rfc", + "edit", + "RFC-0001", + "changelog.version", + "--set", + "9.9.9", + ], + &[ + "rfc", + "edit", + "RFC-0001", + "changelog.date", + "--set", + "2026-01-01", + ], + &[ + "rfc", + "edit", + "RFC-0001", + "changelog[0].summary", + "--set", + "Historical rewrite", + ], + &["rfc", "get", "RFC-0001", "changelog.summary"], + &["rfc", "edit", "RFC-0001", "version", "--set", "9.9.9"], + ], + )?; + + assert!(output.contains("Unknown nested field 'version'")); + assert!(output.contains("Unknown nested field 'date'")); + assert!(output.contains("Cannot index into non-list field 'changelog'")); + assert!(output.contains("Path does not support verb 'get'")); + assert!(output.contains("RFC version is lifecycle-owned")); + assert_eq!(std::fs::read(&rfc_path)?, before); + Ok(()) +} + +#[test] +fn test_rfc_changelog_edit_rejects_duplicate_current_entries_without_mutation() -> common::TestResult +{ + let temp_dir = init_project()?; + run_commands(temp_dir.path(), &[&["rfc", "new", "Test RFC"]])?; + let rfc_path = temp_dir.path().join("gov/rfc/RFC-0001/rfc.toml"); + let mut malformed: toml::Value = toml::from_str(&std::fs::read_to_string(&rfc_path)?)?; + let duplicate = malformed["changelog"] + .as_array() + .and_then(|entries| entries.first()) + .cloned() + .ok_or("missing current changelog entry")?; + malformed["changelog"] + .as_array_mut() + .ok_or("changelog is not an array")? + .push(duplicate); + std::fs::write(&rfc_path, toml::to_string_pretty(&malformed)?)?; + let before = std::fs::read(&rfc_path)?; + + let output = run_commands( + temp_dir.path(), + &[&[ + "rfc", + "edit", + "RFC-0001", + "changelog.fixed", + "--add", + "Must not be written", + ]], + )?; + + assert!(output.contains("error[E0115]"), "output: {output}"); + assert!(output.contains("found 2"), "output: {output}"); + assert_eq!(std::fs::read(&rfc_path)?, before); + Ok(()) +} + +#[test] +fn test_rfc_changelog_operations_reject_missing_current_entry_without_mutation() +-> common::TestResult { + let temp_dir = init_project()?; + run_commands(temp_dir.path(), &[&["rfc", "new", "Test RFC"]])?; + let rfc_path = temp_dir.path().join("gov/rfc/RFC-0001/rfc.toml"); + let mut malformed: toml::Value = toml::from_str(&std::fs::read_to_string(&rfc_path)?)?; + malformed["govctl"]["version"] = toml::Value::String("9.9.9".to_string()); + std::fs::write(&rfc_path, toml::to_string_pretty(&malformed)?)?; + let before = std::fs::read(&rfc_path)?; + + let output = run_commands( + temp_dir.path(), + &[ + &["rfc", "get", "RFC-0001", "changelog"], + &[ + "rfc", + "edit", + "RFC-0001", + "changelog.summary", + "--set", + "Must not be written", + ], + ], + )?; + + assert_eq!( + output.matches("error[E0111]").count(), + 2, + "output: {output}" + ); + assert!(output.contains("found 0"), "output: {output}"); + assert_eq!(std::fs::read(&rfc_path)?, before); + Ok(()) +} diff --git a/tests/error_tests/rfc_clause_cases/check.rs b/tests/error_tests/rfc_clause_cases/check.rs index 295bbf1b..5c7db449 100644 --- a/tests/error_tests/rfc_clause_cases/check.rs +++ b/tests/error_tests/rfc_clause_cases/check.rs @@ -47,6 +47,66 @@ fn write_adr_toml(project_dir: &std::path::Path, content: &str) -> common::TestR Ok(()) } +#[test] +fn test_rfc_check_rejects_missing_current_changelog_entry() -> common::TestResult { + let temp_dir = init_project()?; + write_rfc_toml( + temp_dir.path(), + r#"[govctl] +id = "RFC-0001" +title = "Missing Current Changelog" +version = "1.0.0" +status = "draft" +phase = "spec" +owners = ["@test"] +created = "2026-01-01" + +[[sections]] +title = "Specification" +"#, + )?; + + let output = run_commands(temp_dir.path(), &[&["check"]])?; + assert!(output.contains("error[E0111]"), "output: {output}"); + assert!(output.contains("found 0"), "output: {output}"); + Ok(()) +} + +#[test] +fn test_rfc_check_rejects_duplicate_current_changelog_entries() -> common::TestResult { + let temp_dir = init_project()?; + write_rfc_toml( + temp_dir.path(), + r#"[govctl] +id = "RFC-0001" +title = "Duplicate Current Changelog" +version = "1.0.0" +status = "draft" +phase = "spec" +owners = ["@test"] +created = "2026-01-01" + +[[sections]] +title = "Specification" + +[[changelog]] +version = "1.0.0" +date = "2026-01-01" +notes = "First" + +[[changelog]] +version = "1.0.0" +date = "2026-01-02" +notes = "Duplicate" +"#, + )?; + + let output = run_commands(temp_dir.path(), &[&["check"]])?; + assert!(output.contains("error[E0115]"), "output: {output}"); + assert!(output.contains("found 2"), "output: {output}"); + Ok(()) +} + #[test] fn test_broken_superseded_check() -> common::TestResult { let (temp_dir, date) = init_project_with_date()?; diff --git a/tests/error_tests/rfc_clause_cases/wire_format.rs b/tests/error_tests/rfc_clause_cases/wire_format.rs index 2683bbc6..0f51a554 100644 --- a/tests/error_tests/rfc_clause_cases/wire_format.rs +++ b/tests/error_tests/rfc_clause_cases/wire_format.rs @@ -54,6 +54,11 @@ created = "2026-01-01" [[sections]] title = "Summary" + +[[changelog]] +version = "0.1.0" +date = "2026-01-01" +notes = "Initial version" "#, )?; @@ -83,6 +88,11 @@ created = "2026-01-01" [[sections]] title = "Spec" clauses = ["clauses/C-TEST.toml"] + +[[changelog]] +version = "0.1.0" +date = "2026-01-01" +notes = "Initial version" "#, )?; @@ -259,6 +269,11 @@ created = "2026-01-01" [[sections]] title = "Summary" + +[[changelog]] +version = "0.1.0" +date = "2026-01-01" +notes = "Initial version" "#, )?; @@ -284,6 +299,11 @@ created = "2026-01-01" [[sections]] title = "Spec" clauses = ["clauses/C-FLAT.toml"] + +[[changelog]] +version = "0.1.0" +date = "2026-01-01" +notes = "Initial version" "#, )?; diff --git a/tests/lifecycle_tests/clause.rs b/tests/lifecycle_tests/clause.rs index 5d046c7a..ab822c53 100644 --- a/tests/lifecycle_tests/clause.rs +++ b/tests/lifecycle_tests/clause.rs @@ -28,6 +28,162 @@ fn test_deprecate_legacy_json_clause_requires_migrate() -> common::TestResult { assert_lifecycle_snapshot!(normalize_output(&output, temp_dir.path(), &date)?); Ok(()) } + +// ============================================================================ +// Clause Version Assignment Tests +// ============================================================================ + +#[test] +fn test_clause_new_assigns_since_only_for_normative_spec_rfc() -> common::TestResult { + let temp_dir = init_project()?; + run_commands( + temp_dir.path(), + &[ + &["rfc", "new", "Test RFC"], + &[ + "clause", + "new", + "RFC-0001:C-DRAFT", + "Draft Clause", + "-s", + "Specification", + "-k", + "normative", + ], + ], + )?; + + let clause_dir = temp_dir.path().join("gov/rfc/RFC-0001/clauses"); + let draft_before: toml::Value = + toml::from_str(&fs::read_to_string(clause_dir.join("C-DRAFT.toml"))?)?; + assert!(draft_before["govctl"].get("since").is_none()); + + run_commands( + temp_dir.path(), + &[ + &["rfc", "finalize", "RFC-0001", "normative"], + &[ + "clause", + "new", + "RFC-0001:C-SPEC", + "Spec Clause", + "-s", + "Specification", + "-k", + "normative", + ], + ], + )?; + + let draft_after: toml::Value = + toml::from_str(&fs::read_to_string(clause_dir.join("C-DRAFT.toml"))?)?; + let spec_clause: toml::Value = + toml::from_str(&fs::read_to_string(clause_dir.join("C-SPEC.toml"))?)?; + assert_eq!(draft_after["govctl"]["since"].as_str(), Some("0.1.0")); + assert_eq!(spec_clause["govctl"]["since"].as_str(), Some("0.1.0")); + + run_commands( + temp_dir.path(), + &[ + &["rfc", "advance", "RFC-0001", "impl"], + &[ + "clause", + "new", + "RFC-0001:C-IMPL", + "Implementation Amendment", + "-s", + "Specification", + "-k", + "normative", + ], + ], + )?; + + let impl_clause: toml::Value = + toml::from_str(&fs::read_to_string(clause_dir.join("C-IMPL.toml"))?)?; + assert!(impl_clause["govctl"].get("since").is_none()); + Ok(()) +} + +#[test] +fn test_clause_new_rejects_deprecated_rfc_without_mutation() -> common::TestResult { + let temp_dir = init_project()?; + run_commands( + temp_dir.path(), + &[ + &["rfc", "new", "Test RFC"], + &["rfc", "finalize", "RFC-0001", "normative"], + &["rfc", "deprecate", "RFC-0001", "--force"], + ], + )?; + + 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(), + &[&[ + "clause", + "new", + "RFC-0001:C-REJECTED", + "Rejected Clause", + "-s", + "Specification", + "-k", + "normative", + ]], + )?; + + assert!(output.contains("error[E0104]"), "output: {output}"); + assert!(output.contains("deprecated RFC"), "output: {output}"); + assert_eq!(fs::read(&rfc_path)?, before); + assert!( + !temp_dir + .path() + .join("gov/rfc/RFC-0001/clauses/C-REJECTED.toml") + .exists() + ); + Ok(()) +} + +#[cfg(unix)] +#[test] +fn test_clause_new_rfc_write_failure_rolls_back_created_clause() -> common::TestResult { + use std::os::unix::fs::PermissionsExt; + + let temp_dir = init_project()?; + run_commands(temp_dir.path(), &[&["rfc", "new", "Test RFC"]])?; + + let rfc_dir = temp_dir.path().join("gov/rfc/RFC-0001"); + let rfc_path = rfc_dir.join("rfc.toml"); + let clause_path = rfc_dir.join("clauses/C-ROLLBACK.toml"); + let original_rfc = fs::read(&rfc_path)?; + let original_permissions = fs::metadata(&rfc_dir)?.permissions(); + let mut unwritable_permissions = original_permissions.clone(); + unwritable_permissions.set_mode(original_permissions.mode() & !0o222); + fs::set_permissions(&rfc_dir, unwritable_permissions)?; + + let output = run_commands( + temp_dir.path(), + &[&[ + "clause", + "new", + "RFC-0001:C-ROLLBACK", + "Rollback Clause", + "-s", + "Specification", + "-k", + "normative", + ]], + ); + fs::set_permissions(&rfc_dir, original_permissions)?; + let output = output?; + + assert!(output.contains("error[E0901]"), "output: {output}"); + assert!(!output.contains("Created clause"), "output: {output}"); + assert_eq!(fs::read(&rfc_path)?, original_rfc); + assert!(!clause_path.exists()); + Ok(()) +} // ============================================================================ // Clause Supersede Tests // ============================================================================ diff --git a/tests/lifecycle_tests/rfc_cases/advance.rs b/tests/lifecycle_tests/rfc_cases/advance.rs index 5f1938cb..1bcdf654 100644 --- a/tests/lifecycle_tests/rfc_cases/advance.rs +++ b/tests/lifecycle_tests/rfc_cases/advance.rs @@ -119,9 +119,9 @@ fn test_advance_nonexistent_rfc() -> common::TestResult { } #[test] -fn test_advance_rejects_unversioned_content_amendment() -> common::TestResult { +fn test_advance_seals_content_edits_made_during_spec() -> common::TestResult { let temp_dir = init_project()?; - let output = run_commands( + run_commands( temp_dir.path(), &[ &["rfc", "new", "Test RFC"], @@ -151,6 +151,16 @@ fn test_advance_rejects_unversioned_content_amendment() -> common::TestResult { "--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 output = run_commands( + temp_dir.path(), + &[ &[ "clause", "edit", @@ -158,20 +168,123 @@ fn test_advance_rejects_unversioned_content_amendment() -> common::TestResult { "--text", "Amended normative behavior.", ], + &[ + "rfc", + "edit", + "RFC-0001", + "title", + "--set", + "Amended Test RFC", + ], &["rfc", "advance", "RFC-0001", "impl"], &["rfc", "get", "RFC-0001", "phase"], + &["rfc", "get", "RFC-0001", "title"], + ], + )?; + + assert!(!output.contains("error[E0114]"), "output: {output}"); + assert!( + output.contains("$ govctl rfc get RFC-0001 phase\nimpl"), + "output: {output}" + ); + assert!( + output.contains("$ govctl rfc get RFC-0001 title\nAmended Test RFC"), + "output: {output}" + ); + let after: toml::Value = toml::from_str(&fs::read_to_string(&rfc_path)?)?; + assert_ne!( + after["govctl"]["signature"].as_str().map(str::to_string), + baseline_signature + ); + Ok(()) +} + +#[test] +fn test_advance_after_impl_rejects_unversioned_content_amendment() -> common::TestResult { + let temp_dir = init_project()?; + let output = run_commands( + temp_dir.path(), + &[ + &["rfc", "new", "Test RFC"], + &[ + "clause", + "new", + "RFC-0001:C-TEST", + "Test Clause", + "-s", + "Specification", + "-k", + "normative", + ], + &[ + "clause", + "edit", + "RFC-0001:C-TEST", + "--text", + "Original normative behavior.", + ], + &["rfc", "finalize", "RFC-0001", "normative"], + &["rfc", "advance", "RFC-0001", "impl"], + &[ + "clause", + "edit", + "RFC-0001:C-TEST", + "--text", + "Unversioned implementation-phase amendment.", + ], + &["rfc", "advance", "RFC-0001", "test"], + &["rfc", "get", "RFC-0001", "phase"], ], )?; assert!(output.contains("error[E0114]"), "output: {output}"); assert!(output.contains("unversioned amendment"), "output: {output}"); assert!( - output.contains("$ govctl rfc get RFC-0001 phase\nspec"), + output.contains("$ govctl rfc get RFC-0001 phase\nimpl"), "output: {output}" ); Ok(()) } +#[test] +fn test_advance_to_impl_rejects_pending_clause_versions_without_mutation() -> common::TestResult { + let temp_dir = init_project()?; + run_commands( + temp_dir.path(), + &[ + &["rfc", "new", "Test RFC"], + &[ + "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"]["status"] = toml::Value::String("normative".to_string()); + 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", "impl"]])?; + + assert!(output.contains("error[E0104]"), "output: {output}"); + assert!(output.contains("C-PENDING"), "output: {output}"); + assert_eq!(fs::read(&rfc_path)?, rfc_before); + assert_eq!(fs::read(&clause_path)?, clause_before); + Ok(()) +} + #[test] fn test_advance_migrates_legacy_signature_before_phase_changes() -> 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 99101312..8cdfb195 100644 --- a/tests/lifecycle_tests/rfc_cases/bump.rs +++ b/tests/lifecycle_tests/rfc_cases/bump.rs @@ -197,6 +197,32 @@ fn test_bump_requires_summary() -> common::TestResult { Ok(()) } +#[test] +fn test_changelog_only_bump_rejects_summary_without_level() -> common::TestResult { + let temp_dir = init_project()?; + run_commands(temp_dir.path(), &[&["rfc", "new", "Test RFC"]])?; + 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", + "--summary", + "Must not be ignored", + "--change", + "fix: Must not be recorded", + ]], + )?; + + assert!(output.contains("error[E0108]"), "output: {output}"); + assert!(output.contains("Bump level"), "output: {output}"); + assert_eq!(fs::read(&rfc_path)?, before); + Ok(()) +} + #[test] fn test_bump_with_change() -> common::TestResult { let (temp_dir, date) = init_project_with_date()?; @@ -214,6 +240,186 @@ fn test_bump_with_change() -> common::TestResult { Ok(()) } +#[test] +fn test_bump_change_resolves_reordered_current_entry_without_version_change() -> common::TestResult +{ + let temp_dir = init_project()?; + 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"], + ], + )?; + + 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)?)?; + let current = rfc["changelog"] + .as_array() + .and_then(|entries| entries.first()) + .cloned() + .ok_or("missing current changelog entry")?; + let mut historical = current; + historical["version"] = toml::Value::String("0.0.1".to_string()); + historical["date"] = toml::Value::String("2026-01-01".to_string()); + historical["notes"] = toml::Value::String("Historical summary".to_string()); + rfc["changelog"] + .as_array_mut() + .ok_or("changelog is not an array")? + .insert(0, historical); + fs::write(&rfc_path, toml::to_string_pretty(&rfc)?)?; + let before: toml::Value = toml::from_str(&fs::read_to_string(&rfc_path)?)?; + + let output = run_commands( + temp_dir.path(), + &[&[ + "rfc", + "bump", + "RFC-0001", + "--change", + "fix: Current-only correction", + ]], + )?; + + assert!( + output.contains("Added change to RFC-0001 v0.1.0"), + "output: {output}" + ); + let after: toml::Value = toml::from_str(&fs::read_to_string(&rfc_path)?)?; + for field in ["version", "phase", "signature"] { + assert_eq!(after["govctl"][field], before["govctl"][field]); + } + assert_eq!(after["changelog"][0], before["changelog"][0]); + assert_eq!( + after["changelog"][1]["date"], + before["changelog"][1]["date"] + ); + assert_eq!( + after["changelog"][1]["fixed"] + .as_array() + .and_then(|items| items.first()) + .and_then(toml::Value::as_str), + Some("Current-only correction") + ); + Ok(()) +} + +#[test] +fn test_bump_change_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", "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("<!-- SIGNATURE: sha256:") + .and_then(|value| value.strip_suffix(" -->")) + }) + .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"] + .as_table_mut() + .ok_or("RFC metadata is not a table")? + .insert( + "signature".to_string(), + 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", + "bump", + "RFC-0001", + "--change", + "fix: Must not rewrite signature", + ]], + )?; + + assert!(output.contains("error[E0505]"), "output: {output}"); + assert!( + output.contains("legacy amendment signature"), + "output: {output}" + ); + assert_eq!(fs::read(&rfc_path)?, before); + Ok(()) +} + +#[test] +fn test_baseline_bump_with_pending_clause_restarts_at_spec() -> common::TestResult { + let temp_dir = init_project()?; + run_commands( + temp_dir.path(), + &[ + &["rfc", "new", "Test RFC"], + &["rfc", "finalize", "RFC-0001", "normative"], + ], + )?; + + 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"]["phase"] = toml::Value::String("stable".to_string()); + rfc["govctl"] + .as_table_mut() + .ok_or("RFC metadata is not a table")? + .remove("signature"); + fs::write(&rfc_path, toml::to_string_pretty(&rfc)?)?; + run_commands( + temp_dir.path(), + &[&[ + "clause", + "new", + "RFC-0001:C-PENDING", + "Pending Clause", + "-s", + "Specification", + "-k", + "normative", + ]], + )?; + + 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"], + ], + )?; + + 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: {output}" + ); + Ok(()) +} + #[test] fn test_bump_rejects_empty_bump_after_signature_baseline() -> common::TestResult { let (temp_dir, date) = init_project_with_date()?; diff --git a/tests/lifecycle_tests/rfc_cases/deprecation.rs b/tests/lifecycle_tests/rfc_cases/deprecation.rs index 18052350..060f55f4 100644 --- a/tests/lifecycle_tests/rfc_cases/deprecation.rs +++ b/tests/lifecycle_tests/rfc_cases/deprecation.rs @@ -21,6 +21,46 @@ fn test_deprecate_normative_rfc() -> common::TestResult { Ok(()) } +#[test] +fn test_deprecate_rfc_preserves_pending_clause_version() -> common::TestResult { + let temp_dir = init_project()?; + run_commands( + temp_dir.path(), + &[ + &["rfc", "new", "Old RFC"], + &["rfc", "finalize", "RFC-0001", "normative"], + &["rfc", "advance", "RFC-0001", "impl"], + &["rfc", "advance", "RFC-0001", "test"], + &["rfc", "advance", "RFC-0001", "stable"], + &[ + "clause", + "new", + "RFC-0001:C-PENDING", + "Pending Clause", + "-s", + "Specification", + "-k", + "normative", + ], + ], + )?; + + let clause_path = temp_dir + .path() + .join("gov/rfc/RFC-0001/clauses/C-PENDING.toml"); + let before = fs::read(&clause_path)?; + let output = run_commands( + temp_dir.path(), + &[&["rfc", "deprecate", "RFC-0001", "--force"]], + )?; + + assert!(!output.contains("Set C-PENDING.since"), "output: {output}"); + assert_eq!(fs::read(&clause_path)?, 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_supersede_rfc() -> common::TestResult { let temp_dir = init_project()?; diff --git a/tests/snapshots/test_errors__legacy_flat_clause_toml_accepted.snap b/tests/snapshots/test_errors__legacy_flat_clause_toml_accepted.snap index e9a78c94..b184ed14 100644 --- a/tests/snapshots/test_errors__legacy_flat_clause_toml_accepted.snap +++ b/tests/snapshots/test_errors__legacy_flat_clause_toml_accepted.snap @@ -1,6 +1,6 @@ --- -source: tests/test_errors.rs -expression: "normalize_output(&output, temp_dir.path(), &date)" +source: tests/error_tests/rfc_clause_cases/wire_format.rs +expression: snapshot --- $ govctl check Checked: @@ -10,6 +10,5 @@ Checked: 0 work items 0 verification guards -warning[W0101]: RFC has no changelog entries (hint: run `govctl rfc bump`) (gov/rfc/RFC-0001/rfc.toml) warning[W0102]: Clause 'C-FLAT' has no 'since' version (hint: it will be set automatically by `govctl rfc bump` or `govctl rfc finalize`) (gov/rfc/RFC-0001/clauses/C-FLAT.toml) exit: 0 diff --git a/tests/snapshots/test_errors__legacy_flat_rfc_toml_accepted.snap b/tests/snapshots/test_errors__legacy_flat_rfc_toml_accepted.snap index f22de644..cf8996b2 100644 --- a/tests/snapshots/test_errors__legacy_flat_rfc_toml_accepted.snap +++ b/tests/snapshots/test_errors__legacy_flat_rfc_toml_accepted.snap @@ -1,6 +1,6 @@ --- -source: tests/test_errors.rs -expression: "normalize_output(&output, temp_dir.path(), &date)" +source: tests/error_tests/rfc_clause_cases/wire_format.rs +expression: snapshot --- $ govctl check Checked: @@ -10,5 +10,5 @@ Checked: 0 work items 0 verification guards -warning[W0101]: RFC has no changelog entries (hint: run `govctl rfc bump`) (gov/rfc/RFC-0001/rfc.toml) +✓ All checks passed exit: 0 diff --git a/tests/snapshots/test_errors__valid_clause_toml_wire_format.snap b/tests/snapshots/test_errors__valid_clause_toml_wire_format.snap index e06e0265..d0333755 100644 --- a/tests/snapshots/test_errors__valid_clause_toml_wire_format.snap +++ b/tests/snapshots/test_errors__valid_clause_toml_wire_format.snap @@ -1,6 +1,6 @@ --- -source: tests/test_errors.rs -expression: "normalize_output(&output, temp_dir.path(), &date)" +source: tests/error_tests/rfc_clause_cases/wire_format.rs +expression: snapshot --- $ govctl check Checked: @@ -10,5 +10,5 @@ Checked: 0 work items 0 verification guards -warning[W0101]: RFC has no changelog entries (hint: run `govctl rfc bump`) (gov/rfc/RFC-0001/rfc.toml) +✓ All checks passed exit: 0 diff --git a/tests/snapshots/test_errors__valid_rfc_toml_wire_format.snap b/tests/snapshots/test_errors__valid_rfc_toml_wire_format.snap index f22de644..cf8996b2 100644 --- a/tests/snapshots/test_errors__valid_rfc_toml_wire_format.snap +++ b/tests/snapshots/test_errors__valid_rfc_toml_wire_format.snap @@ -1,6 +1,6 @@ --- -source: tests/test_errors.rs -expression: "normalize_output(&output, temp_dir.path(), &date)" +source: tests/error_tests/rfc_clause_cases/wire_format.rs +expression: snapshot --- $ govctl check Checked: @@ -10,5 +10,5 @@ Checked: 0 work items 0 verification guards -warning[W0101]: RFC has no changelog entries (hint: run `govctl rfc bump`) (gov/rfc/RFC-0001/rfc.toml) +✓ 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 new file mode 100644 index 00000000..0958a337 --- /dev/null +++ b/tests/snapshots/test_help__rfc_bump_help.snap @@ -0,0 +1,32 @@ +--- +source: tests/test_help.rs +expression: normalized +--- +$ govctl rfc bump --help +Bump RFC version + +Usage: govctl rfc bump [OPTIONS] <ID> + +Arguments: + <ID> RFC ID + +Options: + -C, --config <CONFIG> Path to govctl config (TOML) + --patch Patch version bump + --dry-run Dry run: preview changes without writing files + --minor Minor version bump + --major Major version bump + -m, --summary <SUMMARY> Changelog summary + -c, --change <CHANGES> Add change description(s) + -h, --help Print help + +EXAMPLES: + govctl rfc bump RFC-0001 --patch -m "Clarify examples" + govctl rfc bump RFC-0001 --minor -m "Add a normative clause" -c "change: Define the new behavior" + govctl rfc bump RFC-0001 -c "fix: Correct current-version wording" + +NOTES: + - 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. +exit: 0 diff --git a/tests/snapshots/test_help__rfc_edit_help.snap b/tests/snapshots/test_help__rfc_edit_help.snap new file mode 100644 index 00000000..4a852a0f --- /dev/null +++ b/tests/snapshots/test_help__rfc_edit_help.snap @@ -0,0 +1,71 @@ +--- +source: tests/test_help.rs +expression: normalized +--- +$ govctl rfc edit --help +Canonical path-first edit entrypoint + +Usage: govctl rfc edit [OPTIONS] <ID> <PATH> + +Arguments: + <ID> + Artifact ID + + <PATH> + Canonical field path + +Options: + -C, --config <CONFIG> + Path to govctl config (TOML) + + --set [<SET>] + Set a scalar value (omit VALUE only when using --stdin) + + --add [<ADD>] + Append a value to a list (omit VALUE only when using --stdin) + + --dry-run + Dry run: preview changes without writing files + + --remove [<REMOVE>] + Remove a matching value, or omit PATTERN when removing an indexed path + + --tick <TICK> + Update checklist-style item status + + Possible values: + - done: Mark work items as done + - pending: Mark work items as pending + - cancelled: Mark work items as cancelled + - accepted: Mark ADR alternatives as accepted + - considered: Mark ADR alternatives as considered + - rejected: Mark ADR alternatives as rejected + + --stdin + Read set/add value from stdin + + --at <AT> + Match by index for remove/tick + + --exact + Exact match for remove/tick + + --regex + Regex match for remove/tick + + --all + Remove all matches + + -h, --help + Print help (see a summary with '-h') + +EXAMPLES: + govctl rfc edit RFC-0001 changelog.summary --set "Clarify retry behavior" + govctl rfc edit RFC-0001 changelog.fixed --add "Correct timeout wording" + govctl rfc edit RFC-0001 changelog.fixed[0] --remove + govctl rfc edit RFC-0001 refs --add RFC-0002 + +NOTES: + - Changelog edits apply only to the entry matching the RFC's current version. + - RFC and changelog version/date fields are lifecycle-owned. +exit: 0 diff --git a/tests/snapshots/test_help__rfc_get_help.snap b/tests/snapshots/test_help__rfc_get_help.snap index 3a61a2dc..37e69d83 100644 --- a/tests/snapshots/test_help__rfc_get_help.snap +++ b/tests/snapshots/test_help__rfc_get_help.snap @@ -1,7 +1,6 @@ --- source: tests/test_help.rs -assertion_line: 13 -expression: "normalize_output(&output, temp_dir.path(), &date)" +expression: normalized --- $ govctl rfc get --help Get RFC metadata or specific field @@ -18,7 +17,7 @@ Options: -h, --help Print help VALID FIELDS: - - title, version, status, phase, owners, refs + - title, version, status, phase, owners, refs, changelog EXAMPLES: govctl rfc get RFC-0001 diff --git a/tests/test_help.rs b/tests/test_help.rs index 3d7802f6..d06c2696 100644 --- a/tests/test_help.rs +++ b/tests/test_help.rs @@ -24,6 +24,16 @@ fn test_rfc_root_help() -> common::TestResult { assert_help_snapshot!(&["rfc", "--help"]) } +#[test] +fn test_rfc_edit_help() -> common::TestResult { + assert_help_snapshot!(&["rfc", "edit", "--help"]) +} + +#[test] +fn test_rfc_bump_help() -> common::TestResult { + assert_help_snapshot!(&["rfc", "bump", "--help"]) +} + #[test] fn test_adr_get_help() -> common::TestResult { assert_help_snapshot!(&["adr", "get", "--help"]) From 95d71a96003f0a4204586a679c58575e6b87edef Mon Sep 17 00:00:00 2001 From: Gabriel Wu <13583761+lucifer1004@users.noreply.github.com> Date: Thu, 16 Jul 2026 18:42:28 +0800 Subject: [PATCH 2/2] fix(rfc): enforce lifecycle bump invariants Reject version-changing bumps for draft and deprecated RFCs while preserving changelog-only updates. Clarify rollback restoration state and strengthen lifecycle regression coverage. --- .claude/skills/gov/SKILL.md | 6 +- .claude/skills/rfc-writer/SKILL.md | 12 +- .claude/skills/spec/SKILL.md | 5 +- AGENTS.md | 10 +- CHANGELOG.md | 5 + docs/guide/rfcs.md | 11 +- docs/rfc/RFC-0000.md | 16 +- docs/rfc/RFC-0002.md | 36 ++++- gov/rfc/RFC-0000/clauses/C-CLAUSE-DEF.toml | 4 +- gov/rfc/RFC-0000/rfc.toml | 10 +- .../RFC-0002/clauses/C-LIFECYCLE-VERBS.toml | 16 +- gov/rfc/RFC-0002/rfc.toml | 16 +- ...16-address-valid-pr-review-follow-ups.toml | 40 +++++ ...-version-bumps-for-non-normative-rfcs.toml | 50 +++++++ src/cli/resources/rfc.rs | 1 + src/cmd/edit/doc_target/rfc_changelog.rs | 2 +- src/cmd/lifecycle/rfc.rs | 13 ++ src/validate/rfc.rs | 2 + src/write/mod.rs | 23 +++ tests/lifecycle_tests/rfc_cases/bump.rs | 137 +++++++++++++++++- .../lifecycle_tests/rfc_cases/deprecation.rs | 5 + tests/snapshots/test_help__rfc_bump_help.snap | 1 + 22 files changed, 384 insertions(+), 37 deletions(-) create mode 100644 gov/work/2026-07-16-address-valid-pr-review-follow-ups.toml create mode 100644 gov/work/2026-07-16-reject-version-bumps-for-non-normative-rfcs.toml diff --git a/.claude/skills/gov/SKILL.md b/.claude/skills/gov/SKILL.md index e01ec3ac..2d27c51a 100644 --- a/.claude/skills/gov/SKILL.md +++ b/.claude/skills/gov/SKILL.md @@ -199,16 +199,16 @@ govctl check --has-active For RFC-governed work, verify the RFC state: -- `draft/spec`: ask permission, then finalize and advance to `impl` +- `draft/spec`: do not bump; ask permission, then finalize and advance to `impl` - `normative/spec`: continue authoring the current version without another bump; ask permission before advancing to `impl`, which seals its RFC and Clause content - `normative/impl+`: proceed against the sealed current-version baseline -- `deprecated`: stop +- `deprecated`: stop; do not edit or version-bump If implementation reveals a spec bug, do not silently deviate: - A code-only implementation defect does not require an RFC edit or bump. - Editing RFC or Clause content after entry to `impl` creates an unversioned amendment. Do not advance to `test` while it exists. -- Release that amendment with an appropriate patch, minor, or major `govctl rfc bump`; the new version starts in `spec`. +- 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 <RFC-ID> changelog.*` and does not affect the sealed content baseline. diff --git a/.claude/skills/rfc-writer/SKILL.md b/.claude/skills/rfc-writer/SKILL.md index aec74697..64688f1f 100644 --- a/.claude/skills/rfc-writer/SKILL.md +++ b/.claude/skills/rfc-writer/SKILL.md @@ -139,17 +139,25 @@ Only include representation details when they are themselves the external contra ## Phase-Aware Authoring -Before editing an existing normative RFC, inspect its phase: +These rules summarize [[RFC-0000:C-STATUS-LIFECYCLE]], +[[RFC-0000:C-PHASE-LIFECYCLE]], and [[RFC-0002:C-LIFECYCLE-VERBS]]. +Before editing an RFC, inspect its status and phase: + +- A draft RFC is published initially through finalization; do not version-bump it. - In `spec`, RFC and Clause edits refine the current version candidate and do not require another version bump. -- In `impl`, `test`, or `stable`, RFC or Clause edits create an unversioned amendment. The `/spec` or `/gov` workflow must release it with a version bump before further phase progression. +- 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 deprecated RFC cannot start another version lifecycle. This helper writes specification content but does not perform lifecycle verbs. Use `/spec` or `/gov` for bump and advance decisions. ## Clause Version Assignment +Clause version assignment follows [[RFC-0000:C-CLAUSE-DEF]] and +[[RFC-0002:C-LIFECYCLE-VERBS]]. + Clause `since` is lifecycle-owned; do not write it into clause text or try to set it through the generic edit surface: diff --git a/.claude/skills/spec/SKILL.md b/.claude/skills/spec/SKILL.md index f7a4db96..b95365a2 100644 --- a/.claude/skills/spec/SKILL.md +++ b/.claude/skills/spec/SKILL.md @@ -105,9 +105,10 @@ For ADR work: For RFC work: - Edit clauses with `govctl clause edit` -- If the RFC is draft and ready to become normative, ask permission before `govctl rfc finalize <RFC-ID> normative` +- If the RFC is draft, do not bump it; when ready, ask permission before `govctl rfc finalize <RFC-ID> 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 the RFC is deprecated, do not edit or version-bump it - Use `govctl rfc edit <RFC-ID> changelog.*` for current-version changelog corrections; these do not change version, phase, or the sealed content signature Semver guidance for RFC amendments: @@ -171,7 +172,7 @@ If the task grows into implementation work, stop here and hand off to `/gov`. 1. Inspect the RFC phase with `govctl rfc show <RFC-ID>` 2. Edit the clause text with `govctl clause edit` 3. Run **rfc-reviewer** -4. If the RFC was already in `spec`, keep the same version; otherwise ask permission, then run `govctl rfc bump <RFC-ID> --patch -m "Clarify wording"` +4. For a draft RFC, keep the version and finalize when ready; for a normative RFC already in `spec`, keep the current version; for a normative RFC in `impl`, `test`, or `stable`, ask permission, then run `govctl rfc bump <RFC-ID> --patch -m "Clarify wording"` 5. Run `govctl check` and `govctl render` ### Prepare a deprecation plan without implementation diff --git a/AGENTS.md b/AGENTS.md index cabd08d9..d879ddcd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,9 +83,12 @@ If behavior is unspecified or ambiguous, escalate. Do not invent. draft → normative → deprecated ``` -- **draft**: Under discussion. Implementation MUST NOT depend on draft RFCs. +Status and phase rules derive from [[RFC-0000:C-STATUS-LIFECYCLE]], +[[RFC-0000:C-PHASE-LIFECYCLE]], and [[RFC-0002:C-LIFECYCLE-VERBS]]. + +- **draft**: Under discussion. Implementation MUST NOT depend on draft RFCs. Finalization is the initial publication boundary; version-changing bumps are forbidden. - **normative**: Binding. Content in `spec` is the current version candidate; entry to `impl` seals that version as the implementation baseline. Later amendments start a new version lifecycle through a version bump per [[ADR-0016]]. -- **deprecated**: Superseded. No new work permitted. +- **deprecated**: Superseded. No new work or version-changing bump is permitted. Reverse transitions are forbidden. @@ -103,6 +106,9 @@ 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. +Clause version assignment follows [[RFC-0000:C-CLAUSE-DEF]] and +[[RFC-0002:C-LIFECYCLE-VERBS]]. + 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index c5ff8285..359fd64e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ Release entries are curated summaries for readers. Work item traceability remain - Lifecycle RFC clauses document current-version spec authoring, implementation-entry sealing, and current-changelog correction boundaries (WI-2026-07-16-001) - Change-only rfc bump --change uses current-version resolution and CLI help documents its no-version-bump behavior (WI-2026-07-16-001) +- Lifecycle RFC clauses define version-changing bump eligibility and preserve changelog-only correction behavior (WI-2026-07-16-002) +- Hand-authored RFC guide documents the draft finalization and normative bump boundary (WI-2026-07-16-002) +- Lifecycle rollback specification defines target path existence and byte-content restoration boundaries (WI-2026-07-16-003) ### Fixed @@ -25,6 +28,8 @@ Release entries are curated summaries for readers. Work item traceability remain - RFC changelog editing rejects historical entries and lifecycle-owned version and date fields (WI-2026-07-16-001) - Clause creation assigns since immediately only for normative spec RFCs, while draft and non-spec amendments remain pending for finalize or bump (WI-2026-07-16-001) - Entering impl seals the current content signature and rejects unresolved pending Clause versions without rewriting them (WI-2026-07-16-001) +- Version-changing bump rejects draft and deprecated RFCs without mutation (WI-2026-07-16-002) +- Draft Clause `since` remains absent after rejected version bumps and is assigned by normative finalization (WI-2026-07-16-002) ## [0.11.0] - 2026-07-16 diff --git a/docs/guide/rfcs.md b/docs/guide/rfcs.md index ea9cc489..72614a50 100644 --- a/docs/guide/rfcs.md +++ b/docs/guide/rfcs.md @@ -62,8 +62,8 @@ commands or automatic assignment instead: ```bash # Lifecycle-managed fields use dedicated verbs -govctl rfc bump RFC-0010 --minor -m "Add new clause for edge case" govctl rfc finalize RFC-0010 normative +govctl rfc bump RFC-0010 --minor -m "Add new clause for edge case" # Correct metadata for the current RFC version only govctl rfc get RFC-0010 changelog @@ -85,6 +85,11 @@ New clause text here 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`. + ## Working with Clauses ### Create a Clause @@ -226,7 +231,9 @@ do not affect the sealed baseline. ## Versioning -RFCs use semantic versioning: +RFCs use semantic versioning after normative finalization. Draft RFCs remain on +their initial version while they are authored, and deprecated RFCs cannot start +a new version lifecycle: ```bash # Bump version with changelog entry diff --git a/docs/rfc/RFC-0000.md b/docs/rfc/RFC-0000.md index a823ad3d..87f19d66 100644 --- a/docs/rfc/RFC-0000.md +++ b/docs/rfc/RFC-0000.md @@ -1,9 +1,9 @@ <!-- GENERATED: do not edit. Source: RFC-0000 --> -<!-- SIGNATURE: sha256:9f73ddfbabb5bba1f7dfc6300ed6dad3cafe77da8bfc3bf90d01438806e09808 --> +<!-- SIGNATURE: sha256:343dc69cafb6aebb839104033582d2159a08ce43085acf7c221bebd9e7f936a4 --> # RFC-0000: govctl Governance Framework -> **Version:** 1.6.0 | **Status:** normative | **Phase:** stable +> **Version:** 1.6.1 | **Status:** normative | **Phase:** stable --- @@ -201,13 +201,13 @@ Clause creation and version assignment MUST follow these rules: - Finalizing a draft RFC as normative MUST assign the RFC's current version to every pending clause. - A new clause in a normative RFC whose phase is `spec` MUST set `since` to the RFC's current version. - A new clause in a normative RFC whose phase is `impl`, `test`, or `stable` MUST be pending. -- A version bump MUST assign the new RFC version to every pending clause that it releases. +- 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. 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 at finalization, amendments created outside `spec` receive a version at 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. > **Tags:** `core`, `schema` @@ -430,6 +430,14 @@ A guard check MUST pass only when the command exits successfully. If `pattern` i ## Changelog +### v1.6.1 (2026-07-16) + +Clarify Clause version assignment at RFC publication boundaries + +#### Fixed + +- Keep draft Clause versions pending until normative finalization + ### v1.6.0 (2026-07-16) Define current-version RFC authoring and changelog invariants diff --git a/docs/rfc/RFC-0002.md b/docs/rfc/RFC-0002.md index 697dd9a1..ed7a1399 100644 --- a/docs/rfc/RFC-0002.md +++ b/docs/rfc/RFC-0002.md @@ -1,9 +1,9 @@ <!-- GENERATED: do not edit. Source: RFC-0002 --> -<!-- SIGNATURE: sha256:5509fece42d884dc1bfe563c684895dc4eb01f4ab2a2c6cfcb6700861fa33946 --> +<!-- SIGNATURE: sha256:f26fede065a991624fd4ae37032750b5b50d761378e2525c38e0a4c51f99aa64 --> # RFC-0002: CLI Resource Model and Command Architecture -> **Version:** 0.13.0 | **Status:** normative | **Phase:** stable +> **Version:** 0.13.2 | **Status:** normative | **Phase:** stable --- @@ -295,6 +295,8 @@ Resource-specific lifecycle and constrained-mutation operations implement state - A later phase transition MUST reject amendment content that differs from the stored signature. 4. `govctl rfc bump <id> <patch|minor|major> -m <message>` + - The version-changing form MUST require normative RFC status. + - Rejection for draft or deprecated RFC status MUST occur without mutation. - Version bumping MUST follow semantic versioning. - A version bump MUST update the changelog automatically. - A version bump MAY include `--change <description>` for additional changelog entries. @@ -305,6 +307,7 @@ Resource-specific lifecycle and constrained-mutation operations implement state - 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. - `--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. @@ -390,14 +393,15 @@ Resource-specific lifecycle and constrained-mutation operations implement state 1. All lifecycle operations MUST validate transitions per [RFC-0001](../rfc/RFC-0001.md) or their resource definition. 2. All lifecycle operations MUST update timestamp fields where applicable. -3. Except for the rollback-failure path in requirement 4, a lifecycle invocation that handles an error and completes through govctl's normal error path with a non-zero exit status MUST restore every governed artifact changed by that invocation byte-for-byte before returning. +3. Except for the rollback-failure path in requirement 4, before a lifecycle invocation exits with a non-zero status, it MUST restore each governed-artifact path it created, deleted, or wrote to the path-existence state observed at baseline and, when that path existed at baseline, to its baseline byte content. 4. If an I/O or storage error prevents complete restoration, the invocation MUST return a non-zero exit status. 5. The rollback-failure diagnostic MUST contain the term `rollback`. 6. The rollback-failure diagnostic MUST state that governed-artifact restoration may be incomplete. -7. The restoration baseline is the artifact content observed after the invocation acquires its exclusive write scope under [RFC-0004:C-SCOPE](../rfc/RFC-0004.md#rfc-0004c-scope) and before its first governed-artifact mutation. -8. Unexpected process termination before normal error completion, host failure, and the rollback-failure path in requirement 4 are outside the lifecycle atomicity guarantee. -9. All lifecycle operations MUST support global `--dry-run` flag. -10. Invalid transitions MUST error with clear explanation of valid transitions. +7. For each governed-artifact path covered by requirement 3, the restoration baseline is whether its resolved path existed and, when it existed, its artifact byte content observed after the invocation acquires its exclusive write scope under [RFC-0004:C-SCOPE](../rfc/RFC-0004.md#rfc-0004c-scope) and before its first governed-artifact mutation. +8. Unexpected process termination before restoration completes, host failure, and the rollback-failure path in requirement 4 are outside the lifecycle atomicity guarantee. +9. Permissions, ownership, timestamps, and other filesystem metadata are outside the lifecycle atomicity guarantee. +10. All lifecycle operations MUST support global `--dry-run` flag. +11. Invalid transitions MUST error with clear explanation of valid transitions. **Rationale:** @@ -408,7 +412,7 @@ Lifecycle operations are resource-specific because: - Release correction has a newest-entry and expected-version boundary. - Each resource has different valid transitions. -Scoping these operations to resources makes their applicability explicit and prevents confusion. Lifecycle atomicity defines the byte content observed when an invocation completes through its normal error path; it does not require a crash-recovery subsystem for the flat-file artifact store. +Scoping these operations to resources makes their applicability explicit and prevents confusion. Lifecycle atomicity defines path existence and byte content for the non-zero exits covered by requirement 3; it does not require filesystem-metadata restoration or a crash-recovery subsystem for the flat-file artifact store. > **Tags:** `cli`, `lifecycle` @@ -812,6 +816,22 @@ Search is discovery across the governance corpus, not a resource-specific CRUD o ## Changelog +### v0.13.2 (2026-07-16) + +Clarify lifecycle rollback restoration state + +#### Changed + +- Define rollback over target path existence and byte content + +### v0.13.1 (2026-07-16) + +Reject version-changing bumps for non-normative RFCs + +#### Fixed + +- Restrict version-changing RFC bump to normative status + ### v0.13.0 (2026-07-16) Specify current-version changelog editing and RFC sealing diff --git a/gov/rfc/RFC-0000/clauses/C-CLAUSE-DEF.toml b/gov/rfc/RFC-0000/clauses/C-CLAUSE-DEF.toml index 83a2eae8..45722fc0 100644 --- a/gov/rfc/RFC-0000/clauses/C-CLAUSE-DEF.toml +++ b/gov/rfc/RFC-0000/clauses/C-CLAUSE-DEF.toml @@ -37,10 +37,10 @@ Clause creation and version assignment MUST follow these rules: - Finalizing a draft RFC as normative MUST assign the RFC's current version to every pending clause. - A new clause in a normative RFC whose phase is `spec` MUST set `since` to the RFC's current version. - A new clause in a normative RFC whose phase is `impl`, `test`, or `stable` MUST be pending. -- A version bump MUST assign the new RFC version to every pending clause that it releases. +- 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. 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 at finalization, amendments created outside `spec` receive a version at 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.""" diff --git a/gov/rfc/RFC-0000/rfc.toml b/gov/rfc/RFC-0000/rfc.toml index fdf78802..a4bd16b2 100644 --- a/gov/rfc/RFC-0000/rfc.toml +++ b/gov/rfc/RFC-0000/rfc.toml @@ -3,7 +3,7 @@ [govctl] id = "RFC-0000" title = "govctl Governance Framework" -version = "1.6.0" +version = "1.6.1" status = "normative" phase = "stable" owners = ["@govctl-org"] @@ -15,7 +15,7 @@ tags = [ "validation", "lifecycle", ] -signature = "6af0805b1349bc7e13119ba6912cac5a7165cc024dc8fef8712f845e0831d7b1" +signature = "8225ff5173cd4af351f8ff01bff00baa3bda2a239b0c41f76799c68b1f210048" [[sections]] title = "Summary" @@ -53,6 +53,12 @@ clauses = ["clauses/C-RELEASE-DEF.toml"] title = "Verification Guard Specification" clauses = ["clauses/C-GUARD-DEF.toml"] +[[changelog]] +version = "1.6.1" +date = "2026-07-16" +notes = "Clarify Clause version assignment at RFC publication boundaries" +fixed = ["Keep draft Clause versions pending until normative finalization"] + [[changelog]] version = "1.6.0" date = "2026-07-16" diff --git a/gov/rfc/RFC-0002/clauses/C-LIFECYCLE-VERBS.toml b/gov/rfc/RFC-0002/clauses/C-LIFECYCLE-VERBS.toml index be502988..b1e1b968 100644 --- a/gov/rfc/RFC-0002/clauses/C-LIFECYCLE-VERBS.toml +++ b/gov/rfc/RFC-0002/clauses/C-LIFECYCLE-VERBS.toml @@ -34,6 +34,8 @@ Resource-specific lifecycle and constrained-mutation operations implement state - A later phase transition MUST reject amendment content that differs from the stored signature. 4. `govctl rfc bump <id> <patch|minor|major> -m <message>` + - The version-changing form MUST require normative RFC status. + - Rejection for draft or deprecated RFC status MUST occur without mutation. - Version bumping MUST follow semantic versioning. - A version bump MUST update the changelog automatically. - A version bump MAY include `--change <description>` for additional changelog entries. @@ -44,6 +46,7 @@ Resource-specific lifecycle and constrained-mutation operations implement state - 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. - `--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. @@ -129,14 +132,15 @@ Resource-specific lifecycle and constrained-mutation operations implement state 1. All lifecycle operations MUST validate transitions per [[RFC-0001]] or their resource definition. 2. All lifecycle operations MUST update timestamp fields where applicable. -3. Except for the rollback-failure path in requirement 4, a lifecycle invocation that handles an error and completes through govctl's normal error path with a non-zero exit status MUST restore every governed artifact changed by that invocation byte-for-byte before returning. +3. Except for the rollback-failure path in requirement 4, before a lifecycle invocation exits with a non-zero status, it MUST restore each governed-artifact path it created, deleted, or wrote to the path-existence state observed at baseline and, when that path existed at baseline, to its baseline byte content. 4. If an I/O or storage error prevents complete restoration, the invocation MUST return a non-zero exit status. 5. The rollback-failure diagnostic MUST contain the term `rollback`. 6. The rollback-failure diagnostic MUST state that governed-artifact restoration may be incomplete. -7. The restoration baseline is the artifact content observed after the invocation acquires its exclusive write scope under [[RFC-0004:C-SCOPE]] and before its first governed-artifact mutation. -8. Unexpected process termination before normal error completion, host failure, and the rollback-failure path in requirement 4 are outside the lifecycle atomicity guarantee. -9. All lifecycle operations MUST support global `--dry-run` flag. -10. Invalid transitions MUST error with clear explanation of valid transitions. +7. For each governed-artifact path covered by requirement 3, the restoration baseline is whether its resolved path existed and, when it existed, its artifact byte content observed after the invocation acquires its exclusive write scope under [[RFC-0004:C-SCOPE]] and before its first governed-artifact mutation. +8. Unexpected process termination before restoration completes, host failure, and the rollback-failure path in requirement 4 are outside the lifecycle atomicity guarantee. +9. Permissions, ownership, timestamps, and other filesystem metadata are outside the lifecycle atomicity guarantee. +10. All lifecycle operations MUST support global `--dry-run` flag. +11. Invalid transitions MUST error with clear explanation of valid transitions. **Rationale:** @@ -147,4 +151,4 @@ Lifecycle operations are resource-specific because: - Release correction has a newest-entry and expected-version boundary. - Each resource has different valid transitions. -Scoping these operations to resources makes their applicability explicit and prevents confusion. Lifecycle atomicity defines the byte content observed when an invocation completes through its normal error path; it does not require a crash-recovery subsystem for the flat-file artifact store.""" +Scoping these operations to resources makes their applicability explicit and prevents confusion. Lifecycle atomicity defines path existence and byte content for the non-zero exits covered by requirement 3; it does not require filesystem-metadata restoration or a crash-recovery subsystem for the flat-file artifact store.""" diff --git a/gov/rfc/RFC-0002/rfc.toml b/gov/rfc/RFC-0002/rfc.toml index 2e1fb150..92ad27b6 100644 --- a/gov/rfc/RFC-0002/rfc.toml +++ b/gov/rfc/RFC-0002/rfc.toml @@ -3,7 +3,7 @@ [govctl] id = "RFC-0002" title = "CLI Resource Model and Command Architecture" -version = "0.13.0" +version = "0.13.2" status = "normative" phase = "stable" owners = ["@govctl-org"] @@ -16,7 +16,7 @@ tags = [ "validation", "release", ] -signature = "8242e784a13b698482afb54a215c799dc65bb36fb705f84fb33e563cb997b475" +signature = "a0a0d520c414eb9a74584ca0d5ca954a2928e4330316b35771151641c7fe98c0" [[sections]] title = "Summary" @@ -36,6 +36,18 @@ clauses = [ "clauses/C-SEARCH-COMMAND.toml", ] +[[changelog]] +version = "0.13.2" +date = "2026-07-16" +notes = "Clarify lifecycle rollback restoration state" +changed = ["Define rollback over target path existence and byte content"] + +[[changelog]] +version = "0.13.1" +date = "2026-07-16" +notes = "Reject version-changing bumps for non-normative RFCs" +fixed = ["Restrict version-changing RFC bump to normative status"] + [[changelog]] version = "0.13.0" date = "2026-07-16" diff --git a/gov/work/2026-07-16-address-valid-pr-review-follow-ups.toml b/gov/work/2026-07-16-address-valid-pr-review-follow-ups.toml new file mode 100644 index 00000000..8b4ea8fe --- /dev/null +++ b/gov/work/2026-07-16-address-valid-pr-review-follow-ups.toml @@ -0,0 +1,40 @@ +#:schema ../schema/work.schema.json + +[govctl] +id = "WI-2026-07-16-003" +title = "Clarify rollback restoration and lifecycle regression coverage" +status = "done" +created = "2026-07-16" +started = "2026-07-16" +completed = "2026-07-16" +refs = [ + "RFC-0002:C-LIFECYCLE-VERBS", + "RFC-0000:C-RFC-DEF", +] +tags = [ + "lifecycle", + "testing", +] + +[content] +description = "Clarify lifecycle rollback restoration boundaries and strengthen regression coverage and source traceability for RFC lifecycle validation." + +[[content.acceptance_criteria]] +text = "Lifecycle rollback specification defines target path existence and byte-content restoration boundaries" +status = "done" +category = "changed" + +[[content.acceptance_criteria]] +text = "RFC deprecation regression test verifies a successful exit and the `deprecated` status" +status = "done" +category = "chore" + +[[content.acceptance_criteria]] +text = "Current-changelog validation cites its governing RFC clause" +status = "done" +category = "chore" + +[[content.acceptance_criteria]] +text = "`just pre-commit` passes" +status = "done" +category = "chore" diff --git a/gov/work/2026-07-16-reject-version-bumps-for-non-normative-rfcs.toml b/gov/work/2026-07-16-reject-version-bumps-for-non-normative-rfcs.toml new file mode 100644 index 00000000..15793435 --- /dev/null +++ b/gov/work/2026-07-16-reject-version-bumps-for-non-normative-rfcs.toml @@ -0,0 +1,50 @@ +#:schema ../schema/work.schema.json + +[govctl] +id = "WI-2026-07-16-002" +title = "Reject version bumps for non-normative RFCs" +status = "done" +created = "2026-07-16" +started = "2026-07-16" +completed = "2026-07-16" +refs = [ + "RFC-0000:C-CLAUSE-DEF", + "RFC-0002:C-LIFECYCLE-VERBS", +] +tags = [ + "lifecycle", + "cli", +] + +[content] +description = "Amend lifecycle specification and implementation so version-changing RFC bump eligibility aligns with the draft finalization boundary, with focused regression coverage." + +[[content.acceptance_criteria]] +text = "Lifecycle RFC clauses define version-changing bump eligibility and preserve changelog-only correction behavior" +status = "done" +category = "changed" + +[[content.acceptance_criteria]] +text = "Version-changing bump rejects draft and deprecated RFCs without mutation" +status = "done" +category = "fixed" + +[[content.acceptance_criteria]] +text = "Draft Clause `since` remains absent after rejected version bumps and is assigned by normative finalization" +status = "done" +category = "fixed" + +[[content.acceptance_criteria]] +text = "Hand-authored RFC guide documents the draft finalization and normative bump boundary" +status = "done" +category = "changed" + +[[content.acceptance_criteria]] +text = "The RFC bump lifecycle test module and `just pre-commit` pass" +status = "done" +category = "chore" + +[[content.acceptance_criteria]] +text = "Agent skills enforce the draft finalization and normative bump boundary" +status = "done" +category = "chore" diff --git a/src/cli/resources/rfc.rs b/src/cli/resources/rfc.rs index aa60b3eb..759fb4fb 100644 --- a/src/cli/resources/rfc.rs +++ b/src/cli/resources/rfc.rs @@ -131,6 +131,7 @@ EXAMPLES: govctl rfc bump RFC-0001 -c \"fix: Correct current-version wording\" NOTES: + - Version-changing bumps require normative RFC status; finalize a draft first. - 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/doc_target/rfc_changelog.rs b/src/cmd/edit/doc_target/rfc_changelog.rs index 0026955b..46b7fa2a 100644 --- a/src/cmd/edit/doc_target/rfc_changelog.rs +++ b/src/cmd/edit/doc_target/rfc_changelog.rs @@ -61,10 +61,10 @@ pub(in crate::cmd::edit) fn get( ) -> DiagnosticResult<()> { let loaded = RfcTomlAdapter::load(config, id)?; let doc = current_changelog_doc(&loaded.data, id)?; - render_target_from_doc(ArtifactType::Rfc, &doc, target, id, NestedGetMode::Allow)?; let root_only = target.path().segments.len() == 1 && target.path().segments[0].index.is_none(); if !root_only { + render_target_from_doc(ArtifactType::Rfc, &doc, target, id, NestedGetMode::Allow)?; return Err(Diagnostic::new( DiagnosticCode::E0804FieldNotEditable, "RFC changelog get supports only the current-entry root path", diff --git a/src/cmd/lifecycle/rfc.rs b/src/cmd/lifecycle/rfc.rs index c488b2d2..d60e7684 100644 --- a/src/cmd/lifecycle/rfc.rs +++ b/src/cmd/lifecycle/rfc.rs @@ -29,6 +29,19 @@ pub fn bump( let mut rfc = read_rfc(config, &rfc_path)?; let refresh_signature_after_write = 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. + if rfc.status != RfcStatus::Normative { + return Err(Diagnostic::new( + DiagnosticCode::E0104RfcInvalidTransition, + format!( + "Cannot bump RFC version while status={}. Version-changing bumps require normative RFC status.", + rfc.status.as_ref() + ), + rfc_id, + )); + } + let releases_content_amendment = ensure_rfc_has_content_amendment(config, &rfc_path, rfc_id)?; diff --git a/src/validate/rfc.rs b/src/validate/rfc.rs index b08d276f..4f328082 100644 --- a/src/validate/rfc.rs +++ b/src/validate/rfc.rs @@ -32,6 +32,8 @@ pub(super) fn validate_rfc(rfc: &RfcIndex, config: &Config, result: &mut Validat )); } + // [[RFC-0000:C-RFC-DEF]] requires exactly one changelog entry for the + // RFC's current version. let current_changelog_count = rfc .rfc .changelog diff --git a/src/write/mod.rs b/src/write/mod.rs index 2522aac1..63ca8886 100644 --- a/src/write/mod.rs +++ b/src/write/mod.rs @@ -511,6 +511,29 @@ mod tests { Ok(()) } + #[test] + fn file_transaction_restores_deleted_file_after_failure() + -> Result<(), Box<dyn std::error::Error>> { + let dir = tempfile::tempdir()?; + let path = dir.path().join("artifact.toml"); + std::fs::write(&path, "original")?; + + let result = with_file_transaction(&[path.as_path()], WriteOp::Execute, || { + std::fs::remove_file(&path).map_err(|err| { + Diagnostic::io_error("delete file", err, path.display().to_string()) + })?; + Err::<(), _>(Diagnostic::new( + crate::diagnostic::DiagnosticCode::E0903UnexpectedError, + "injected failure after deletion", + path.display().to_string(), + )) + }); + + assert!(result.is_err()); + assert_eq!(std::fs::read_to_string(path)?, "original"); + Ok(()) + } + #[cfg(unix)] #[test] fn file_transaction_reports_rollback_failure() -> Result<(), Box<dyn std::error::Error>> { diff --git a/tests/lifecycle_tests/rfc_cases/bump.rs b/tests/lifecycle_tests/rfc_cases/bump.rs index 8cdfb195..123975dc 100644 --- a/tests/lifecycle_tests/rfc_cases/bump.rs +++ b/tests/lifecycle_tests/rfc_cases/bump.rs @@ -76,6 +76,131 @@ fn test_bump_major_version() -> common::TestResult { Ok(()) } +#[test] +fn test_version_bump_rejects_draft_and_preserves_pending_clause() -> common::TestResult { + let temp_dir = init_project()?; + run_commands( + temp_dir.path(), + &[ + &["rfc", "new", "Draft RFC"], + &[ + "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 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", + "Must be rejected", + ]], + )?; + + assert!(output.contains("error[E0104]"), "output: {output}"); + assert!( + output.contains("Version-changing bumps require normative RFC status"), + "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(()) +} + +#[test] +fn test_version_bump_rejects_deprecated_without_mutation() -> common::TestResult { + let temp_dir = init_project()?; + run_commands( + temp_dir.path(), + &[ + &["rfc", "new", "Deprecated RFC"], + &["rfc", "finalize", "RFC-0001", "normative"], + &["rfc", "deprecate", "RFC-0001", "--force"], + ], + )?; + + 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", + "--patch", + "--summary", + "Must be rejected", + ]], + )?; + + assert!(output.contains("error[E0104]"), "output: {output}"); + assert!(output.contains("status=deprecated"), "output: {output}"); + assert!(!output.contains("Bumped RFC-0001"), "output: {output}"); + assert_eq!(fs::read(&rfc_path)?, before); + Ok(()) +} + +#[test] +fn test_changelog_only_bump_remains_available_for_non_normative_rfc() -> common::TestResult { + let temp_dir = init_project()?; + let output = run_commands( + temp_dir.path(), + &[ + &["rfc", "new", "Draft RFC"], + &[ + "rfc", + "bump", + "RFC-0001", + "--change", + "fix: Draft changelog correction", + ], + &["rfc", "new", "Deprecated RFC"], + &["rfc", "finalize", "RFC-0002", "normative"], + &["rfc", "deprecate", "RFC-0002", "--force"], + &[ + "rfc", + "bump", + "RFC-0002", + "--change", + "fix: Deprecated changelog correction", + ], + ], + )?; + + assert!(!output.contains("error["), "output: {output}"); + assert!( + output.contains("Added change to RFC-0001 v0.1.0"), + "output: {output}" + ); + assert!( + output.contains("Added change to RFC-0002 v0.1.0"), + "output: {output}" + ); + Ok(()) +} + #[test] fn test_content_bump_restarts_stable_rfc_at_spec() -> common::TestResult { let temp_dir = init_project()?; @@ -564,7 +689,13 @@ fn test_bump_nonexistent_rfc() -> common::TestResult { #[test] fn test_bump_rejects_malformed_unlisted_clause_without_mutation() -> common::TestResult { let temp_dir = init_project()?; - run_commands(temp_dir.path(), &[&["rfc", "new", "Test RFC"]])?; + run_commands( + temp_dir.path(), + &[ + &["rfc", "new", "Test RFC"], + &["rfc", "finalize", "RFC-0001", "normative"], + ], + )?; let rfc_path = temp_dir.path().join("gov/rfc/RFC-0001/rfc.toml"); let original_rfc = fs::read_to_string(&rfc_path)?; @@ -603,6 +734,10 @@ fn test_failed_bump_preserves_files_without_success_output() -> common::TestResu 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"], &[ "clause", "new", diff --git a/tests/lifecycle_tests/rfc_cases/deprecation.rs b/tests/lifecycle_tests/rfc_cases/deprecation.rs index 060f55f4..9a98dc04 100644 --- a/tests/lifecycle_tests/rfc_cases/deprecation.rs +++ b/tests/lifecycle_tests/rfc_cases/deprecation.rs @@ -48,16 +48,21 @@ fn test_deprecate_rfc_preserves_pending_clause_version() -> common::TestResult { let clause_path = temp_dir .path() .join("gov/rfc/RFC-0001/clauses/C-PENDING.toml"); + let rfc_path = temp_dir.path().join("gov/rfc/RFC-0001/rfc.toml"); let before = fs::read(&clause_path)?; let output = run_commands( temp_dir.path(), &[&["rfc", "deprecate", "RFC-0001", "--force"]], )?; + assert!(!output.contains("error["), "output: {output}"); + assert!(output.ends_with("exit: 0\n\n"), "output: {output}"); assert!(!output.contains("Set C-PENDING.since"), "output: {output}"); assert_eq!(fs::read(&clause_path)?, before); let clause: toml::Value = toml::from_str(&fs::read_to_string(&clause_path)?)?; assert!(clause["govctl"].get("since").is_none()); + let rfc: toml::Value = toml::from_str(&fs::read_to_string(&rfc_path)?)?; + assert_eq!(rfc["govctl"]["status"].as_str(), Some("deprecated")); Ok(()) } diff --git a/tests/snapshots/test_help__rfc_bump_help.snap b/tests/snapshots/test_help__rfc_bump_help.snap index 0958a337..629b6e79 100644 --- a/tests/snapshots/test_help__rfc_bump_help.snap +++ b/tests/snapshots/test_help__rfc_bump_help.snap @@ -26,6 +26,7 @@ EXAMPLES: govctl rfc bump RFC-0001 -c "fix: Correct current-version wording" NOTES: + - Version-changing bumps require normative RFC status; finalize a draft first. - 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.