Compare commits

..
Author SHA1 Message Date
Tabish Bidiwale d664e740d3 chore: add codeowners 2025-09-19 11:47:41 +10:00
13 changed files with 11 additions and 148 deletions
+1 -2
View File
@@ -145,5 +145,4 @@ docs/
# Claude
.claude/
CLAUDE.md
.DS_Store
CLAUDE.md
+1 -2
View File
@@ -15,7 +15,6 @@
<a href="https://nodejs.org/"><img alt="node version" src="https://img.shields.io/node/v/@fission-ai/openspec?style=flat-square" /></a>
<a href="./LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" /></a>
<a href="https://conventionalcommits.org"><img alt="Conventional Commits" src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=flat-square" /></a>
<a href="https://discord.gg/saTQQGQZ"><img alt="Discord" src="https://img.shields.io/discord/1411657095639601154?logo=discord&logoColor=white&style=flat-square" /></a>
</p>
<p align="center">
@@ -23,7 +22,7 @@
</p>
<p align="center">
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/saTQQGQZ">OpenSpec Discord</a> for help and questions.
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates.
</p>
# OpenSpec
@@ -1,12 +0,0 @@
## Why
Recent cross-shell regressions for `openspec` commands revealed that our existing unit/integration tests do not exercise the packaged CLI or shell-specific behavior. The prior attempt at Vitest spawn tests stalled because it coupled e2e coverage with `pnpm pack` installs, which fail in network-restricted environments. With those findings incorporated, we now need an approved plan to realign the work.
## What Changes
- Adopt a phased strategy that first stabilizes direct spawn testing of the built CLI (`node dist/cli/index.js`) using lightweight fixtures and shared helpers.
- Expand coverage to cross-shell/OS matrices once the spawn harness is stable, ensuring both the direct `node dist/cli/index.js` invocation and the bin shim are exercised with non-TTY defaults and captured diagnostics.
- Treat packaging/install validation as an optional CI safeguard: when a runner has registry access, run a simple pnpm-based pack→install→smoke-test flow; otherwise document it as out of scope while closing remaining hardening items.
## Impact
- Tests: add `test/cli-e2e` spawn suite, helpers, and fixture usage updates; adjust `vitest.setup.ts` as needed.
- Tooling: update GitHub Actions workflows to add shell/OS matrices and (optionally) a packaging install check where network is available.
- Docs: keep `CROSS-SHELL-PLAN.md` aligned with the phased rollout and record any limitations called out during execution.
@@ -1,13 +0,0 @@
## 1. Phase 1 – Stabilize Local Spawn Coverage
- [ ] 1.1 Update `vitest.setup.ts` and helpers so the CLI build runs once and `runCLI` executes `node dist/cli.js` with non-TTY defaults.
- [ ] 1.2 Reuse the minimal fixture set (`tmp-init` or copy) to seed initial spawn tests for help/version, a happy-path `validate`, and a representative error flow.
- [ ] 1.3 Document the Phase 1 coverage details in `CROSS-SHELL-PLAN.md`, noting any outstanding gaps.
## 2. Phase 2 – Expand Cross-Shell Validation
- [ ] 2.1 Exercise both entry points (`node dist/cli.js`, `bin/openspec.js`) in the spawn suite and add diagnostics for shell/OS context.
- [ ] 2.2 Extend GitHub Actions to run the spawn suite across a matrix of shells (bash, zsh, fish, pwsh, cmd) on macOS, Linux, and Windows runners.
## 3. Phase 3 – Package Validation (Optional)
- [ ] 3.1 Add a simple CI job on runners with registry access that runs `pnpm pack`, installs the tarball into a temp workspace (e.g., `pnpm add --no-save`), and executes `pnpm exec openspec --version`.
- [ ] 3.2 If network-restricted environments can’t exercise installs, document the limitation in `CROSS-SHELL-PLAN.md` and skip the job there.
- [ ] 3.3 Close out remaining hardening items from the original cross-shell plan (e.g., `.gitattributes`, chmod enforcement, SIGINT follow-ups) and update the plan accordingly.
@@ -1,19 +0,0 @@
# Update Markdown Parser CRLF Handling
## Problem
Windows users report that `openspec validate` raises “Change must have a Why section” even when the section exists (see GitHub issue #77). The CLI currently splits markdown on `\n` and compares headers without stripping `\r`, so files saved with CRLF line endings keep a trailing carriage return in the header token. As a result the parser fails to detect `## Why`/`## What Changes`, triggering false validation errors and breaking the workflow on Windows-default editors.
## Solution
- Normalize markdown content inside the parser so CRLF and lone-CR inputs are treated as `\n` before section detection, trimming any carriage returns from titles and content comparisons.
- Reuse the normalized reader everywhere `MarkdownParser` is constructed to keep behavior consistent for validation, view, spec, and list flows.
- Add regression coverage that reproduces the failure (unit test around `parseChange` and a CLI spawn/e2e test that writes a CRLF change then runs `openspec validate`).
- Update the `cli-validate` spec to codify the expectation that required sections are recognized regardless of line-ending style.
## Benefits
- Restores correct validation behavior for Windows editors without requiring manual line-ending conversion.
- Locks in the fix with targeted tests so future parser refactors keep cross-platform support.
- Clarifies the spec so downstream work (e.g., cross-shell e2e plan) understands the non-negotiable behavior.
## Risks
- Low: parser normalization touches shared code paths that parse specs and changes; need to ensure no regressions in other command consumers (mitigated by existing parser tests plus the new CRLF fixtures).
@@ -1,9 +0,0 @@
## ADDED Requirements
### Requirement: Parser SHALL handle cross-platform line endings
The markdown parser SHALL correctly identify sections regardless of line ending format (LF, CRLF, CR).
#### Scenario: Required sections parsed with CRLF line endings
- **GIVEN** a change proposal markdown saved with CRLF line endings
- **AND** the document contains `## Why` and `## What Changes`
- **WHEN** running `openspec validate <change-id>`
- **THEN** validation SHALL recognize the sections and NOT raise parsing errors
@@ -1,11 +0,0 @@
## 1. Guard the regression
- [x] 1.1 Add a unit test that feeds a CRLF change document into `MarkdownParser.parseChange` and asserts `Why`/`What Changes` are detected.
- [x] 1.2 Add a CLI spawn/e2e test that writes a CRLF change, runs `openspec validate`, and expects success.
## 2. Normalize parsing
- [x] 2.1 Normalize line endings when constructing `MarkdownParser` so headers and content comparisons ignore `\r`.
- [x] 2.2 Ensure all CLI entry points (validate, view, spec conversion) reuse the normalized parser path.
## 3. Document and verify
- [x] 3.1 Update the `cli-validate` spec with a scenario covering CRLF line endings.
- [x] 3.2 Run the parser and CLI test suites (`pnpm test`, relevant spawn tests) to confirm the fix.
+2 -3
View File
@@ -150,7 +150,7 @@ export class ChangeParser extends MarkdownParser {
private parseRenames(content: string): Array<{ from: string; to: string }> {
const renames: Array<{ from: string; to: string }> = [];
const lines = ChangeParser.normalizeContent(content).split('\n');
const lines = content.split('\n');
let currentRename: { from?: string; to?: string } = {};
@@ -177,8 +177,7 @@ export class ChangeParser extends MarkdownParser {
}
private parseSectionsFromContent(content: string): Section[] {
const normalizedContent = ChangeParser.normalizeContent(content);
const lines = normalizedContent.split('\n');
const lines = content.split('\n');
const sections: Section[] = [];
const stack: Section[] = [];
+1 -6
View File
@@ -12,15 +12,10 @@ export class MarkdownParser {
private currentLine: number;
constructor(content: string) {
const normalized = MarkdownParser.normalizeContent(content);
this.lines = normalized.split('\n');
this.lines = content.split('\n');
this.currentLine = 0;
}
protected static normalizeContent(content: string): string {
return content.replace(/\r\n?/g, '\n');
}
parseSpec(name: string): Spec {
const sections = this.parseSections();
const purpose = this.findSection(sections, 'Purpose')?.content || '';
+5 -11
View File
@@ -22,8 +22,7 @@ const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/;
* Extracts the Requirements section from a spec file and parses requirement blocks.
*/
export function extractRequirementsSection(content: string): RequirementsSectionParts {
const normalized = normalizeLineEndings(content);
const lines = normalized.split('\n');
const lines = content.split('\n');
const reqHeaderIndex = lines.findIndex(l => /^##\s+Requirements\s*$/i.test(l));
if (reqHeaderIndex === -1) {
@@ -103,16 +102,11 @@ export interface DeltaPlan {
renamed: Array<{ from: string; to: string }>;
}
function normalizeLineEndings(content: string): string {
return content.replace(/\r\n?/g, '\n');
}
/**
* Parse a delta-formatted spec change file content into a DeltaPlan with raw blocks.
*/
export function parseDeltaSpec(content: string): DeltaPlan {
const normalized = normalizeLineEndings(content);
const sections = splitTopLevelSections(normalized);
const sections = splitTopLevelSections(content);
const added = parseRequirementBlocksFromSection(sections['ADDED Requirements'] || '');
const modified = parseRequirementBlocksFromSection(sections['MODIFIED Requirements'] || '');
const removedNames = parseRemovedNames(sections['REMOVED Requirements'] || '');
@@ -142,7 +136,7 @@ function splitTopLevelSections(content: string): Record<string, string> {
function parseRequirementBlocksFromSection(sectionBody: string): RequirementBlock[] {
if (!sectionBody) return [];
const lines = normalizeLineEndings(sectionBody).split('\n');
const lines = sectionBody.split('\n');
const blocks: RequirementBlock[] = [];
let i = 0;
while (i < lines.length) {
@@ -167,7 +161,7 @@ function parseRequirementBlocksFromSection(sectionBody: string): RequirementBloc
function parseRemovedNames(sectionBody: string): string[] {
if (!sectionBody) return [];
const names: string[] = [];
const lines = normalizeLineEndings(sectionBody).split('\n');
const lines = sectionBody.split('\n');
for (const line of lines) {
const m = line.match(REQUIREMENT_HEADER_REGEX);
if (m) {
@@ -186,7 +180,7 @@ function parseRemovedNames(sectionBody: string): string[] {
function parseRenamedPairs(sectionBody: string): Array<{ from: string; to: string }> {
if (!sectionBody) return [];
const pairs: Array<{ from: string; to: string }> = [];
const lines = normalizeLineEndings(sectionBody).split('\n');
const lines = sectionBody.split('\n');
let current: { from?: string; to?: string } = {};
for (const line of lines) {
const fromMatch = line.match(/^\s*-?\s*FROM:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/);
@@ -32,7 +32,7 @@ const applyReferences = `**Reference**
const archiveSteps = `**Steps**
1. Identify the requested change ID (via the prompt or \`openspec list\`).
2. Run \`openspec archive <id> --yes\` to let the CLI move the change and apply spec updates without prompts (use \`--skip-specs\` only for tooling-only work).
2. Run \`openspec archive <id>\` to let the CLI move the change and apply spec updates (use \`--skip-specs\` only for tooling-only work).
3. Review the command output to confirm the target specs were updated and the change landed in \`changes/archive/\`.
4. Validate with \`openspec validate --strict\` and inspect with \`openspec show <id>\` if anything looks off.`;
-40
View File
@@ -117,46 +117,6 @@ Given A\nWhen B\nThen C`;
process.chdir(originalCwd);
}
});
it('accepts change proposals saved with CRLF line endings', async () => {
const changeId = 'crlf-change';
const toCrlf = (segments: string[]) => segments.join('\n').replace(/\n/g, '\r\n');
const crlfContent = toCrlf([
'# CRLF Proposal',
'',
'## Why',
'This change verifies validation works with Windows line endings.',
'',
'## What Changes',
'- **alpha:** Ensure validation passes on CRLF files',
]);
await fs.mkdir(path.join(changesDir, changeId), { recursive: true });
await fs.writeFile(path.join(changesDir, changeId, 'proposal.md'), crlfContent, 'utf-8');
const deltaContent = toCrlf([
'## ADDED Requirements',
'### Requirement: Parser SHALL accept CRLF change proposals',
'The parser SHALL accept CRLF change proposals without manual edits.',
'',
'#### Scenario: Validate CRLF change',
'- **WHEN** a developer runs openspec validate on the proposal',
'- **THEN** validation succeeds without section errors',
]);
const deltaDir = path.join(changesDir, changeId, 'specs', 'alpha');
await fs.mkdir(deltaDir, { recursive: true });
await fs.writeFile(path.join(deltaDir, 'spec.md'), deltaContent, 'utf-8');
const originalCwd = process.cwd();
try {
process.chdir(testDir);
expect(() => execSync(`node ${bin} validate ${changeId}`, { encoding: 'utf-8' })).not.toThrow();
} finally {
process.chdir(originalCwd);
}
});
});
-19
View File
@@ -169,25 +169,6 @@ Some general description of changes without specific deltas`;
expect(change.deltas).toHaveLength(0);
});
it('parses change documents saved with CRLF line endings', () => {
const crlfContent = [
'# CRLF Change',
'',
'## Why',
'Reasons on Windows editors should parse like POSIX environments.',
'',
'## What Changes',
'- **alpha:** Add cross-platform parsing coverage',
].join('\r\n');
const parser = new MarkdownParser(crlfContent);
const change = parser.parseChange('crlf-change');
expect(change.why).toContain('Windows editors should parse');
expect(change.deltas).toHaveLength(1);
expect(change.deltas[0].spec).toBe('alpha');
});
});
describe('section parsing', () => {