Skip to content

feat(validate): let a MODIFIED block declare a renamed scenario - #1843

Draft
brettheap wants to merge 1 commit into
Fission-AI:mainfrom
brettheap:fix/scenario-rename-declaration
Draft

feat(validate): let a MODIFIED block declare a renamed scenario#1843
brettheap wants to merge 1 commit into
Fission-AI:mainfrom
brettheap:fix/scenario-rename-declaration

Conversation

@brettheap

Copy link
Copy Markdown

Refs #1793, #1697 — this is a proposal, not a fix, so it does not close either issue itself (see "Process note").

This answers #1697's Q1 ("is an explicit scenario-level operator the
direction?") with a concrete spelling and design rationale. Its Q2 (should
it also cover REMOVED Scenario?) is deliberately left open — see "Deliberately
out of scope" in design.md — so that issue should stay open for it.

Process note, read this first

Per CONTRIBUTING §2 this is a format change ("anything that changes OpenSpec's
architecture needs an OpenSpec change proposal first... Open it as a PR
containing only openspec/changes/<name>/ and wait for it to be approved
before you write the code"), so this PR contains only
openspec/changes/add-scenario-rename-declaration/
proposal.md,
design.md, tasks.md, and the two delta specs.

The implementation is written and tested — tasks.md is checked off — but it
is deliberately not in this PR. It sits on a separate branch in my fork,
ready to open as its own PR the moment the format is agreed:

main...brettheap:OpenSpec:fix/scenario-rename-declaration-impl

That link is a read-only diff for reviewers who want to see the shape of the
code before ruling on the format — not a request to review code in this PR.

What

A MODIFIED requirement block may declare that a scenario the main spec still
carries was renamed, not dropped:

## MODIFIED Requirements

### Requirement: Full car park refuses entry
The system SHALL refuse entry when no bay is free, unless the driver holds a permit.

- RENAMED SCENARIO FROM: `#### Scenario: Car arrives at a full car park`
- RENAMED SCENARIO TO: `#### Scenario: Car without a permit arrives at a full car park`

#### Scenario: Car without a permit arrives at a full car park
- **WHEN** a car arrives, no bay is free, and the driver holds no permit
- **THEN** entry is refused

That is #1697's own "Suggested fix" option 1, unchanged.

Why

The loss guard compares scenario TITLES, so a deliberate rename is
indistinguishable from an accidental drop and the only edit that satisfies it is
restoring the old title — reverting the change where the rename was the point.
#1793 is the sharper form: there the narrowed successor contradicts its
predecessor, so "just keep both scenarios" puts two conflicting statements in one
requirement block.

The precedent for the fix is inside this very check: a MODIFIED whose
requirement is renamed away by ## RENAMED Requirements in the same delta is
already skipped (renamedAway). This is that affordance one level down.

Detection is not weakened, and that was the design constraint. @johnmcarbajal's
replay over 75 archived changes (quoted in full in proposal.md) found the guard
catching real losses — 4 caught by hand at archive time, 2 shipped and repaired
by a later change — so this adds no inference and no suppression. An UNDECLARED
omission is reported exactly as today, at the same level, with the same message
and exit code.

Shape

A declaration is a claim, not a suppression. The scenario named by TO: must
really be in the block. Where it is not, no credit is granted, the omission is
still reported as the loss it is, and the unbacked declaration is reported too.
So a declaration can only ever say "this old title is now that new one", and the
new one has to be there to say it.

findMissingCurrentScenarios keeps its signature and its return type; the
declaration enters as extra credit seeded into the tally it already builds. That
is deliberate — #1809 is open on the same function, and this keeps the overlap
textual rather than semantic.

Why NOT #### RENAMED Scenario

The follow-up comment on #1697 suggested a #### RENAMED Scenario heading with
FROM:/TO: bullets under it. That spelling cannot work, and the reason is
worth stating because it looks fine right up until the guard's arithmetic is
wrong: parseScenarioBlocks counts EVERY non-fenced level-4 header as a
scenario, on purpose, because the spec path's countScenarios does and the two
must agree (#1521SCENARIO_HEADER's own comment warns against breaking that
parity). A #### RENAMED Scenario heading would therefore enter the comparison
as a scenario in its own right: the block gets credited with a scenario nobody
wrote. A bullet cannot collide with it.

Alternatives considered

Option Why not
Top-level ## RENAMED Scenarios section (#1793's first ask) A scenario title is only unique within its requirement, so every declaration needs a third line naming the requirement. Worse, it lands on the wrong side of the parity seam: it would have to be threaded from DeltaPlan into both commands, and a caller that missed the new argument would silently lose the declaration — reopening the validate/archive divergence #1477 closed. Inside the block, both commands read it from the one function they already share.
A recognized in-block prose marker (#1793's reported workaround form) Same seam, and it works, but it reads as prose: nothing distinguishes a declaration from a sentence that happens to be phrased that way, and it has no natural place to carry the successor's title as a checkable field. The reporter explicitly said they were not asking for their own spelling.
Per-finding suppression in openspec/config.yaml (#1793's second ask) Moves intent out of the delta and into project config, which is the thing #1793 says makes their current workaround worse than the fix. The reporter ranks it below the operator.
A severity knob for this one check (#1793's third ask) The reporter calls it the weakest option and mentions it only for completeness. It turns a specific judgment into a blanket one.
Infer a rename from scenario bodies #1697's replay settles it: one block produced 6 findings against one requirement, 2 renames and 4 accidents, identical in count, heading and id presence. Intent is not recoverable from structure. This is what the hardening PRs closed.
REMOVED Scenario in the same change (#1697 Q2) A strictly larger semantic: a rename requires a successor and authorizes no loss; a removal authorizes loss and needs its own justification. Shipping it on the rename's evidence would be the wrong trade. Left open.

Scope and safety

  • Opt-in and lexically new: no existing delta can accidentally contain a
    declaration, so a corpus that writes none behaves identically.
  • The one behaviour change for an existing delta is that a line beginning
    RENAMED SCENARIO FROM:/TO: inside a MODIFIED block is now read as a
    declaration rather than prose.
  • The stale-base class stays blocked, which is correct — its remedy is
    reconciling against the current base, not declaring an intent the author never
    had. Cannot rename a scenario — MODIFIED reads a rename as a dropped scenario and blocks archive #1697's replay separates that class for exactly this reason.
  • No new command, flag, config key, or output format. No exit code changes for
    any input that validates today.

Tests (already written, on the linked branch)

  • test/core/parsers/requirement-blocks.test.ts — 18 added: pairing, the three
    accepted name forms, every CommonMark bullet marker, fence masking, unpaired
    halves, absent successor, multiplicity, merge, a declaration in the CURRENT
    spec ignored, and the strip (including byte-identity for a block with none).
  • test/core/validation.scenario-rename.test.ts — 7 added, both reported
    reproductions driven through both commands: accepted and applied, the
    declaration absent from the written spec, already-in-sync on a second archive,
    and refused by both for an absent successor and for an unpaired FROM:.
  • No existing test assertion changed. Three golden hashes in
    test/core/templates/skill-templates-parity.test.ts are updated, because that
    guard pins template payloads byte-for-byte and the guidance edit (documenting
    that the declaration must not be copied into a main spec) is a deliberate
    content change — no assertion logic touched.

I ran the full toolchain myself on the implementation branch before opening
this proposal, in two passes:

  • pnpm build, pnpm exec tsc --noEmit, and pnpm lint are clean. All 68
    tests in the three files this change touches or adds pass, both inside the
    full run and in isolation:
    test/core/parsers/requirement-blocks.test.ts (35), test/core/validation.scenario-rename.test.ts
    (7), test/core/templates/skill-templates-parity.test.ts (26).
  • A first full-suite pass read Tests 4 failed | 4584 passed (4588), all four
    pre-existing and unrelated to this change (two completion-tip timing
    assertions, one file-state lock-contention timeout, one completion-cache
    TTL test that only fails under full-suite parallel load) — I reproduced the
    identical four on unmodified origin/main.
  • A second full-suite pass, on a more heavily loaded machine, surfaced
    additional failures — every one a hard testTimeout/hookTimeout (never an
    assertion) scattered across files this change does not touch (init,
    update, store, workset, doctor, and others). Rather than wave that
    away, I re-ran the exact same file list against a pristine origin/main
    worktree on the same machine and got the same class of failures at a
    comparable rate (e.g. test/core/archive.test.ts alone: 4 failed of 241 on
    both trees, different specific tests each run — consistent with scheduler
    contention, not a regression). The one failing file this change actually
    touches, test/core/specs-apply.security.test.ts, passes 6/6 in isolation.
    Net: build/typecheck/lint are clean, every test this proposal is responsible
    for is green under any load, and nothing failing is new.
  • The repo's own corpus is unaffected: several of this repo's own pending
    openspec/changes/* entries already trip the very check this proposal
    extends (a maintainer call on whether to declare those renames, not mine to
    make).

Notes

  • No changeset, per .changeset/README.md's default of the normal release
    cadence. Say the word and I will add one on the implementation PR — this is
    user-facing enough that you may well want it release-tracked.
  • skills/openspec-sync-specs/SKILL.md moves with
    src/core/templates/workflows/sync-specs.ts via pnpm generate:skills on the
    implementation branch, so the parity test stays green. The guidance change
    matters beyond discoverability: without it an agent following the sync
    workflow would copy the declaration lines into the main spec, which is the one
    thing that guidance says never to do.
  • Not related to fix: scenario-level merge for MODIFIED requirements #843: that changes MODIFIED from whole-block replacement to a
    scenario-level merge, which is a different model for a different problem.
    This proposal assumes today's replacement semantics and would need
    re-reading, not reverting, if fix: scenario-level merge for MODIFIED requirements #843 lands.
  • Independent of fix(validate): report what a MODIFIED block adds, not only what it drops #1809 (open), which prints the counts and added names when the
    guard fires. That says which file to open; this lets an author state an
    intent. Neither needs the other, and the implementation leaves
    findMissingCurrentScenarios's signature alone so the two do not conflict
    semantically.

Provenance

Drafted with Claude Code (Anthropic); I reviewed the design and every line of
the implementation, and ran every command above myself before opening this.

Per CONTRIBUTING's format-change process: a proposal only, ahead of
implementation.

A MODIFIED requirement block's scenario-currency check (validate and
archive) compares scenario titles, so a deliberate rename reads the
same as an accidental drop and the only edit that satisfies the check
is reverting the rename. This proposes a declaration the block can
carry to say a scenario was renamed rather than dropped, answering
Fission-AI#1697's design-review question with a concrete spelling and design
rationale (see design.md for the alternatives considered and why each
was rejected).

An implementation is written and tested; it is on a separate branch,
linked from the PR, pending this proposal's review.

Refs Fission-AI#1793, Fission-AI#1697
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant