Skip to content

archive: applying a delta rewrites a CRLF spec to LF, turning a one-requirement change into a whole-file diff #1935

Description

@GQAdonis

Problem

Applying a delta to a spec with CRLF line endings rewrites the entire file to LF. The content change is correct, but every line of the file registers as modified, so the actual change is buried in the diff.

This is the write-side counterpart to #77. That issue fixed reading CRLF (the parsers normalize on the way in, and openspec validate is correct today). Nothing restores the convention on the way out.

On Windows, core.autocrlf=true is the Git default, so specs are CRLF on disk and this hits on every archive.

Reproduce

git init repro && cd repro
mkdir -p openspec/specs/demo openspec/changes/test/specs/demo

# a spec with CRLF endings, as a Windows checkout produces
printf '# demo Specification\r\n\r\n## Purpose\r\n\r\nDemo.\r\n\r\n## Requirements\r\n\r\n### Requirement: Existing Thing\r\nThe system SHALL do the existing thing.\r\n\r\n#### Scenario: It works\r\n- **WHEN** invoked\r\n- **THEN** it works\r\n' > openspec/specs/demo/spec.md

# a change adding one requirement
printf '## ADDED Requirements\r\n\r\n### Requirement: Another Thing\r\nThe system SHALL do another thing.\r\n\r\n#### Scenario: Another works\r\n- **WHEN** invoked\r\n- **THEN** it works\r\n' > openspec/changes/test/specs/demo/spec.md
printf '# Test\r\n\r\n## Why\r\n\r\nTesting the CRLF round-trip behavior in git diffs.\r\n\r\n## What Changes\r\n\r\n- Add another thing.\r\n' > openspec/changes/test/proposal.md
printf '## 1. Tasks\r\n\r\n- [x] 1.1 Do it\r\n' > openspec/changes/test/tasks.md

git add -A && git -c user.email=t@t -c user.name=t commit -qm base
openspec archive test --yes
git diff --numstat openspec/specs/demo/spec.md

Result

21	14	openspec/specs/demo/spec.md

The change added one requirement (~7 lines). file confirms the spec is no longer CRLF:

openspec/specs/demo/spec.md: ASCII text

Expected

7	0	openspec/specs/demo/spec.md

with the file still reported as ASCII text, with CRLF line terminators.

Notes

writeUpdatedSpec in src/core/specs-apply.ts writes the rebuilt string as-is. The rebuilt string is assembled entirely with \n because the parsers normalized on read, so whatever the file used originally is lost.

FileSystemUtils.updateFileWithMarkers has the same shape: it composes the managed block with \n and splices it into the existing content, so installing shell completions into a CRLF .bashrc or .zshrc leaves the file with mixed endings. bash reports a stray \r there as $'\r': command not found.

Worth noting why CI does not catch this: .gitattributes pins only skills/** to LF, and the test fixtures are written by the tests themselves using \n, so the windows-latest job only ever exercises LF content.

I have a fix with tests and am happy to open a PR.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions