Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Relax SHALL/MUST Validation to Support Non-English Documentation

## Why

OpenSpec currently requires all requirements to contain SHALL or MUST keywords, enforced as an ERROR-level validation check. This blocks users who write specifications in non-English languages, as reported in Issue #243.

While SHALL/MUST keywords follow RFC 2119 best practices for English specifications, mandating them prevents:
- International teams from using their native language for internal documentation
- Organizations with non-English documentation standards from adopting OpenSpec
- Multilingual projects from maintaining consistent language across specs

The current validation is overly restrictive. Best practices should be encouraged (WARNING) rather than enforced (ERROR).

## What Changes

**Validation Behavior**:
- Change SHALL/MUST validation from ERROR to WARNING level
- Users writing in English still see recommendations to follow RFC 2119
- Users writing in other languages can proceed without blockers
- `--strict` mode continues to treat warnings as errors for teams that want enforcement

**Impact**: Minimal code change (2 lines in validator.ts), significant international accessibility improvement.

## Impact

**Users Affected**: All users who run `openspec validate`, especially international users

**Breaking Changes**: None - existing valid specs remain valid, previously invalid non-English specs become valid with warnings

**Migration Required**: No

**Dependencies**: None

**Timeline**: Quick fix - single session implementation
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# CLI Validate Specification Changes

## ADDED Requirements

### Requirement: SHALL/MUST validation as WARNING for internationalization
The validator SHALL check for SHALL/MUST keywords in requirement text and emit a WARNING (not ERROR) to recommend RFC 2119 compliance while allowing non-English documentation.

#### Scenario: Requirement without SHALL/MUST in normal mode
- **WHEN** validating a requirement that lacks SHALL or MUST keywords
- **THEN** emit a WARNING-level issue with message "Requirement '[name]' should contain SHALL or MUST keyword (RFC 2119 best practice)"
- **AND** validation SHALL pass (not fail)
- **AND** the requirement SHALL be considered valid

#### Scenario: Requirement without SHALL/MUST in strict mode
- **WHEN** validating with `--strict` flag
- **AND** a requirement lacks SHALL or MUST keywords
- **THEN** emit a WARNING-level issue
- **AND** validation SHALL fail because --strict treats warnings as errors
- **AND** exit code SHALL be 1

#### Scenario: Non-English requirement text
- **GIVEN** a requirement written in a non-English language (e.g., Chinese, Japanese, Spanish)
- **AND** the requirement does not contain SHALL or MUST keywords
- **WHEN** validating the requirement
- **THEN** emit a WARNING recommending SHALL/MUST for English specs
- **AND** validation SHALL pass
- **AND** users can proceed without blockers

#### Scenario: English requirement with SHALL/MUST
- **WHEN** validating a requirement that contains SHALL or MUST
- **THEN** no warning SHALL be emitted
- **AND** validation passes cleanly
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Tasks

## Implementation
- [ ] Update `src/core/validation/validator.ts` line 166: change SHALL/MUST check from ERROR to WARNING for ADDED requirements
- [ ] Update `src/core/validation/validator.ts` line 187: change SHALL/MUST check from ERROR to WARNING for MODIFIED requirements
- [ ] Update error message to be more guidance-focused (recommend rather than require)

## Testing
- [ ] Add test case: requirement without SHALL/MUST passes validation with WARNING
- [ ] Add test case: requirement without SHALL/MUST fails with ERROR in --strict mode
- [ ] Add test case: non-English requirement text (e.g., Chinese "必须") validates successfully with WARNING
- [ ] Run existing test suite to ensure no regressions

## Validation
- [ ] Run `openspec validate relax-shall-must-validation --strict` and resolve all issues
- [ ] Test with English spec (should show WARNING for missing SHALL/MUST)
- [ ] Test with non-English spec (should show WARNING but pass)
- [ ] Test with --strict mode (should fail on WARNING)
29 changes: 29 additions & 0 deletions openspec/specs/cli-validate/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,32 @@ The markdown parser SHALL correctly identify sections regardless of line ending
- **WHEN** running `openspec validate <change-id>`
- **THEN** validation SHALL recognize the sections and NOT raise parsing errors

### Requirement: SHALL/MUST validation as WARNING for internationalization
The validator SHALL check for SHALL/MUST keywords in requirement text and emit a WARNING (not ERROR) to recommend RFC 2119 compliance while allowing non-English documentation.

#### Scenario: Requirement without SHALL/MUST in normal mode
- **WHEN** validating a requirement that lacks SHALL or MUST keywords
- **THEN** emit a WARNING-level issue with message "Requirement '[name]' should contain SHALL or MUST keyword (RFC 2119 best practice)"
- **AND** validation SHALL pass (not fail)
- **AND** the requirement SHALL be considered valid

#### Scenario: Requirement without SHALL/MUST in strict mode
- **WHEN** validating with `--strict` flag
- **AND** a requirement lacks SHALL or MUST keywords
- **THEN** emit a WARNING-level issue
- **AND** validation SHALL fail because --strict treats warnings as errors
- **AND** exit code SHALL be 1

#### Scenario: Non-English requirement text
- **GIVEN** a requirement written in a non-English language (e.g., Chinese, Japanese, Spanish)
- **AND** the requirement does not contain SHALL or MUST keywords
- **WHEN** validating the requirement
- **THEN** emit a WARNING recommending SHALL/MUST for English specs
- **AND** validation SHALL pass
- **AND** users can proceed without blockers

#### Scenario: English requirement with SHALL/MUST
- **WHEN** validating a requirement that contains SHALL or MUST
- **THEN** no warning SHALL be emitted
- **AND** validation passes cleanly

4 changes: 2 additions & 2 deletions src/core/validation/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export class Validator {
if (!requirementText) {
issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" is missing requirement text` });
} else if (!this.containsShallOrMust(requirementText)) {
issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" must contain SHALL or MUST` });
issues.push({ level: 'WARNING', path: entryPath, message: `ADDED "${block.name}" should contain SHALL or MUST keyword (RFC 2119 best practice for English specs)` });
}
const scenarioCount = this.countScenarios(block.raw);
if (scenarioCount < 1) {
Expand All @@ -184,7 +184,7 @@ export class Validator {
if (!requirementText) {
issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" is missing requirement text` });
} else if (!this.containsShallOrMust(requirementText)) {
issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" must contain SHALL or MUST` });
issues.push({ level: 'WARNING', path: entryPath, message: `MODIFIED "${block.name}" should contain SHALL or MUST keyword (RFC 2119 best practice for English specs)` });
}
const scenarioCount = this.countScenarios(block.raw);
if (scenarioCount < 1) {
Expand Down
70 changes: 66 additions & 4 deletions test/core/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ The system SHALL handle all errors gracefully.
expect(report.summary.errors).toBe(0);
});

it('should fail when requirement text lacks SHALL/MUST', async () => {
it('should warn when requirement text lacks SHALL/MUST', async () => {
const changeDir = path.join(testDir, 'test-change-3');
const specsDir = path.join(changeDir, 'specs', 'test-spec');
await fs.mkdir(specsDir, { recursive: true });
Expand All @@ -422,12 +422,74 @@ The system will log all events.
const specPath = path.join(specsDir, 'spec.md');
await fs.writeFile(specPath, deltaSpec);

const validator = new Validator(true);
const validator = new Validator(false); // non-strict mode
const report = await validator.validateChangeDeltaSpecs(changeDir);

// Should pass with warning
expect(report.valid).toBe(true);
expect(report.summary.warnings).toBeGreaterThan(0);
expect(report.issues.some(i => i.level === 'WARNING' && i.message.includes('should contain SHALL or MUST'))).toBe(true);
});

it('should fail in strict mode when requirement lacks SHALL/MUST', async () => {
const changeDir = path.join(testDir, 'test-change-3-strict');
const specsDir = path.join(changeDir, 'specs', 'test-spec');
await fs.mkdir(specsDir, { recursive: true });

const deltaSpec = `# Test Spec

## ADDED Requirements

### Requirement: Logging Feature
**ID**: REQ-LOG-001

The system will log all events.

#### Scenario: Event occurs
**Given** an event
**When** it occurs
**Then** it is logged`;

const specPath = path.join(specsDir, 'spec.md');
await fs.writeFile(specPath, deltaSpec);

const validator = new Validator(true); // strict mode
const report = await validator.validateChangeDeltaSpecs(changeDir);

// Should fail because strict mode treats warnings as errors
expect(report.valid).toBe(false);
expect(report.summary.errors).toBeGreaterThan(0);
expect(report.issues.some(i => i.message.includes('must contain SHALL or MUST'))).toBe(true);
expect(report.issues.some(i => i.level === 'WARNING' && i.message.includes('should contain SHALL or MUST'))).toBe(true);
});

it('should validate non-English requirement text with warning', async () => {
const changeDir = path.join(testDir, 'test-change-chinese');
const specsDir = path.join(changeDir, 'specs', 'test-spec');
await fs.mkdir(specsDir, { recursive: true });

const deltaSpec = `# Test Spec

## ADDED Requirements

### Requirement: 日志功能
**ID**: REQ-LOG-001

系统必须记录所有事件。

#### Scenario: 事件发生
**Given** 有一个事件
**When** 事件发生
**Then** 事件被记录`;

const specPath = path.join(specsDir, 'spec.md');
await fs.writeFile(specPath, deltaSpec);

const validator = new Validator(false);
const report = await validator.validateChangeDeltaSpecs(changeDir);

// Should pass with warning (no SHALL/MUST in Chinese text)
expect(report.valid).toBe(true);
expect(report.summary.warnings).toBeGreaterThan(0);
expect(report.issues.some(i => i.level === 'WARNING' && i.message.includes('should contain SHALL or MUST'))).toBe(true);
});

it('should handle requirements without metadata fields', async () => {
Expand Down