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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/a-change-is-never-both-active-and-archived.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@openspec-ui/core": patch
"@openspec-ui/cli": patch
---

The merge gate refuses a change the pull request puts back among the active ones after its base archived it, so the board cannot show a shipped change as unfinished.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
schema: spec-driven
created: 2026-10-13
follows:
- a-change-is-archived-with-nothing-open
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Design

## Context

The merge gate (`validate` with `--base <ref>`) already reads the base's
tree to compare people, histories and the archive. The duplicate of
`cli-is-visible-on-the-homepage` shows the missing case: nothing read the
base's *archived* names against the working tree's *active* names. ADR 0035
and ADR 0036 make archiving a product action; the gate is the one moment a
pull request can undo it.

## Goals / Non-Goals

**Goals:**

- A pull request that puts a change back after the base archived it is
refused, naming the change and where it was archived.
- The archive's own pull request keeps passing.
- The one existing duplicate is removed from `main`.

**Non-Goals:**

- No Pipeline change. The gate is where a pull request is judged; the board
only draws what the gate let through.

## Decisions

1. **In the gate, with the base.** The rule reads two listings the way
`readArchivedSince` reads the archive: the base's active names and
archived names, and the working tree's active names. Without `--base`
there is nothing to compare, and the rule says nothing. Rejected: a
Pipeline-time check — it would redraw cards but still let the bad pull
request land.
2. **Core computes; the CLI reports.** `readResurrectedChanges` returns the
change and its archived folder, and the CLI prints one `FAIL` line per
change. The `ok` flag folds them in, so the exit code is the contract it
already is.
3. **The archive is exempt.** A change the base still has active is not
named, even if it also appears in the base's archive under an older date:
archiving is exactly the pull request that has the active copy in the
commit before it moves it, and refusing it would jam the product's own
archive. Rejected: refusing both directions — it cannot tell the archive
apart from a resurrection.
4. **The date prefix is stripped before comparing.** An archive folder is
`<date>-<change>`; the active folder is `<change>`. The same strip
`readArchivedSince` already relies on keeps the two keyed by one name.
5. **The stray copy goes with the rule.** The rule and its one existing
instance are the same defect: landing the rule while leaving the copy
would leave the board lying for one more cycle.

## Risks / Trade-offs

- A change archived under an older date and legitimately re-opened later
(a new proposal under the same name) is refused. The repository has no
such case; a change that must live again is given a new name, as its
history already does. Documented, not a defect.
- The comparison needs the base's tree, which the gate already fetches at
depth one; no new fetch.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Proposal

## Why

On 2026-10-10 `cli-is-visible-on-the-homepage` was archived: pull request
#881 renamed its folder into `openspec/changes/archive/2026-10-10-…`. Eight
minutes later pull request #883 ("add 2 more changes") added the same change
back among the active ones, every task open, and the merge gate passed it.
The Pipeline then drew a change as unfinished that had already shipped.

The merge gate already compares the people and the histories against the
base, and holds each archived change to the open-item rule; it had no rule
for the reverse — a change put back after its base archived it. A change
being both active and archived makes the board lie, and nothing mechanical
refused the one pull request that could do it.

## What Changes

- The merge gate, with `--base <ref>`, refuses every active change whose
name the base has archived, naming the change and its archived folder.
- The rule does not fire for the change a pull request is archiving: the
archive's own pull request holds the active copy in the commit before it
moves it, and the base still has it active.
- The one existing duplicate, `cli-is-visible-on-the-homepage`, is removed:
its archived copy is the source of truth.

## Capabilities

### New Capabilities

None.

### Modified Capabilities

- `ci-cli`: the merge gate refuses a change that is both active and
archived.

## Impact

`packages/core` (`resurrected-changes.ts`), `packages/cli`
(`openspec-validate.ts`, `main.ts`), the merge-gate tests, and
`openspec/specs/ci-cli/spec.md`. No command or event of the protocol
changes; `--base` gains one more comparison in the report it already
prints. A patch for `@openspec-ui/core` and `@openspec-ui/cli`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## ADDED Requirements

### Requirement: The merge gate refuses a change that is both active and archived

With `--base <ref>`, `validate` SHALL fail on every change the working tree
has active whose name the base has archived, naming the change and the
base's archived folder for it. A change the base still has active SHALL NOT
be named: the archive's own pull request holds the active copy in the commit
before it moves it.

#### Scenario: A change put back after its base archived it

- **WHEN** `validate --base origin/main` runs on a branch that has
`cli-is-visible-on-the-homepage` active and `origin/main` has
`2026-10-10-cli-is-visible-on-the-homepage` archived
- **THEN** the report fails, naming the change and that it is archived as
`2026-10-10-cli-is-visible-on-the-homepage`

#### Scenario: The archive's own pull request

- **WHEN** `validate --base origin/main` runs on the branch that archives
`landing`, and `origin/main` still has `landing` active
- **THEN** the change is not named for being both active and archived
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
From a defect found on 2026-10-10: pull request #883 put
`cli-is-visible-on-the-homepage` back among the active changes after the
base archived it, and the merge gate passed it.

## 1. Core

- [x] 1.1 `packages/core/src/resurrected-changes.ts`: `readResurrectedChanges`
returns each active change whose name the base has archived, with its
archived folder; it exempts a change the base still has active, strips the
date prefix, and throws where the base cannot be read. Exported from
`packages/core/src/index.ts`.
- [x] 1.2 `packages/core/src/resurrected-changes.test.ts`: names a change put
back, exempts the archive's own change, ignores a new change and a bare
older archive, reads a name containing a date, and throws on an unreadable
base. Verified to fail with the base-active exemption removed.

## 2. The merge gate

- [x] 2.1 `packages/cli/src/openspec-validate.ts`: `runValidateAll` reads
`readResurrectedChanges` when a base is given, folds the findings into
`resurrectedChanges` and into `ok`, and reuses `archiveCheckFailed` where
the base cannot be read.
- [x] 2.2 `packages/cli/src/main.ts`: the text report prints one `FAIL` line
per resurrected change, naming it and its archived folder.
- [x] 2.3 `packages/cli/src/openspec-validate.gate.test.ts`: the gate fails a
change put back, passes the archive's own change, and says nothing without
a base. Verified to fail when `ok` ignores the findings.

## 3. The existing duplicate

- [x] 3.1 `openspec/changes/cli-is-visible-on-the-homepage/` removed: the
active copy with every task open, already archived as
`2026-10-10-cli-is-visible-on-the-homepage`.

## 4. Documents

- [x] 4.1 `openspec/specs/ci-cli/spec.md` gains the requirement, carried by
this change's delta.
- [x] 4.2 A changeset: core, cli - patch.

## 5. Checks

- [x] 5.1 `npm run typecheck && npm run lint`, and the affected core and CLI
tests, with two workers.
- [x] 5.2 `openspec validate a-change-is-never-both-active-and-archived
--strict`.
- [x] 5.3 On the real #883 head tree and its base listings: the check names
`cli-is-visible-on-the-homepage`, archived as
`2026-10-10-cli-is-visible-on-the-homepage`.
50 changes: 0 additions & 50 deletions openspec/changes/cli-is-visible-on-the-homepage/design.md

This file was deleted.

35 changes: 0 additions & 35 deletions openspec/changes/cli-is-visible-on-the-homepage/proposal.md

This file was deleted.

This file was deleted.

34 changes: 0 additions & 34 deletions openspec/changes/cli-is-visible-on-the-homepage/tasks.md

This file was deleted.

3 changes: 3 additions & 0 deletions packages/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,9 @@ function formatText(result: ValidateAllResult): string {
for (const problem of result.historyProblems ?? []) {
lines.push(`FAIL ${problem.file}: ${problem.problem}`);
}
for (const one of result.resurrectedChanges ?? []) {
lines.push(`FAIL ${one.changeName} is active, and the base archived it as ${one.archivedAs}`);
}
lines.push(result.ok ? "\nAll changes valid." : "\nOne or more changes failed validation.");
return lines.join("\n");
}
Expand Down
Loading
Loading