Skip to content

fix(archive): refuse requirement names differing only in case - #1864

Merged
clay-good merged 1 commit into
Fission-AI:mainfrom
dwin-gharibi:fix-requirement-name-near-miss
Sep 16, 2026
Merged

clay-good merged 1 commit into
Fission-AI:mainfrom
dwin-gharibi:fix-requirement-name-near-miss

Conversation

@dwin-gharibi

@dwin-gharibi dwin-gharibi commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1863.

Why

Requirement names are matched exactly, but two spellings that differ only in
case or interior whitespace are one requirement written twice — the comment on
foldRequirementName (src/core/parsers/requirement-blocks.ts:25-26) calls them
"a mistake, never two requirements". REMOVED (src/core/specs-apply.ts:447) and
the RENAMED source (:400) already refuse such a near-miss. The two operations
that introduce a name did not:

  • ADDED looked the name up with an exact nameToBlock.get(key);
  • the RENAMED target check (specs-apply.ts:413) used an exact
    nameToBlock.has(to).

So an ADDED late fees beside an existing Late Fees, or a rename to
LATE FEES, archived with exit 0 and left two contradicting requirements in the
main spec (Late Fees "30 days" and late fees "15 days"), which
openspec validate then accepted.

Verified against a project built entirely by openspec init + openspec new change, with the main spec written by archiving a first change.

What Changes

  • ADDED: when no requirement matches exactly but one matches after folding
    case and whitespace, buildUpdatedSpec throws
    billing ADDED failed for header "### Requirement: late fees" - "### Requirement: Late Fees" already exists and differs only in case or spacing; use MODIFIED with that exact header to change it, or choose a distinct name.
  • RENAMED target: the same check against every requirement other than the
    rename's own source, with ...; choose a distinct name.
  • The foldRequirementName doc comment now lists ADDED and RENAMED among its
    uses.

Unchanged, and pinned by tests:

  • the exact-duplicate ADDED error and the exact target already exists error;
  • a case-only rename of a requirement to its own name (Late Fees →
    LATE FEES) still applies — the source is exempt from the target check;
  • a case variant of a requirement the same delta REMOVES or RENAMES away is still
    allowed. Operations apply RENAMED → REMOVED → MODIFIED → ADDED, and ADDED is
    checked against the spec as it stands at that point, exactly as the existing
    exact-name check is, so the result holds one requirement, not two;
  • MODIFIED with a fold-only match already refused (- not found) and still does.

Testing

Written first and watched fail, then passing:

  • test/core/specs-apply.requirement-name-near-miss.test.ts — 14 tests
    (ADDED ×8, RENAMED ×5, MODIFIED ×1)
  • test/cli-e2e/archive-requirement-name-near-miss.test.ts — 4 end-to-end
    tests through the built CLI: exact duplicate refused (control), ADDED case
    variant refused with spec and change left untouched, RENAMED case variant
    refused, case-only self-rename still archives

Before the fix: 9 failed / 9 passed (the 9 passing are controls and the
ordering cases that were already right). After: 18 passed.

Edge cases covered: case variant, interior-whitespace variant, a case variant
whose body is identical to the existing requirement (not treated as an
early-sync no-op), two ADDED entries in one delta that differ only in case, a
variant of a requirement removed or renamed away in the same delta, a RENAMED
target colliding with a requirement the same delta removes (refused, as an
exact target already was, since RENAMED runs first).

The round-2 report's own reproduction for this bug (4 tests through the built
CLI) failed on main and passes on this branch.

CI parity, run on Node 20.19.0 (the CI version) with the commands from
.github/workflows/ci.yml:

  • pnpm run build — pass
  • pnpm exec tsc --noEmit — pass
  • pnpm lint — pass
  • changeset status --since=origin/main — pass (patch → 1.13.1), run on
    Node 22 because @changesets/cli 3 needs module.enableCompileCache; the
    CI job that runs it uses Node 24
  • VITEST_MAX_WORKERS=4 pnpm test — 4562 passed, 19 failed of 4581. All 19
    are timing failures on a heavily shared host (load average 8–19 on 6 cores):
    18 × "Test timed out in 10000ms" and 1 × spawnSync npm ETIMEDOUT, in
    store, workset, package-install and CLI e2e files that do not touch spec
    merging. None is an assertion failure.
  • Those 8 files rerun on their own (VITEST_MAX_WORKERS=2): 196 of 197
    passed. The one left is the npm pack + tsc test in
    package-install-scripts.test.ts, which timed out at 11.6s.
  • That npm pack test run alone, twice each on main, this branch and
    fix-closed-requirement-heading: 8 of 8 passed every time.
  • Full suite again on a quieter host (VITEST_MAX_WORKERS=2 pnpm test): 4580
    of 4581 passed. The one failure was that same npm pack test timing out
    (12.1s against the 10s limit). Rerun on its own immediately afterwards, it
    passed 8 of 8 on this branch and on clean main. The test builds a
    throwaway fixture in a temp directory from build.js and the
    package.json scripts, neither of which this branch changes.

Changeset

.changeset/archive-refuses-requirement-name-near-miss.md (patch). Worth noting
for release notes: a change that used to archive by adding a case variant of an
existing requirement is now rejected until the name is fixed.

Notes for reviewers

  • validate still does not compare ADDED names against the main spec — not for
    exact duplicates either — so this stays an archive-time check, consistent with
    the existing exact-duplicate behavior.
  • fix-closed-requirement-heading also edits
    src/core/parsers/requirement-blocks.ts (the normalizeRequirementName body,
    a few lines above the comment changed here). The hunks do not overlap.

Summary by CodeRabbit

  • Bug Fixes
    • Archive operations now reject added or renamed requirements whose names differ from existing requirements only by capitalization or spacing.
    • Error messages identify the conflicting requirement and preserve existing exact-duplicate behavior.
    • Case-only renames of the same requirement remain supported.
    • Changes are safely rejected without modifying the main specification or change records.

ADDED and the RENAMED target compared requirement names exactly, while
REMOVED and the RENAMED source already treated a name that differs only
in case or interior whitespace as a mistyped header. An ADDED `late fees`
beside an existing `Late Fees`, or a rename to `LATE FEES`, therefore
archived cleanly and left two contradicting copies of one requirement in
the main spec, which validate then accepted.

Both now refuse with an error naming the existing requirement. The source
of a rename is exempt from the target check, so a case-only rename of a
requirement to its own name still applies, and ADDED is still checked
against the spec as it stands after the earlier operations, so a variant
of a requirement the same delta removes or renames away is allowed.
Copilot AI lite review requested due to automatic review settings September 12, 2026 16:12
@dwin-gharibi
dwin-gharibi requested a review from a team as a code owner September 12, 2026 16:12
@dwin-gharibi
dwin-gharibi requested review from clay-good and removed request for a team September 12, 2026 16:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: d866c0eb-f9cd-46cc-a946-6cb0a101623d

📥 Commits

Reviewing files that changed from the base of the PR and between 9d4e597 and 34805d2.

📒 Files selected for processing (5)
  • .changeset/archive-refuses-requirement-name-near-miss.md
  • src/core/parsers/requirement-blocks.ts
  • src/core/specs-apply.ts
  • test/cli-e2e/archive-requirement-name-near-miss.test.ts
  • test/core/specs-apply.requirement-name-near-miss.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

The archive flow now rejects ADDED and RENAMED requirement names that differ from existing names only by case or whitespace. Core and end-to-end tests cover rejection, operation ordering, unchanged behavior, and valid case-only self-renames.

Changes

Requirement name validation

Layer / File(s) Summary
Near-miss collision guards
src/core/specs-apply.ts, src/core/parsers/requirement-blocks.ts
RENAMED and ADDED reject case or whitespace variants of existing requirement names. Case-only self-renames remain valid.
Core application validation
test/core/specs-apply.requirement-name-near-miss.test.ts
Core tests cover exact duplicates, near-miss names, operation ordering, self-renames, and unchanged MODIFIED behavior.
Archive integration validation
test/cli-e2e/archive-requirement-name-near-miss.test.ts, .changeset/archive-refuses-requirement-name-near-miss.md
Archive tests verify rejected changes leave files unchanged. The changeset records a patch release.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 34805

The archive validation change has no identified merge-blocking risk.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main archive behavior change: it rejects requirement names that differ from existing names only by case. It omits interior whitespace, but the title does not need to co…
Linked Issues check ✅ Passed The PR addresses issue #1863. buildUpdatedSpec now rejects ADDED and RENAMED names that fold to an existing requirement name. RENAMED excludes its own source, so case-only self-renames remain valid.…
Out of Scope Changes check ✅ Passed The changes stay within issue #1863. The source change implements near-miss rejection. The foldRequirementName documentation, unit tests, CLI tests, and patch changeset directly document, verify, or…
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 4 files. (1 skipped: 1 …
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The near-match guard handles ADDED and RENAMED targets, preserves exact-duplicate behavior and self case-only renames, and respects operation order. Unit and end-to-end tests pass.

@clay-good
clay-good added this pull request to the merge queue Sep 16, 2026
Merged via the queue into Fission-AI:main with commit 767d63c Sep 16, 2026
15 checks passed
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.

ADDED and RENAMED accept a name that differs only in case or spacing, leaving two copies of one requirement

4 participants