Skip to content

refactor: unify requirement reader and surface #498#1281

Merged
clay-good merged 9 commits into
Fission-AI:mainfrom
clay-good:fix-spec-parser-fidelity
Jul 7, 2026
Merged

refactor: unify requirement reader and surface #498#1281
clay-good merged 9 commits into
Fission-AI:mainfrom
clay-good:fix-spec-parser-fidelity

Conversation

@clay-good

@clay-good clay-good commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Status

LGTM — ready to review. Planning artifacts (proposal / design / spec delta / tasks) and the implementation + tests are now all in this PR. Passes openspec validate fix-spec-parser-fidelity --strict; a changeset (patch) is included.

What was wrong

The requirement reader was implemented twice and the two copies had drifted. MarkdownParser.parseRequirements (used by validate <spec>/archive) and Validator.extractRequirementText/countScenarios (used by validate <change>) disagreed in five ways — each reproduced against main with the bundled CLI:

spec reader delta reader
Body capture first line only first line only
Skips **metadata**: no yes
Ignores fenced code in body no no
Counts fenced #### Scenario: no yes
SHALL/MUST predicate substring includes word-boundary \b…\b

Concrete bugs this produced:

How it was fixed

Part A — unify the reader (fixes #361, #418, #312, fenced-scenario). Both readers now delegate to one shared, fence-/metadata-/multi-line-aware extraction in src/core/parsers/requirement-text.ts so they cannot diverge again:

  • Capture the full requirement body up to the first non-fenced #### Scenario:, skipping blank, **metadata**:, and fenced-code lines; run SHALL/MUST detection over the whole body.
  • Count only non-fenced #### headers, so a fenced #### Scenario: no longer counts (parity with validate <spec>).
  • One whole-word \b(SHALL|MUST)\b predicate (containsShallOrMust) shared by the validator and base.schema, replacing the substring/word-boundary split.
  • buildCodeFenceMask extracted into the shared module; MarkdownParser and ChangeParser import the single implementation.

Part B — surface #498 safely. validate <change> emits an INFO note when an ADDED/MODIFIED Requirements section contains a non-### Requirement: level-3 header (one the delta reader silently skips). INFO never changes the valid result, including under --strict (valid = no errors && no warnings), so nothing newly fails.

Recognition tightening was rejected (key finding of the review)

The obvious #498 fix — recognize only ### Requirement: headers — is rejected. Bare ### <statement> headers are a supported, tested requirement format: test/core/validation.test.ts asserts a bare-header spec is valid, and bare headers appear across json-converter/archive/spec tests and the tmp-init fixtures. Tightening would break a large test surface and silently drop requirements from real specs. Part B gives a consistent signal without that breakage.

Proof it works

Safety

  • Write path unaffected: specs-apply rebuilds from raw ### Requirement: blocks (extractRequirementsSection + RequirementBlock.raw), never parseSpec/req.text — so Part A cannot change archived spec content (display/validation only). Displayed text in JSON output and delta descriptions now reflects the full body.
  • Requirement counts in view/list are unchanged.

Notes / scope

Review surface

  • Implementation: src/core/parsers/requirement-text.ts (new shared reader), markdown-parser.ts, change-parser.ts, requirement-blocks.ts, src/core/validation/validator.ts, src/core/schemas/base.schema.ts.
  • Tests: test/core/validation.test.ts, test/core/parsers/markdown-parser.test.ts.
  • Planning: proposal.md, design.md, tasks.md, specs/cli-validate/spec.md; changeset .changeset/spec-parser-reading-fidelity.md.

🤖 Generated with Claude Code

…I#361, Fission-AI#498, Fission-AI#312)

The requirement-parsing layer silently misreads valid Markdown:

- Fission-AI#361: requirement-body extraction returns only the first non-blank line,
  so a SHALL/MUST that wraps onto line 2 fails `validate --strict`.
- Fission-AI#498: `validate` (delta-block parser) and `archive` (full-spec parser)
  recognize requirements by different rules, so a stray `###` header passes
  validate but becomes a phantom requirement that blocks archive.
- Fission-AI#312 (residual): the requirement-body loop breaks on any `#` line without
  consulting the code-fence mask, truncating bodies that contain fenced
  code with `#` comments.

Proposal: one shared, multi-line, fence-aware requirement-body extractor used
by both the validator and the markdown parser; recognize only
`### Requirement:`-prefixed level-3 headers; guarantee validate/archive parity.
Adds regression + parity tests. Fission-AI#559 investigated and deferred (ambiguous root
cause — see design.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clay-good clay-good requested a review from TabishB as a code owner June 29, 2026 23:25
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new fix-spec-parser-fidelity change entry, expands the cli-validate spec, and updates parser/validator code and tests so requirement-body extraction, scenario counting, and SHALL/MUST detection use shared fence-aware logic. It also adds INFO-only handling for non-canonical delta headers.

Changes

Parser Fidelity Fix Planning

Layer / File(s) Summary
Proposal and design documents
openspec/changes/fix-spec-parser-fidelity/.openspec.yaml, openspec/changes/fix-spec-parser-fidelity/proposal.md, openspec/changes/fix-spec-parser-fidelity/design.md
Metadata is added, and the proposal/design describe the extracted-body divergence, shared fence-aware extraction plan, and INFO-only handling for non-canonical delta headers.
Updated spec and implementation checklist
openspec/changes/fix-spec-parser-fidelity/specs/cli-validate/spec.md, openspec/changes/fix-spec-parser-fidelity/tasks.md
The spec and tasks define full-body keyword detection, fence-aware body/scenario boundaries, INFO-only delta-header surfacing, and the matching regression checklist.
Shared parsing and validation helpers
src/core/parsers/change-parser.ts, src/core/parsers/markdown-parser.ts, src/core/parsers/requirement-blocks.ts, src/core/parsers/requirement-text.ts, src/core/schemas/base.schema.ts, src/core/validation/validator.ts
Shared helpers are introduced and wired into parsing, schema refinement, and validation; requirement bodies, scenarios, and normative keywords now use the shared fence-aware logic, and delta validation emits INFO for non-canonical level-3 headers.
Parser and validator tests
test/core/parsers/markdown-parser.test.ts, test/core/validation.test.ts
Tests are updated and expanded for wrapped keywords, metadata skipping, fenced content, CRLF handling, and the INFO-only stray-header case.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #361: Directly matches the wrapped SHALL/MUST validation failure that this PR addresses.
  • Fission-AI/OpenSpec#312: Shares fence-aware markdown parsing changes that ignore fenced content during header/scenario handling.
  • Fission-AI/OpenSpec#1156: Overlaps with the main-spec SHALL/MUST detection path and requirement-body validation logic.

Possibly related PRs

  • Fission-AI/OpenSpec#966: Both PRs adjust markdown parsing so fenced content does not affect requirement or section detection.
  • Fission-AI/OpenSpec#1031: Both touch REQUIREMENT_HEADER_REGEX and requirement-header matching behavior in requirement-blocks.ts.
  • Fission-AI/OpenSpec#1135: Both update validator.ts around delta requirement handling and SHALL/MUST-related diagnostics.

Suggested reviewers

  • alfred-openspec
  • TabishB

Poem

🐇 I nibbled the fence and counted the stars,
Then found every SHALL hiding in jars.
Metadata skipped,
Scenario lines zipped,
Now validator hops true by the bars. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The shared extractor, keyword alignment, fence-aware scenario handling, and INFO-only #498 note satisfy #361, #418, #312, and #498.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes stand out; the added docs, helper exports, validation tweaks, and tests all support the stated fidelity work.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: unifying requirement reading and surfacing the #498 case.
✨ 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.

…vidence

Hardened the proposal after reproducing every claim against main with the
bundled CLI and correcting two inaccuracies:

- Fission-AI#498 reframed: archive does NOT hard-fail. validate passes; archive emits
  NON-BLOCKING phantom "Proposal warnings in proposal.md" because
  validateChange/parseRequirements counts every level-3 header as a
  requirement, while the delta-block parser (validate) and specs-apply
  (rebuild) only recognize canonical `### Requirement:`. It is a consistency
  bug, not data loss. Verified the rebuilt spec is clean.
- Fission-AI#312 reframed: the original repro is already fixed by codeFenceLineMask
  (requirement count verified correct). The residual is a regression hazard:
  the body loop is fence-unaware, harmless only while first-line-only, so the
  multi-line fix must be fence-aware from the start.

Also: unify recognition on the canonical REQUIREMENT_HEADER_REGEX
(/^###\s*Requirement:\s*(.+)$/i, case-insensitive); surfaced a third latent
inconsistency (Zod substring includes('SHALL') vs delta word-boundary
\b(SHALL|MUST)\b) and added a single-predicate requirement; verified zero
non-Requirement level-3 headers in repo specs (CI-safe); added edge-case
scenarios (multi-line spec+delta paths, fenced scenario-looking lines,
REMOVED/RENAMED unaffected, display vs detection); replaced broken relative
links with plain paths. Proposal passes `openspec validate --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (2)
openspec/changes/fix-spec-parser-fidelity/design.md (1)

67-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify the display-text mitigation strategy.

The risk mitigation states "keep display text concise (first line) while running detection over the full body, or document the widened text" — this ambiguity leaves implementers without a single source of truth. Commit to one approach: either preserve first-line display consistently (with full body for validation only), or explicitly widen req.text and document the breaking change. The "or" creates uncertainty for downstream consumers like view and show.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/fix-spec-parser-fidelity/design.md` around lines 67 - 68,
Clarify the mitigation in the spec parser design by choosing one display-text
behavior and reflecting it consistently in the relevant spec text: either keep
`req.text` as the first line while using the full body only for requirement
detection, or explicitly widen `req.text` and document that as a breaking
change. Update the section mentioning `view`/`show` counts and downstream
display so `design.md` states a single source of truth for `req.text`, removing
the current “or” ambiguity.
openspec/changes/fix-spec-parser-fidelity/specs/cli-validate/spec.md (1)

29-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clarify the "whole words" specification.

The requirement states the predicate "SHALL match SHALL or MUST as whole words" but doesn't define what constitutes a word boundary. For spec completeness and to prevent implementation drift, either:

  • Reference the shared predicate by name (e.g., containsShallOrMust), or
  • Include the boundary pattern explicitly (e.g., /\b(SHALL|MUST)\b/).

This ensures the scenario "Keyword detection agrees across paths" has an unambiguous acceptance criterion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/fix-spec-parser-fidelity/specs/cli-validate/spec.md` around
lines 29 - 36, The normative-keyword predicate description is ambiguous because
“whole words” is not defined, which can lead to drift between validation paths.
Update the requirement text in cli-validate/spec.md to either reference the
shared predicate name used by the validator (such as containsShallOrMust) or
state the exact word-boundary rule explicitly so both the delta-spec and
schema-based paths use the same acceptance criterion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@openspec/changes/fix-spec-parser-fidelity/design.md`:
- Around line 67-68: Clarify the mitigation in the spec parser design by
choosing one display-text behavior and reflecting it consistently in the
relevant spec text: either keep `req.text` as the first line while using the
full body only for requirement detection, or explicitly widen `req.text` and
document that as a breaking change. Update the section mentioning `view`/`show`
counts and downstream display so `design.md` states a single source of truth for
`req.text`, removing the current “or” ambiguity.

In `@openspec/changes/fix-spec-parser-fidelity/specs/cli-validate/spec.md`:
- Around line 29-36: The normative-keyword predicate description is ambiguous
because “whole words” is not defined, which can lead to drift between validation
paths. Update the requirement text in cli-validate/spec.md to either reference
the shared predicate name used by the validator (such as containsShallOrMust) or
state the exact word-boundary rule explicitly so both the delta-spec and
schema-based paths use the same acceptance criterion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 693c293e-6ee4-435d-aec0-8110b02cbc6e

📥 Commits

Reviewing files that changed from the base of the PR and between 0b07106 and 6d9f44d.

📒 Files selected for processing (6)
  • openspec/changes/fix-spec-parser-fidelity/design.md
  • openspec/changes/fix-spec-parser-fidelity/proposal.md
  • openspec/changes/fix-spec-parser-fidelity/specs/cli-archive/spec.md
  • openspec/changes/fix-spec-parser-fidelity/specs/cli-validate/spec.md
  • openspec/changes/fix-spec-parser-fidelity/specs/openspec-conventions/spec.md
  • openspec/changes/fix-spec-parser-fidelity/tasks.md
✅ Files skipped from review due to trivial changes (2)
  • openspec/changes/fix-spec-parser-fidelity/tasks.md
  • openspec/changes/fix-spec-parser-fidelity/specs/cli-archive/spec.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • openspec/changes/fix-spec-parser-fidelity/specs/openspec-conventions/spec.md

@clay-good clay-good self-assigned this Jun 29, 2026
… upgrade Fission-AI#312, tier the risk

Second adversarial bulletproofing pass (reproduced everything against main):

- Add Fission-AI#418 (metadata-before-description): live on the spec path
  (req.text = "**ID**: ...") but ALREADY fixed on the delta path. The
  asymmetry is direct evidence for unifying the two extractors.
- Upgrade Fission-AI#312 from "regression hazard" to LIVE bug: a fenced code block
  before the prose line makes req.text = "```bash" on both paths today
  (distinct from the already-fixed section-count manifestation).
- Tier the fixes by risk after auditing the existing test contract
  (markdown-parser.test.ts, 15 tests green on main):
    Tier 1 (false-negative fixes Fission-AI#361/Fission-AI#418/Fission-AI#312): only widens what is read;
      updates one test (:331, which asserts the first-line bug). Fence tests
      (:106/:139) preserved because skip-and-join keeps SHALL-first bodies.
    Tier 2 (recognition tightening Fission-AI#498): canonical ### Requirement: only;
      a deliberate behavior change that updates bare-header tests (:258/:310)
      and needs a migration note. Flagged for maintainer decision, with a
      conservative opt-in-lint alternative documented.
- Surface the four-column extractor divergence table (capture / metadata /
  recognition / predicate) and an explicit "Behavior changes and test impact"
  section with exact test line refs.

Proposal passes `openspec validate --strict`. Does not claim Fission-AI#1156 (PR Fission-AI#1280).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clay-good clay-good changed the title Propose: spec parser reading fidelity — multi-line + fence-aware requirement parsing, validate/archive parity (#361, #498, #312) Propose: spec parser reading fidelity — multi-line/metadata/fence-aware parsing + recognition parity (#361, #418, #312, #498) Jun 29, 2026
…d-scenario bug, Fission-AI#498→safe INFO

Third deep pass found the prior Tier 2 (recognition tightening to
`### Requirement:`) was the WRONG fix and over-scoped:

- Bare `### <statement>` headers are a SUPPORTED, tested requirement format:
  test/core/validation.test.ts asserts a bare-header spec is valid, and bare
  headers appear across json-converter/archive/spec tests and tmp-init
  fixtures. Tightening would break a large test surface and silently drop
  requirements from real specs. REJECTED, with evidence documented.
- Replace the Fission-AI#498 fix with a SAFE INFO note in validate <change> that surfaces
  non-`### Requirement:` headers in delta sections. INFO never fails validation
  (strict: valid = no errors && no warnings), so nothing newly fails.
- New bug found and folded in: countScenarios is fence-unaware, so a `####
  Scenario:` inside a fenced block is counted as real — a malformed delta passes
  validate <change> while validate <spec> correctly fails. Same fence family.
- Proved the archive WRITE path is independent of the reader: specs-apply
  rebuilds from raw `### Requirement:` blocks (extractRequirementsSection +
  RequirementBlock.raw), never parseSpec/req.text → Part A cannot change
  archived content.

Net effect: recognition is unchanged, so the proposal now updates exactly ONE
existing test (:331, the first-line assertion) instead of breaking bare-header
tests. Consolidated to a single cli-validate delta (dropped cli-archive and
openspec-conventions deltas). Dropped the no-space-header hypothesis (no
divergence). Passes `openspec validate --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clay-good clay-good changed the title Propose: spec parser reading fidelity — multi-line/metadata/fence-aware parsing + recognition parity (#361, #418, #312, #498) Propose: spec parser reading fidelity — unify the requirement reader (fence/metadata/multi-line aware) + surface #498 (#361, #418, #312) Jun 30, 2026

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (2)
openspec/changes/fix-spec-parser-fidelity/specs/cli-validate/spec.md (2)

42-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify scope: does the INFO-note requirement apply only to change deltas?

The requirement specifies "When an ## ADDED/## MODIFIED Requirements section in a change delta contains..." which correctly scopes it to change deltas. However, the relationship with the subsequent bare-header requirement (lines 51-54) could be clearer. Consider adding an explicit cross-reference note that this INFO-note behavior is specific to change-delta parsing and does not apply to main-spec bare headers, to prevent implementer confusion about whether bare ### <statement> headers in main specs should also trigger INFO notes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/fix-spec-parser-fidelity/specs/cli-validate/spec.md` around
lines 42 - 50, Clarify that the INFO-note behavior is scoped only to
change-delta parsing in the validate spec, not main-spec bare headers. Update
the Requirement and Scenario around the `## ADDED`/`## MODIFIED Requirements`
section to explicitly reference the separate bare-header behavior so
implementers know `openspec validate` should emit INFO only for non-canonical
level-3 headers inside change deltas, while the bare `### <statement>` handling
in main specs remains unchanged.

22-22: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Capitalize "Markdown" as proper noun.

"Markdown" is a proper noun referring to the specific markup language and should be capitalized.

-The validator and markdown parser SHALL ignore lines inside fenced code blocks
+The validator and Markdown parser SHALL ignore lines inside fenced code blocks
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/fix-spec-parser-fidelity/specs/cli-validate/spec.md` at line
22, Update the spec wording in the CLI validate documentation so “markdown” is
capitalized as “Markdown” wherever it appears in the affected requirement text.
Keep the existing meaning unchanged, and adjust the sentence in the spec entry
that describes the validator and Markdown parser behavior so the proper noun is
consistently capitalized.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@openspec/changes/fix-spec-parser-fidelity/specs/cli-validate/spec.md`:
- Around line 42-50: Clarify that the INFO-note behavior is scoped only to
change-delta parsing in the validate spec, not main-spec bare headers. Update
the Requirement and Scenario around the `## ADDED`/`## MODIFIED Requirements`
section to explicitly reference the separate bare-header behavior so
implementers know `openspec validate` should emit INFO only for non-canonical
level-3 headers inside change deltas, while the bare `### <statement>` handling
in main specs remains unchanged.
- Line 22: Update the spec wording in the CLI validate documentation so
“markdown” is capitalized as “Markdown” wherever it appears in the affected
requirement text. Keep the existing meaning unchanged, and adjust the sentence
in the spec entry that describes the validator and Markdown parser behavior so
the proper noun is consistently capitalized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ec15f7df-979b-49a9-a47c-2d854d0ef52e

📥 Commits

Reviewing files that changed from the base of the PR and between 5506711 and c63913b.

📒 Files selected for processing (4)
  • openspec/changes/fix-spec-parser-fidelity/design.md
  • openspec/changes/fix-spec-parser-fidelity/proposal.md
  • openspec/changes/fix-spec-parser-fidelity/specs/cli-validate/spec.md
  • openspec/changes/fix-spec-parser-fidelity/tasks.md
✅ Files skipped from review due to trivial changes (3)
  • openspec/changes/fix-spec-parser-fidelity/design.md
  • openspec/changes/fix-spec-parser-fidelity/proposal.md
  • openspec/changes/fix-spec-parser-fidelity/tasks.md

@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.

Reviewed the proposal against the parser and validator paths. This is the right fix shape: unify requirement body/scenario reading while preserving the separate specs-apply write path and avoiding a breaking tightening of bare requirement headers. Approved, with focused tests needed for multiline bodies, metadata-before-prose, fenced code, fenced scenario exclusion, and INFO-only skipped-header surfacing.

…aware (Fission-AI#361, Fission-AI#418, Fission-AI#312); surface Fission-AI#498

The requirement reader was implemented twice — MarkdownParser.parseRequirements
(validate <spec>/archive) and Validator.extractRequirementText/countScenarios
(validate <change>) — and the two had drifted. Both now delegate to one shared,
fence-/metadata-/multi-line-aware extraction in parsers/requirement-text.ts so
they cannot diverge again.

Part A — unify the reader:
- Capture the full requirement body up to the first non-fenced `#### Scenario:`,
  skipping blank, `**metadata**:`, and fenced-code lines; run SHALL/MUST
  detection over the whole body. Fixes a wrapped keyword being dropped (Fission-AI#361),
  metadata before the description failing validate <spec> (Fission-AI#418), and a fenced
  block before the prose line becoming the requirement text (Fission-AI#312).
- Count only non-fenced `#### ` headers, so a `#### Scenario:` inside a fenced
  example no longer counts as a real scenario in validate <change> (parity with
  validate <spec>).
- One whole-word `\b(SHALL|MUST)\b` predicate (containsShallOrMust) shared by the
  validator and base.schema, replacing the substring/word-boundary split.
- Extract buildCodeFenceMask into the shared module; MarkdownParser and
  ChangeParser import it (single fence implementation).

Part B — surface Fission-AI#498 safely:
- validate <change> emits an INFO note when an ADDED/MODIFIED Requirements
  section contains a non-`### Requirement:` level-3 header (one the delta reader
  silently skips). INFO never changes the valid result, including under --strict,
  so nothing newly fails. Recognition is unchanged: bare `### <statement>`
  headers remain a supported requirement format.

Write path is unaffected: specs-apply rebuilds from raw `### Requirement:`
blocks, never req.text, so archived content cannot change. Displayed text in JSON
output and delta descriptions now reflects the full body.

Tests: markdown-parser.test.ts:331 updated to expect the full body; regression
tests added for Fission-AI#361/Fission-AI#418/Fission-AI#312, the fenced scenario, the Fission-AI#498 INFO note, a
single-line guard, and CRLF. Changeset added (patch). tasks.md completed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/parsers/requirement-text.ts`:
- Around line 71-72: Update the scenario header detection in requirement-text so
countScenarios() only counts real `#### Scenario:` headings, not any level-4
heading. Tighten the `SCENARIO_HEADER` pattern to match the documented scenario
format in `requirement-text.ts`, and keep the logic in `countScenarios()`
unchanged aside from using the stricter matcher. Add a regression test covering
a non-scenario H4 like `#### Notes` to confirm it no longer satisfies the “at
least one scenario” rule.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5af75cae-d271-47ec-a695-212ce1643d7f

📥 Commits

Reviewing files that changed from the base of the PR and between c63913b and 4fb8658.

📒 Files selected for processing (10)
  • .changeset/spec-parser-reading-fidelity.md
  • openspec/changes/fix-spec-parser-fidelity/tasks.md
  • src/core/parsers/change-parser.ts
  • src/core/parsers/markdown-parser.ts
  • src/core/parsers/requirement-blocks.ts
  • src/core/parsers/requirement-text.ts
  • src/core/schemas/base.schema.ts
  • src/core/validation/validator.ts
  • test/core/parsers/markdown-parser.test.ts
  • test/core/validation.test.ts
✅ Files skipped from review due to trivial changes (2)
  • .changeset/spec-parser-reading-fidelity.md
  • openspec/changes/fix-spec-parser-fidelity/tasks.md

Comment thread src/core/parsers/requirement-text.ts Outdated
Comment on lines +71 to +72
/** A level-4 scenario header (`#### Scenario: ...`). */
const SCENARIO_HEADER = /^####\s+/;

@coderabbitai coderabbitai Bot Jun 30, 2026

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restrict scenario detection to real #### Scenario: headers.

Line 72 currently matches any level-4 heading, so countScenarios() will accept #### Notes as satisfying the “at least one scenario” rule. This weakens strict delta validation; match the documented scenario format instead and add a regression for a non-scenario H4.

Proposed fix
-const SCENARIO_HEADER = /^####\s+/;
+const SCENARIO_HEADER = /^####\s+Scenario:/i;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** A level-4 scenario header (`#### Scenario: ...`). */
const SCENARIO_HEADER = /^####\s+/;
/** A level-4 scenario header (`#### Scenario: ...`). */
const SCENARIO_HEADER = /^####\s+Scenario:/i;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/parsers/requirement-text.ts` around lines 71 - 72, Update the
scenario header detection in requirement-text so countScenarios() only counts
real `#### Scenario:` headings, not any level-4 heading. Tighten the
`SCENARIO_HEADER` pattern to match the documented scenario format in
`requirement-text.ts`, and keep the logic in `countScenarios()` unchanged aside
from using the stricter matcher. Add a regression test covering a non-scenario
H4 like `#### Notes` to confirm it no longer satisfies the “at least one
scenario” rule.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not taking the tightening: the spec path treats every level-4 child of a requirement as a scenario, so restricting the delta counter to #### Scenario: would reintroduce exactly the cross-reader divergence this PR removes. 44fb1d7 documents the permissive match as deliberate and adds a test locking in that #### Notes satisfies the scenario rule on both paths.

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.

@clay-good Good catch — I hadn't considered that the spec-path (MarkdownParser) already treats every level-4 child heading as a scenario, so tightening the delta-side matcher would only reintroduce the exact divergence this PR is fixing. Keeping #### ... permissive on both sides for parity makes sense here, and locking it in with a test for #### Notes is a good way to prevent regression. Thanks for the context — withdrawing my suggestion.


✏️ Learnings added
Learnt from: clay-good
Repo: Fission-AI/OpenSpec PR: 1281
File: src/core/parsers/requirement-text.ts:0-0
Timestamp: 2026-07-02T22:26:54.642Z
Learning: In the OpenSpec parser fidelity work (src/core/parsers/requirement-text.ts, requirements-related validators), `countScenarios()`'s `SCENARIO_HEADER` regex deliberately matches any level-4 heading (`/^####\s+/`), not just `#### Scenario:`, because `MarkdownParser` (the main-spec reading path) already treats every level-4 child heading under a requirement as a scenario. Tightening the delta-path matcher to only `#### Scenario:` would reintroduce cross-reader divergence between `validate <change>` and `validate <spec>`. This permissive behavior is intentional and is locked in by a regression test asserting `#### Notes` counts as a scenario on both paths (see commit 44fb1d7).

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

…gn edge cases)

Exhaustive verification of the unified reader surfaced two design "edge cases
for tests" not yet covered by committed unit tests:

- Cross-reader predicate agreement: a SHALL substring inside a word ("MARSHALL")
  is rejected identically by validate <change> and validate <spec> — proving the
  one shared whole-word predicate, and guarding against a regression to the old
  substring check.
- Metadata-only body still fails validation (no requirement text) on the delta
  path.

Behavior unchanged; tests only. Full end-to-end parity across all four spec
requirements confirmed against the real Validator; no spurious INFO note fires on
any existing repo change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@clay-good clay-good changed the title Propose: spec parser reading fidelity — unify the requirement reader (fence/metadata/multi-line aware) + surface #498 (#361, #418, #312) [Bug] Unify the requirement reader (fence/metadata/multi-line aware) + surface #498 (#361, #418, #312) Jul 1, 2026

@TabishB TabishB 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.

This review is AI-generated.

}

/** Lines that look like `**ID**: ...` / `**Priority**: ...` metadata. */
const METADATA_LINE = /^\*\*[^*]+\*\*:/;

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.

This also swallows requirement text written as **Constraint**: The system MUST ... — that spec is valid on main, fails here, and then blocks archive.

And when the skip leaves the body empty, the spec path falls back to the header title (markdown-parser.ts:158) while the delta path errors with "missing requirement text" — same content, different verdicts. Maybe skip metadata only when other body text remains, and put one empty-body rule in this shared helper?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 44fb1d7: metadata lines are now skipped only when other body text remains, so a **Constraint**: The system MUST ... body is kept as the requirement text (regression tests on both paths). The empty-body rule now lives in the shared extractRequirementText: both readers fall back to the header title, so the spec path and delta path reach the same verdict for the same block.

Comment thread src/core/parsers/requirement-text.ts Outdated
for (let i = 0; i < bodyLines.length; i++) {
if (mask[i]) continue; // inside a fenced code block
const line = bodyLines[i];
if (SCENARIO_HEADER.test(line)) break; // reached the first real scenario

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.

This doesn't stop at ### lines. On the delta path, a stray ### Background divider plus its notes gets absorbed into the requirement body, so a MUST in the notes now passes the keyword check (it errored on main) — and the new INFO's "is ignored by validation" becomes untrue. Break on any #-prefixed line like the old reader did?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 44fb1d7: the body now ends at the first non-fenced markdown header (/^#{1,6}\s/), so a stray ### Background divider's notes no longer feed the keyword check — added a regression where the MUST in the notes fails validation again and the divider gets its INFO note.

const METADATA_LINE = /^\*\*[^*]+\*\*:/;

/** A level-4 scenario header (`#### Scenario: ...`). */
const SCENARIO_HEADER = /^####\s+/;

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.

Nit: the comment says #### Scenario: but this matches any #### header. Worth noting that's deliberate (spec-path parity) so nobody "fixes" it later.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done in 44fb1d7 — the comment now states the any-#### match is deliberate spec-path parity and warns against tightening it one-sided; also added a test locking in that #### Notes counts on both paths.

Comment thread src/core/parsers/requirement-blocks.ts Outdated
* `### Documentation Requirements` divider that `validate <change>` would
* otherwise pass without comment.
*/
export function findNonRequirementLevel3Headers(

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.

This scanner is fence-aware but splitTopLevelSections/parseRequirementBlocksFromSection aren't, so the note can describe boundaries the real reader doesn't use — e.g. a fenced ## ... line ends the section for the reader and a silently dropped requirement gets no INFO, and one unclosed fence suppresses all notes. Simpler for parseDeltaSpec to report the headers it actually skips?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reworked in 44fb1d7 as suggested: the standalone scanner is gone; parseDeltaSpec now records the non-canonical ### headers it skips while parsing (DeltaPlan.skippedHeaders), so the note uses the reader's real section boundaries and there's no whole-file fence mask to suppress notes across sections. Fence masking is per-section, matching how extractRequirementBody treats those lines as content.

Comment thread src/core/parsers/requirement-blocks.ts Outdated

const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/i;
/** The canonical requirement header the delta reader recognizes. */
export const REQUIREMENT_HEADER_REGEX = /^###\s*Requirement:\s*(.+)\s*$/i;

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.

Nit: nothing imports this — the INFO check ended up in this same file. Drop the export?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Dropped the export in 44fb1d7 (and with the scanner folded into parseDeltaSpec, nothing outside the file needs it).

Comment thread src/core/parsers/requirement-blocks.ts Outdated
after: string;
}

import { buildCodeFenceMask } from './requirement-text.js';

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.

Nit: move up top with the other imports.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Moved up top in 44fb1d7.

Comment thread src/core/validation/validator.ts Outdated
level: 'INFO',
path: entryPath,
line: stray.line,
message: `Header "### ${stray.header}" in ${stray.section} is not a "### Requirement:" header and is ignored by validation. Use "### Requirement: ${stray.header}" if it should be validated as a requirement.`,

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.

Nit: for a nameless ### Requirement: this reads: it "is not a ### Requirement: header ... Use ### Requirement: Requirement:". Special-case the empty name?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Special-cased in 44fb1d7: a nameless ### Requirement: now gets "is missing a requirement name ... Add a name, e.g. "### Requirement: "" instead of the Requirement: Requirement: suggestion. Test added.


### Bug Fixes

- **Requirement reading fidelity** — The requirement reader used by `validate <change>`, `validate <spec>`, and `archive` is now unified into one fence-, metadata-, and multi-line-aware extraction, so the change-delta path and the main-spec path can no longer disagree:

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.

"can no longer disagree" oversells a bit — e.g. a #### Scenario: with no body still passes validate <change> but fails validate <spec> (and blocks archive). Worth softening, and listing the known leftovers in design.md.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Softened in 44fb1d7 to "closing the known divergences" with a pointer to the design doc, and design.md now has a "Known remaining divergences" section listing the empty-scenario case you mention, the deliberate recognition split, and the fence-unaware delta section/block splitting.

@clay-good clay-good changed the title [Bug] Unify the requirement reader (fence/metadata/multi-line aware) + surface #498 (#361, #418, #312) refactor: unify requirement reader and surface #498 Jul 2, 2026
…traction, reader-derived INFO

- Skip **metadata**: lines only when other body text remains; a body written
  entirely as metadata (e.g. `**Constraint**: The system MUST ...`) is kept as
  the requirement text instead of being emptied (was a regression vs main).
- Move the empty-body rule into the shared reader: both paths fall back to the
  header title, so the same block cannot pass one path and fail the other.
- End body extraction at any non-fenced markdown header, restoring old-reader
  parity: a stray `### Background` divider's notes no longer satisfy the
  SHALL/MUST check.
- Replace the standalone fence-aware INFO scanner with skipped-header
  collection inside parseDeltaSpec, so the note reflects exactly what the
  reader skipped (same section boundaries, no whole-file fence mask).
- Special-case the nameless `### Requirement:` INFO message; document that the
  any-#### scenario match is deliberate spec-path parity; un-export
  REQUIREMENT_HEADER_REGEX; move the import up top.
- Soften the changeset claim and list the known remaining divergences in
  design.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mc856

mc856 commented Jul 3, 2026

Copy link
Copy Markdown

Checked this branch out at 44fb1d7 on top of current main (546224e) and ran the parser/validation suites — 69 passing here, and the #361/#418/#312 and fenced-scenario cases behave as described.

One recognition edge slips past the INFO note, found while poking at header variants: ###Requirement: with no space after the hashes. REQUIREMENT_HEADER_REGEX (/^###\s*Requirement:\s*(.+)\s*$/i, requirement-blocks.ts:22) accepts it, but MarkdownParser.parseSections requires whitespace (markdown-parser.ts:90) — matching GFM, which doesn't treat ###Requirement: as a heading at all. For this delta:

## ADDED Requirements

###Requirement: Export headers
The system SHALL export data with headers.

#### Scenario: export
- **WHEN** exporting
- **THEN** headers are included

validateChangeDeltaSpecs under strict returns valid with zero INFO — the reader accepts the no-space header rather than skipping it, so the reader-derived skip note never fires (and the h3 probe at requirement-blocks.ts:215 requires a space anyway). The same content as a main spec fails (Requirement must contain SHALL or MUST plus must have at least one scenario, because the spec reader promotes the #### Scenario: block to a requirement). And extractRequirementsSection on the write path does recognize the no-space header, so the block syncs into the main spec as-is. Net: change validates clean, the synced spec then fails validate <spec> — the same shape as #498.

Both regexes are unchanged from main, so this is pre-existing and I'd call it non-blocking here — but c63913b's message records the no-space hypothesis as dropped with "no divergence", which doesn't match what I see. Requiring one space in REQUIREMENT_HEADER_REGEX (the write path shares the constant, so it stays aligned) or having the scanner flag ###-prefixed lines regardless of spacing would both close it; at minimum it belongs on the known-leftovers list TabishB suggested for design.md.

@alfred-openspec

alfred-openspec commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Jun's no-space edge is real, but I would not tighten REQUIREMENT_HEADER_REGEX in this PR. The branch already treats ###Requirement: as an accepted legacy/header-normalization case in requirement-blocks.test.ts, and changing that shared regex would turn this from a reader-fidelity fix into a canonical Markdown/deprecation change with possible write-path drop behavior.

Please add this to design.md under known remaining divergences instead: no-space ###Requirement: is accepted by the delta/write requirement-block reader, but not by the Markdown spec reader, so it can validate pre-sync and fail post-sync. If we want to close it later, I’d do it as a separate compatibility change: either deprecate no-space headers with an INFO/WARN first, or broaden the skipped-header scanner to catch ^### lines regardless of spacing before tightening recognition.

TabishB and others added 2 commits July 3, 2026 21:59
…-conflict

# Conflicts:
#	src/core/schemas/base.schema.ts
#	src/core/validation/validator.ts
…nown leftover

Jun's edge (reproduced): the delta/write reader's REQUIREMENT_HEADER_REGEX
accepts `###Requirement:` with no space, but MarkdownParser.parseSections
requires whitespace (per GFM) — so a no-space requirement validates as a
change with zero INFO, syncs as-is, then fails validate <spec>. Pre-existing
on main and out of scope here (tightening the shared regex would change
write-path recognition); documented under known remaining divergences with
the follow-up options, folded together with the bullet from the merge
resolution. Corrects c63913b's 'no divergence' note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@clay-good

Copy link
Copy Markdown
Collaborator Author

@mc856 confirmed — I reproduced your exact trace on this branch: the no-space delta validates clean under --strict with zero INFO, and the same content as a main spec fails with Requirement must contain SHALL or MUST + must have at least one scenario. The mechanism is the \s* / \s+ split you identified (requirement-blocks.ts:22 vs markdown-parser.ts:90). You're also right about c63913b — its "no divergence" note is wrong; the hypothesis was dismissed without testing the cross-path sync shape you demonstrated.

Taking @alfred-openspec's route rather than tightening: the no-space form is a tested normalization case (requirement-blocks.test.ts), and REQUIREMENT_HEADER_REGEX is shared with the write path, so tightening it here would turn a reader-fidelity fix into a recognition/deprecation change. a7cc64c records it under "Known remaining divergences" in design.md — full mechanism, why it's accepted, and the two follow-up options (deprecation INFO/WARN first, or broaden the skipped-header collection to any ^### line before tightening) — folded together with the bullet Tabish added in the merge resolution (aae16e0) so there's a single entry.

Re-verified on the merged head (post-#1280): build clean, 1811 tests passing (the 17 zsh-installer failures are local-env-only — oh-my-zsh present; CI unaffected), openspec validate fix-spec-parser-fidelity --strict passes, and the no-space repro still behaves exactly as now documented.

@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.

Re-reviewed latest a7cc64c. The docs now clearly call out the remaining no-space divergence as pre-existing/out-of-scope, and the implementation keeps the safer INFO-only behavior for skipped delta headers. CI is green, so this is good to merge from my side.

@clay-good clay-good requested a review from TabishB July 6, 2026 14:28
@clay-good clay-good added this pull request to the merge queue Jul 7, 2026
Merged via the queue into Fission-AI:main with commit 9a0dfb5 Jul 7, 2026
9 checks passed
@clay-good clay-good deleted the fix-spec-parser-fidelity branch July 7, 2026 16:16
HerbertGao added a commit to HerbertGao/OpenSpec-cn that referenced this pull request Jul 11, 2026
* fix(resolution): converge validate, view, and archive onto canonical resolution (#1182, #1202, #1156) (#1280)

* docs(openspec): propose resolution/validation parity bug bundle (#1182, #1202, #1156)

Planning artifacts only (proposal/design/spec deltas/tasks) for a focused
bug-fix bundle. Three read/validate paths silently diverge from the canonical
logic a sibling command already gets right:

- #1182 validate ignores workspace planning homes that status/instructions resolve
- #1202 view counts only changes/<name>/tasks.md, ignoring the schema tasks glob
- #1156 the SHALL/MUST body-keyword hint fires for deltas but not main specs

Fix converges each divergent path onto the canonical one; parity is asserted by
test. No new surface, no behavior change to the already-correct paths. Validates
--strict.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): bulletproof the parity bundle after adversarial source review

Hardened all three bugs after tracing each path to source with parallel
verification agents. Material corrections:

- #1182: reframed from "workspace planning home resolution" (planning homes are
  repo-only; the feature is now 'stores', and validate already accepts --store)
  to the real, reproducible-at-HEAD mechanism: validate's proposal.md membership
  gate (getActiveChangeIds) vs status/instructions' directory-existence rule
  (validateChangeExists). Pulled nested specs/<area>/<cap> delta discovery and
  bulk --all into scope; noted show.ts sibling.
- #1202: widened from view-only to the shared helper's real blast radius — also
  the archive incomplete-task gate (silently archives unfinished glob-tasks
  changes: data safety) and a 2nd hardcoded copy in change.ts. Pinned apply.tracks
  as the source, change-dir scope containment, and the no-schema fallback. Added
  cli-archive delta for the gate.
- #1156: the main-spec parser discards the requirement header before Zod runs, so
  the hint can't be "lifted" — fix needs header recovery (reuse requirement-blocks)
  + Zod de-dup, and the main-spec message can't be byte-identical to the delta's
  (no ADDED prefix). Pinned the actionable sentence + single-emission + regression
  scenarios across all main-spec surfaces.

4 deltas (cli-validate x2, cli-view, cli-archive). Validates --strict.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): deep-harden the parity bundle with empirical reproduction

Round 2 of bulletproofing: 3 parallel agents reproduced every bug against the
built (pre-fix) CLI and traced fix sites. This pass corrected two substantive
errors in my own prior spec and closed several gaps.

#1202 (two corrections to the prior draft):
- apply.tracks is a FILENAME that selects the tracked artifact, NOT a glob; the
  glob is that artifact's `generates`. status resolves via
  resolveArtifactOutputs(changeDir, artifact.generates). Fixed all wording.
- "view/archive counts equal status" is FALSE: status checks file EXISTENCE, not
  checkboxes (proven: status calls a 3/5 change isComplete:true). Deleted the two
  count-parity scenarios; reframed as resolution-mechanism parity (same files).
- Added schema-resolution-failure fallback (resolveSchema throws; helper must
  catch or view/list/archive crash). Added projectRoot param + 6-site wiring.
- Empirically PROVEN data-safety bug: archive moved a 3/5 unfinished change into
  changes/archive/.

#1182:
- Found a THIRD getActiveChangeIds site (interactive selector, validate.ts:97).
- Proven: --all with a lone proposal-less change exits 0 silently. Added
  exit-code scenarios. Trimmed over-scope: getSpecIds spec-side is NOT a bug;
  no store-specific scenario needed; noun-form scoped out.

#1156:
- Refine-relaxation regression resolved: deltas don't use the Zod refine
  (validate imperatively), so REMOVE it (not relax) once applySpecRules owns both
  header-only and no-keyword cases. Added RENAMED (out-of-scope), lowercase, and
  the new no-body-line-valid-today scenarios; pinned exact message + prefix.

Still 4 deltas; validates --strict; empirical evidence section added to design.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: converge validate/view/archive onto canonical resolution (#1182, #1202, #1156)

Implements the resolution/validation parity bug bundle planned in
openspec/changes/fix-validate-view-resolution-parity. Each fix points a
divergent read/validate path at the canonical implementation a sibling
command already gets right, with parity tests guarding against re-forking.

#1182 — validate resolves changes like status. validate now resolves a
change by directory existence (shared getAvailableChanges) instead of
requiring proposal.md, at all three sites (targeted, bulk, interactive
selector). A scaffolded/still-authoring change is validated rather than
reported Unknown item; a resolved-but-invalid change exits non-zero.
show.ts and the deprecated noun-form change validate are scoped out.

#1182b — validateChangeDeltaSpecs recurses the nested multi-area layout
(specs/<area>/<capability>/spec.md) via a new findDeltaSpecFiles walker,
so a resolved multi-area change validates its deltas instead of reporting
"No delta sections found".

#1202 — getTaskProgressForChange resolves task progress through the
tracked-tasks artifact's generates glob (the same resolveArtifactOutputs
status uses), aggregating checkboxes across every matched tasks.md scoped
to the change dir, with a never-throw fallback to a single top-level
tasks.md. Updates all four callers (view/list/archive x2) for the new
projectRoot arg and folds the second copy in change.ts onto the helper.
Fixes view's Draft misclassification and the archive incomplete-task
gate that let an unfinished glob-tasks change archive (data safety).

#1156 — the SHALL/MUST body-keyword hint applies to main specs.
applySpecRules recovers the requirement header via extractRequirementsSection
and emits the targeted hint (header-only) or generic message (no keyword),
exactly once; the Zod refine is removed (deltas never used it). The
actionable sentence is byte-identical to the change-delta path.

Adds parity/regression tests (Decision 7): validate<->status resolution
incl. exit code, view/archive resolve the same files as status, and the
main-spec<->delta actionable-sentence parity. Full suite green (1791
passed; only the pre-existing, environment-specific zsh-installer
failures remain). Change validates --strict; all 36 repo specs pass
--specs --strict with no new false positives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: add cloudflare documentation deployment website (#1285)

* docs(website): add Fumadocs documentation site for Cloudflare Pages

Add a self-contained marketing + documentation site under website/, built
with Fumadocs (Next.js) and configured as a static export so it deploys
directly to Cloudflare Pages with no server runtime.

What's included:
- A marketing landing page (hero, the two-folder model, the four core
  ideas, the explore→propose→apply→archive loop, and the "why").
- 13 documentation pages rewritten for clarity and delight: introduction,
  installation, getting started, how commands work, core concepts, the
  workflow, explore first, existing projects, editing a change,
  customization, FAQ, and a reference section (slash commands, CLI,
  supported tools).
- Static client-side search (Orama), per-page Open Graph images, and
  llms.txt / llms-full.txt routes — fitting for an AI-native tool.
- website/README.md with one-table Cloudflare Pages deploy settings
  (root: website, build: npm run build, output: out).

Content is faithful to the docs/ overhaul from #1237, restated in a
simpler, friendlier voice. Verified with a clean `next build` (48 static
pages, no warnings).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(website): sharpen the sell, add a Stores guide

Completes the documentation work begun in #1237 by tightening the
Fumadocs site toward the quality bar of the stores user-guide:

- Intro now opens problem-first ("the requirements lived only in chat"),
  adds an honest "How it compares" table (Spec Kit / Kiro / nothing), and
  frames the tradeoff in a "When the ceremony isn't worth it" callout.
- New Stores guide (beta) distilled from docs/stores-beta/user-guide.md:
  the problem, the annotated shape, a five-minute walkthrough with real
  command output, a role-based story, the root-resolution order, and an
  honest-limitations section. Linked from Existing Projects.

Verified with a clean `next build` (51 static pages, no warnings).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(website): make the value tangible — landing sections + Examples page

Continue the #1237 docs completion with a stronger product story:

- Landing page now reads like a real product site:
  - "Works with the tools you already use" strip (15 named assistants + more)
  - "What a change actually looks like" — three real artifacts
    (proposal.md, a spec delta, tasks.md) so the workflow is concrete
  - "The honest middle" comparison block (Spec Kit / Kiro / no specs)
  - Robust hero gradient via color-mix instead of v3 theme() syntax
- New Examples & Recipes page: seven copy-pasteable, narrated walkthroughs
  (small feature, bug fix, explore-first, parallel changes, no-behavior
  refactor with --skip-specs, step-by-step, onboard). Linked from the intro
  and getting-started.

Verified: clean `next build` (54 static pages, no warnings); Tailwind
opacity/color-mix utilities confirmed in the generated CSS; all internal
links resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(website): add favicon, sitemap, and robots for a complete public site

- Branded SVG favicon (app/icon.svg) in the OpenSpec indigo.
- Static sitemap.xml covering the home page and every doc, built from the
  content source and NEXT_PUBLIC_SITE_URL.
- robots.txt allowing all and pointing at the sitemap.

All three are emitted by the static export. Clean `next build`, 57 pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: lead with stores as "why teams adopt OpenSpec"; complete docs coverage

Final pass completing the #1237 documentation work.

Reposition stores (beta) as the team adoption story, consistently:
- README.md gains a prominent "Why teams adopt OpenSpec" section right
  after the demo (cross-repo features, shared requirements, plan before
  code), leading with stores.
- Landing page gains a matching "Why teams adopt OpenSpec" section.
- Docs intro gains a teams card + callout pointing at stores.
- Stores page expanded with full References and Worksets technical
  examples (the cross-team requirements story, workset create/open).

Incorporate the remaining source-doc knowledge so the site is complete:
- New pages: Glossary, Troubleshooting, Multi-Language, and an
  Agents & Automation reference (the machine-readable --json surfaces and
  workflow primitives that make OpenSpec AI-native).
- The Workflow page now covers ff-vs-continue, a three-dimension verify
  example, and the update-vs-start-fresh decision guide.
- Nav restructured with a Help section; reference section gains Agents.

Build hardening: `build` now runs `fumadocs-mdx && next build` so the
content source is always regenerated. Clean build: 69 static pages, no
warnings; all internal links verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(website): fix docs GitHub source links + address review nits

- page.tsx: prefix ViewOptionsPopover githubUrl with website/ so the
  "view/edit source" links resolve to website/content/docs/... instead
  of 404-ing on every deployed docs page (Alfred blocker).
- installation.mdx: note that `yarn global add` is Classic Yarn only and
  point Yarn Berry users at `yarn dlx` / npm / pnpm.
- index.mdx: label the comparison table's first column ("Option").
- (home)/page.tsx: use the shared docsRoute constant for all /docs links
  instead of hardcoded paths.

Verified with `npm run build` in website/ — 69 static pages, and the
built getting-started page links to blob/main/website/content/docs/...

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(website): mirror docs/*.md into the site + auto-deploy on a cadence

Make the repository's docs/*.md the single source of truth for the docs
site instead of maintaining a parallel set of hand-written MDX pages that
silently drift.

- scripts/sync-docs.mjs mirrors ../docs into content/docs/ on every build:
  derives title/description, injects Fumadocs frontmatter (+ githubSource),
  rewrites internal *.md links to /docs routes, and emits meta.json. Pages
  are written as .md so <placeholders>/{braces} in the docs stay literal
  and never break the MDX build.
- docs.sync.config.mjs is the one manifest deciding which docs publish and
  their slug/section/icon. content/docs/ is now generated + git-ignored;
  the curated .mdx pages are removed. The marketing landing page stays
  hand-authored.
- build/dev/types:check run sync:docs first, so the site is always current.
- .github/workflows/deploy-docs.yml rebuilds and deploys to Cloudflare
  Pages via Wrangler on push to docs/**|website/**, daily on a schedule,
  on demand, and as a build-only check on PRs. Needs CLOUDFLARE_API_TOKEN
  + CLOUDFLARE_ACCOUNT_ID secrets and the DOCS_SITE_URL variable.
- source.config.ts carries githubSource so "edit this page" opens the real
  docs/*.md; website/README.md documents the pipeline.

Verified: clean build, 23 pages generated, 78 static pages, no warnings;
all internal doc links resolve; MDX-hazard docs (cli, customization) build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(website): fall back to default site URL when NEXT_PUBLIC_SITE_URL is empty

The deploy workflow passes NEXT_PUBLIC_SITE_URL from the DOCS_SITE_URL repo
variable, which resolves to an empty string when unset. `?? fallback` does
not catch '' (only null/undefined), so `metadataBase: new URL('')` crashed
`next build` with ERR_INVALID_URL while collecting page data. Use `||` so an
empty value also falls back. Verified: `NEXT_PUBLIC_SITE_URL='' npm run build`
now generates all 78 static pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: add reviewing, writing-specs, and team-workflow guides

Fill the biggest gaps a new user hits, in the plain-language voice of the
stores user guide:

- reviewing-changes.md: the two-minute human review of an AI-drafted plan
  before /opsx:apply — what to open, in what order, and the red flags per
  artifact — plus the /opsx:verify pass after code.
- writing-specs.md: what a strong requirement and scenario are made of,
  choosing ADDED/MODIFIED/REMOVED, and right-sizing a change.
- team-workflow.md: how a change maps onto a branch and a pull request,
  reviewing spec deltas in a PR, when to archive, and parallel changes —
  framed as convention, since OpenSpec never touches git.

Wire them into the docs map (README), the site nav (docs.sync.config.mjs),
and light "next steps" cross-links from getting-started, editing-changes,
and workflows. Verified: site builds clean, 26 pages, all internal links
resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(website): add one-time deploy setup checklist + landing-page note

Spell out the three maintainer steps that activate auto-deploy (create the
openspec-docs Pages project, add CLOUDFLARE_API_TOKEN/ACCOUNT_ID secrets,
merge to main), and note that the pipeline mirrors docs on build regardless.
Also flag that openspec.dev is a separate Astro landing page and whether to
keep/port this Fumadocs landing page is a maintainer decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(website): address review feedback on docs-site PR

Maintainer review (TabishB) + Alfred blocker:

- deploy-docs.yml: guard the Cloudflare deploy on `github.ref ==
  refs/heads/main`. A `workflow_dispatch` on a feature branch previously
  passed the guard and, since wrangler hardcodes `--branch=main` (a
  production deploy), would overwrite the live docs site. Non-main
  dispatches are now build-only. Also resolves Alfred's deploy-path blocker.
- package.json: drop the direct `cnfast` dependency and delete the dead
  `lib/cn.ts` (nothing imports it; a class-merge helper isn't used).
- package.json: declare `zod` (^4.4.3) — it was a phantom dep only
  resolving via fumadocs-mdx's hoisted copy. Refresh the lockfile.
- docs page: omit the on-page <DocsDescription>. The frontmatter
  description is derived from the first body paragraph, so it rendered
  the intro twice on every page. Kept in generateMetadata for SEO/OG.
- team-workflow.md: `openspec store create` does an initial commit, so
  scope "never commits" to the user's project and reframe the store
  clause as "never clones or syncs on its own."
- README.md: bump stale "20+ AI assistants" to "30+" to match the site.

Verified: npm run types:check + npm run build pass, 26 docs synced,
intro paragraph now renders once per page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(website): use pnpm to match the rest of the repo

Per maintainer review (TabishB): the root repo is pnpm (ci.yml runs
`pnpm install --frozen-lockfile` against a v9 `pnpm-lock.yaml`), but
`website/` had introduced npm + a `package-lock.json`. Standardize on
one package manager:

- Replace website/package-lock.json with website/pnpm-lock.yaml
  (lockfileVersion 9.0, generated with pnpm v9 to match root).
- deploy-docs.yml: add pnpm/action-setup@v4 (version 9, before
  setup-node, as in ci.yml), switch setup-node to `cache: pnpm` /
  `cache-dependency-path: website/pnpm-lock.yaml`, and
  `npm ci` → `pnpm install --frozen-lockfile`, `npm run build` →
  `pnpm run build`.
- package.json scripts + README: `npm run ...` → `pnpm run ...`.

website/ stays a standalone package (no pnpm-workspace.yaml), as before.

Verified: `pnpm install --frozen-lockfile`, `pnpm run build`, and
`pnpm run types:check` all pass — 26 docs synced, 87/87 static pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: temporarily disable docs deploy

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
Co-authored-by: TabishB <tabishbidiwale@gmail.com>

* Fix `archive` exit code on validation failure (#1311)

* Fix archive exit code on validation failure

In human (non-JSON) mode, openspec archive returned exit code 0 when
validation failed and nothing was archived. The three blocking paths
in ArchiveCommand.run() printed an error message but returned null
silently, leaving process.exitCode at 0. Scripts and CI could not
distinguish a blocked archive from a successful one.

The --json path was already correct (it throws ArchiveBlockedError,
caught by printJsonFailure which sets exitCode = 1). This was an
asymmetry between the two modes for the same failure.

Set process.exitCode = 1 at the three human-mode abort points before
returning null:
  - delta-spec validation failure
  - spec rebuild failure
  - rebuilt-spec validation failure

Legitimate user cancellations (selecting no change, declining a
confirmation prompt) remain exit 0 by design.

Aligns archive with the same exit-code guarantee already approved for
apply instructions in #1250. References #498.

* Add regression test for rebuilt-spec validation exit code

Cover the third archive blocking path (spot 3): buildUpdatedSpec
succeeds but Validator.validateSpecContent rejects the rebuilt
content. Spy on validateSpecContent (same pattern as the existing
--no-validate test) to force the rebuilt spec invalid while the rest
of the flow runs for real, since this branch is otherwise defensive
and nearly unreachable — spot 1 already enforces the same
SHALL/MUST/scenario rules on the delta.

Asserts process.exitCode === 1, the failure is logged, the main spec
is left unchanged, and no archive is created.

* feat(skills): propose /opsx:update planning-artifact update skill (#1278)

* docs(openspec): propose add-update-workflow — graph-driven /opsx:update + cohesive audit

Dogfooded OpenSpec proposal for the missing first-class "update" action:
a /opsx:update workflow that propagates an edit to one artifact across its
downstream dependents (targeted mode) or audits a whole change for stale/
incoherent artifacts (audit mode) — driven by the schema's artifact graph,
never hardcoded filenames, editing planning artifacts only (never code).

- artifact-graph: expose reverse-dependency queries (getDependents/getDownstream)
  + a requires-edge mtime staleness signal (the engine already builds the
  dependents map at graph.ts:98 and discards it).
- cli-artifact-workflow: surface requires/dependents/stale on `openspec status
  --json` and add a `--impact <artifact>` downstream-revisit-order selector.
- opsx-update-skill: the user-facing /opsx:update command (targeted + audit).

Supersedes the proposal-only stub add-artifact-regeneration-support. Addresses
the cluster #1188/#705/#673/#247 (closes), #694/#684/#618 (answers), and is
graph-driven to avoid the #777/#666 hardcoded-artifact-pattern bug class.
Validates clean under `openspec validate --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): make add-update-workflow deterministic & grounded (Tabish review)

Reframe per the steer "more deterministic and grounded in reality":

- Deterministic spine: the CLI computes the impact set (which downstream
  artifacts to revisit, in build order, with paths) as a pure function of
  schema edges + filesystem. The agent only rewrites prose. Grounded in real
  APIs already present: getUnlockedArtifacts (direct dependents), getBuildOrder
  (order), resolveArtifactOutputs (paths); reverse map built at graph.ts:82-87.
- Replace fragile mtime staleness with a newline-normalized SHA-256 content
  digest (reproducible cross-platform). Drift = upstream digest vs recorded
  baseline; no baseline => "unknown", never a false positive. mtime and pure-git
  rejected with rationale; digest ledger is a separable, optional layer.
- Explicit determinism boundary decision (CLI decides files/order/drift; agent
  rewrites). Skill MUST source the file list/order from `openspec status
  --impact`, never compute it.
- Corrected all code citations to verified lines (graph.ts:82-87,
  instruction-loader.ts:366/429, status.ts); noted #1277's coverage helpers are
  not in this branch's base (coordinate, don't reuse).
- Specs updated: artifact-graph Content Digest requirement; cli status digest +
  deterministic impact ordering; skill determinism + baseline-aware audit.
  tasks add digest/determinism/cross-platform tests + optional ledger section.

Still validates clean under `openspec validate add-update-workflow --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): harden add-update-workflow determinism; drop direct name refs

- Digest ledger tracks DIRECT upstream digests; document that transitive drift
  emerges hop-by-hop as downstream is reconciled (no transitive bookkeeping).
- Ground audit's no-baseline structural facts on signals available in this
  branch (missing/empty output, blocked/incomplete); capability-coverage is an
  add-on only when #1277's validateChangeCapabilityCoverage is present.
- Add the "update revises only existing downstream; defer not-yet-created ones
  to /opsx:continue" rule across proposal/design/specs/tasks; impact entries now
  carry existence/status.
- Note artifact-level (not file-level) granularity and that getDownstream
  terminates by the schema's acyclic guarantee.
- Remove direct personal references from the docs.

Validates clean under `openspec validate add-update-workflow --strict`; 10 deltas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): full issue/PR/discussion coverage + command-family design

After a comprehensive sweep of open issues, PRs, and discussions, grounded the
proposal in the complete adjacent landscape and answered the open design
questions the cluster raises:

- #783 (Cross-artifact quality review before apply) is now a primary Closes:
  it IS audit mode. Answer its open "new skill vs. extend validate" question via
  the determinism split — deterministic checks (drift/completeness/coverage) are
  CLI/validate-shaped; the semantic cross-artifact review is the skill. Added a
  skill spec scenario for the #783 patterns (scope contradiction, spec gap,
  duplication).
- Discussion #1206 ("refine proposal now?") + prior-art PR #372: official answer
  is /opsx:update.
- New design Decision 8 (command family): delineate /opsx:update from
  /opsx:clarify (#702, within-artifact), /opsx:review (#1251, plan-vs-code), and
  verify; /opsx:update consolidates update+regen+refine into one action,
  addressing skill-sprawl (#1263, #783).
- Reuse, don't reinvent: audit's empty/incomplete check reuses #1098's
  artifactOutputComplete (same outputs.ts the digest helper lives in); capability
  coverage reuses #1277's validateChangeCapabilityCoverage.
- New open questions: surface deterministic coherence in `validate` for a CI gate
  (#783-B, #829); naming reconciliation with #783's /opsx:refine.
- Confirmed add-update-command* branches are the `openspec update` tool-file
  refresh (not artifact update) — no collision.

Validates clean under --strict; 10 deltas; all relative links resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): resolve open questions to committed decisions; drift in scope

Per review steer, every open question is now a committed happy-path decision so
build-out has no dangling forks, and the deterministic drift baseline is pulled
into scope (it is what makes audit-mode drift deterministic vs. agent-guessed):

- Digest ledger IN SCOPE (design Decision 3): per-artifact DIRECT upstream
  digests in ChangeMetadataSchema, written by a deterministic `openspec status
  --record`; pre-existing changes (no baseline) degrade to drift `unknown` +
  structural checks. Generating-flow auto-recording stays optional (graceful).
- cli-artifact-workflow spec: folded drift into the digest requirement (record
  baseline / drift vs baseline / unknown-without-baseline) — stays at 10 deltas.
- opsx-update-skill spec: skill records baseline via `--record` after each
  confirmed edit, so audits clear once reconciled.
- Replaced "## Open Questions" with "## Decisions resolved": ledger in scope;
  targeted entry baseline-aware; apply stays standalone (points to update on
  drift); cross-change (#247), continue/ff de-hardcoding (#777), and validate
  CI-gate (#783-B/#829) are named follow-ups, not deferrals of the core feature;
  /opsx:update kept as the umbrella name.
- Migration Plan + Capabilities + Impact + tasks updated; status JSON gains
  `drift`, CLI gains `--record`. Re-synced with upstream main (0 behind).

Validates clean under --strict; 10 deltas; all links resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): harden add-update-workflow — close cross-OS, read-only, edge gaps

Stress-tested every claim against live source and fixed the soft spots:

- Cross-OS digest determinism (real bug): resolveArtifactOutputs (outputs.ts:34)
  sorts ABSOLUTE paths via .sort(), which differs by OS — so a multi-file glob
  artifact (specs/**/*.md) would hash differently on Windows vs POSIX. Digest now
  specified to order files by change-relative forward-slash path and hash
  relpath+content. Added spec scenarios (cross-platform glob stability; rename
  changes digest) and a cross-OS test task.
- Read-only status invariant: moved baseline recording OFF `openspec status`
  (a read command silently mutating the drift reference is a footgun) to a
  dedicated `openspec reconcile` write verb. Updated spec, skill, design, impact,
  capabilities, tasks; reconciled the "no new verb" claims.
- Edge case: missing upstream at record time is stored as an explicit `absent`
  marker so later creating it registers as drift (spec scenario added).
- Edge case: coherent change yields no edits (clean-path scenario).
- Grounding fixes: continue-change hardcoded block is duplicated (skill 103-112 +
  command 225-234) — both must be fixed in the #777 follow-up; verified no
  content-hash util exists.
- Fixed two stale claims the layered edits left: the Impact digest bullet
  (concatenation→relative-path) and the naming-boundary line.

Validates clean under --strict; 10 deltas (4+3+3), 44 scenarios; all links
resolve; re-synced with upstream main (0 behind); issue/PR/discussion sweep
re-run, no new items.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): pin data contracts + digest forward-compat; delineate #880

Grounded the surface so an implementer builds it without guessing, and added
proportionate forward-compatibility:

- New design "Data contracts" section with exact shapes: extended ArtifactStatus
  (requires/dependents/digest/drift/driftFrom — additive to the real interface at
  instruction-loader.ts:120), the --impact response, and the `.openspec.yaml`
  baselines ledger. All additive; nothing existing changes type.
- Digest scheme tag (`sha256-relpath-v1:`) + forward-compat: drift compares only
  same-scheme digests; an unrecognized/older scheme reports `unknown` rather than
  silently mis-comparing — re-reconcile restores it. Added a cli spec scenario
  and tasks for it.
- Grounded the ledger write: there is no central change-metadata writer today
  (change-metadata/index.ts only re-exports schema), so reconcile does a safe
  read-modify-write of .openspec.yaml mirroring the store's
  parse/serialize/writeStoreMetadataState pattern (foundation.ts).
- Coverage: re-swept; folded #880 (/opsx:validate code-vs-living-specs) into the
  plan-vs-code delineation alongside #1251/#1073. Main unchanged (546224e); all
  citations still valid.

Validates clean under --strict; 10 deltas; links resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): simplify add-update-workflow to a thin /opsx:update skill

Rework per @TabishB review (PR #1278): the proposal over-built. Drop the
deterministic-spine machinery and lean on the existing status command.

- Cut the reverse-dependency graph API (getDependents/getDownstream),
  SHA-256 content digests, the .openspec.yaml baseline ledger, the
  `openspec reconcile` write op, the drift report, and `status --impact`.
  Removes the artifact-graph and cli-artifact-workflow spec deltas.
- Reframe propagation as bidirectional coherence (editing design can
  require revising proposal), not downstream-only.
- Center the feature on one thin skill over the existing
  `openspec status` / `openspec list`; design now sketches the actual
  minimal skill instruction body ("written by hand").
- v1 adds no new CLI/graph/schema code: just update-change.ts + wiring.

Validates clean: `openspec validate add-update-workflow --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(update-workflow): pin the status path contract to existingOutputPaths

Address @alfred-openspec's review: the skill's write target was described
loosely as "resolved paths." Make it precise across proposal/design/spec/tasks:

- `openspec status --json` already returns everything the skill needs, in the
  top-level `artifactPaths` map — `resolvedOutputPath` and `existingOutputPaths`
  per artifact. No new CLI field is required.
- The skill edits `existingOutputPaths` (the concrete, glob-expanded files) and
  never writes to `resolvedOutputPath`, which for a glob artifact like
  `specs/**/*.md` remains the glob pattern rather than a real file.
- Add spec scenarios for editing a glob artifact's concrete files and for
  deferring a brand-new file under a glob artifact to `/opsx:continue`.
- Tighten the cross-platform scenario and add a template test (3.4) asserting
  the write target is `existingOutputPaths`, not a glob `resolvedOutputPath`.

Validates clean under `openspec validate add-update-workflow --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(update-workflow): address review — default profile, next-step guidance, change-scoped naming

- Register /opsx:update in the default core profile, not expanded-only
  (maintainer call on the PR)
- Add next-step guidance: after updating, recommend /opsx:continue,
  /opsx:apply (esp. when the change was already implemented), or
  /opsx:archive — guidance only, never acted on
- Pin naming scope: skill openspec-update-change, change proposals only;
  generalizing update to other graph types is an explicit non-goal

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(skills): implement the /opsx:update skill (openspec-update-change)

Implements the approved add-update-workflow change: one thin skill over
the existing status/list commands, in the default core profile.

- new update-change.ts template (skill + command), registered across
  init, profiles, skill-generation, tool-detection, profile-sync-drift
- update joins CORE_WORKFLOWS and ALL_WORKFLOWS
- docs: opsx.md command row + usage note, commands.md reference section,
  supported-tools.md skill list
- retire the superseded add-artifact-regeneration-support stub
- template tests pin the guardrails (schema-driven ids, planning-only,
  existingOutputPaths write contract, next-step guidance); parity hashes
  regenerated; profile/init/update/config tests cover the new core set
- tasks.md checked off; validate --strict passes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: unify requirement reader and surface #498 (#1281)

* docs(openspec): propose spec parser reading fidelity (fixes #361, #498, #312)

The requirement-parsing layer silently misreads valid Markdown:

- #361: requirement-body extraction returns only the first non-blank line,
  so a SHALL/MUST that wraps onto line 2 fails `validate --strict`.
- #498: `validate` (delta-block parser) and `archive` (full-spec parser)
  recognize requirements by different rules, so a stray `###` header passes
  validate but becomes a phantom requirement that blocks archive.
- #312 (residual): the requirement-body loop breaks on any `#` line without
  consulting the code-fence mask, truncating bodies that contain fenced
  code with `#` comments.

Proposal: one shared, multi-line, fence-aware requirement-body extractor used
by both the validator and the markdown parser; recognize only
`### Requirement:`-prefixed level-3 headers; guarantee validate/archive parity.
Adds regression + parity tests. #559 investigated and deferred (ambiguous root
cause — see design.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): bulletproof parser-fidelity proposal with empirical evidence

Hardened the proposal after reproducing every claim against main with the
bundled CLI and correcting two inaccuracies:

- #498 reframed: archive does NOT hard-fail. validate passes; archive emits
  NON-BLOCKING phantom "Proposal warnings in proposal.md" because
  validateChange/parseRequirements counts every level-3 header as a
  requirement, while the delta-block parser (validate) and specs-apply
  (rebuild) only recognize canonical `### Requirement:`. It is a consistency
  bug, not data loss. Verified the rebuilt spec is clean.
- #312 reframed: the original repro is already fixed by codeFenceLineMask
  (requirement count verified correct). The residual is a regression hazard:
  the body loop is fence-unaware, harmless only while first-line-only, so the
  multi-line fix must be fence-aware from the start.

Also: unify recognition on the canonical REQUIREMENT_HEADER_REGEX
(/^###\s*Requirement:\s*(.+)$/i, case-insensitive); surfaced a third latent
inconsistency (Zod substring includes('SHALL') vs delta word-boundary
\b(SHALL|MUST)\b) and added a single-predicate requirement; verified zero
non-Requirement level-3 headers in repo specs (CI-safe); added edge-case
scenarios (multi-line spec+delta paths, fenced scenario-looking lines,
REMOVED/RENAMED unaffected, display vs detection); replaced broken relative
links with plain paths. Proposal passes `openspec validate --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): deepen parser-fidelity proposal — add #418, upgrade #312, tier the risk

Second adversarial bulletproofing pass (reproduced everything against main):

- Add #418 (metadata-before-description): live on the spec path
  (req.text = "**ID**: ...") but ALREADY fixed on the delta path. The
  asymmetry is direct evidence for unifying the two extractors.
- Upgrade #312 from "regression hazard" to LIVE bug: a fenced code block
  before the prose line makes req.text = "```bash" on both paths today
  (distinct from the already-fixed section-count manifestation).
- Tier the fixes by risk after auditing the existing test contract
  (markdown-parser.test.ts, 15 tests green on main):
    Tier 1 (false-negative fixes #361/#418/#312): only widens what is read;
      updates one test (:331, which asserts the first-line bug). Fence tests
      (:106/:139) preserved because skip-and-join keeps SHALL-first bodies.
    Tier 2 (recognition tightening #498): canonical ### Requirement: only;
      a deliberate behavior change that updates bare-header tests (:258/:310)
      and needs a migration note. Flagged for maintainer decision, with a
      conservative opt-in-lint alternative documented.
- Surface the four-column extractor divergence table (capture / metadata /
  recognition / predicate) and an explicit "Behavior changes and test impact"
  section with exact test line refs.

Proposal passes `openspec validate --strict`. Does not claim #1156 (PR #1280).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(openspec): third pass — reject recognition tightening, add fenced-scenario bug, #498→safe INFO

Third deep pass found the prior Tier 2 (recognition tightening to
`### Requirement:`) was the WRONG fix and over-scoped:

- Bare `### <statement>` headers are a SUPPORTED, tested requirement format:
  test/core/validation.test.ts asserts a bare-header spec is valid, and bare
  headers appear across json-converter/archive/spec tests and tmp-init
  fixtures. Tightening would break a large test surface and silently drop
  requirements from real specs. REJECTED, with evidence documented.
- Replace the #498 fix with a SAFE INFO note in validate <change> that surfaces
  non-`### Requirement:` headers in delta sections. INFO never fails validation
  (strict: valid = no errors && no warnings), so nothing newly fails.
- New bug found and folded in: countScenarios is fence-unaware, so a `####
  Scenario:` inside a fenced block is counted as real — a malformed delta passes
  validate <change> while validate <spec> correctly fails. Same fence family.
- Proved the archive WRITE path is independent of the reader: specs-apply
  rebuilds from raw `### Requirement:` blocks (extractRequirementsSection +
  RequirementBlock.raw), never parseSpec/req.text → Part A cannot change
  archived content.

Net effect: recognition is unchanged, so the proposal now updates exactly ONE
existing test (:331, the first-line assertion) instead of breaking bare-header
tests. Consolidated to a single cli-validate delta (dropped cli-archive and
openspec-conventions deltas). Dropped the no-space-header hypothesis (no
divergence). Passes `openspec validate --strict`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(parser): unify the requirement reader, fence/metadata/multi-line aware (#361, #418, #312); surface #498

The requirement reader was implemented twice — MarkdownParser.parseRequirements
(validate <spec>/archive) and Validator.extractRequirementText/countScenarios
(validate <change>) — and the two had drifted. Both now delegate to one shared,
fence-/metadata-/multi-line-aware extraction in parsers/requirement-text.ts so
they cannot diverge again.

Part A — unify the reader:
- Capture the full requirement body up to the first non-fenced `#### Scenario:`,
  skipping blank, `**metadata**:`, and fenced-code lines; run SHALL/MUST
  detection over the whole body. Fixes a wrapped keyword being dropped (#361),
  metadata before the description failing validate <spec> (#418), and a fenced
  block before the prose line becoming the requirement text (#312).
- Count only non-fenced `#### ` headers, so a `#### Scenario:` inside a fenced
  example no longer counts as a real scenario in validate <change> (parity with
  validate <spec>).
- One whole-word `\b(SHALL|MUST)\b` predicate (containsShallOrMust) shared by the
  validator and base.schema, replacing the substring/word-boundary split.
- Extract buildCodeFenceMask into the shared module; MarkdownParser and
  ChangeParser import it (single fence implementation).

Part B — surface #498 safely:
- validate <change> emits an INFO note when an ADDED/MODIFIED Requirements
  section contains a non-`### Requirement:` level-3 header (one the delta reader
  silently skips). INFO never changes the valid result, including under --strict,
  so nothing newly fails. Recognition is unchanged: bare `### <statement>`
  headers remain a supported requirement format.

Write path is unaffected: specs-apply rebuilds from raw `### Requirement:`
blocks, never req.text, so archived content cannot change. Displayed text in JSON
output and delta descriptions now reflects the full body.

Tests: markdown-parser.test.ts:331 updated to expect the full body; regression
tests added for #361/#418/#312, the fenced scenario, the #498 INFO note, a
single-line guard, and CRLF. Changeset added (patch). tasks.md completed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(parser): add cross-reader predicate + metadata-only guards (design edge cases)

Exhaustive verification of the unified reader surfaced two design "edge cases
for tests" not yet covered by committed unit tests:

- Cross-reader predicate agreement: a SHALL substring inside a word ("MARSHALL")
  is rejected identically by validate <change> and validate <spec> — proving the
  one shared whole-word predicate, and guarding against a regression to the old
  substring check.
- Metadata-only body still fails validation (no requirement text) on the delta
  path.

Behavior unchanged; tests only. Full end-to-end parity across all four spec
requirements confirmed against the real Validator; no spurious INFO note fires on
any existing repo change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(parser): address review — metadata-only bodies, header-bounded extraction, reader-derived INFO

- Skip **metadata**: lines only when other body text remains; a body written
  entirely as metadata (e.g. `**Constraint**: The system MUST ...`) is kept as
  the requirement text instead of being emptied (was a regression vs main).
- Move the empty-body rule into the shared reader: both paths fall back to the
  header title, so the same block cannot pass one path and fail the other.
- End body extraction at any non-fenced markdown header, restoring old-reader
  parity: a stray `### Background` divider's notes no longer satisfy the
  SHALL/MUST check.
- Replace the standalone fence-aware INFO scanner with skipped-header
  collection inside parseDeltaSpec, so the note reflects exactly what the
  reader skipped (same section boundaries, no whole-file fence mask).
- Special-case the nameless `### Requirement:` INFO message; document that the
  any-#### scenario match is deliberate spec-path parity; un-export
  REQUIREMENT_HEADER_REGEX; move the import up top.
- Soften the changeset claim and list the known remaining divergences in
  design.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(openspec): record the no-space ###Requirement: divergence as a known leftover

Jun's edge (reproduced): the delta/write reader's REQUIREMENT_HEADER_REGEX
accepts `###Requirement:` with no space, but MarkdownParser.parseSections
requires whitespace (per GFM) — so a no-space requirement validates as a
change with zero INFO, syncs as-is, then fails validate <spec>. Pre-existing
on main and out of scope here (tightening the shared regex would change
write-path recognition); documented under known remaining divergences with
the follow-up options, folded together with the bullet from the merge
resolution. Corrects c63913b's 'no divergence' note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: TabishB <tabishbidiwale@gmail.com>

* feat(skills): auto-approve the openspec CLI in generated skills and commands (#1300)

* feat(skills): auto-approve the openspec CLI in generated skills

Emit `allowed-tools: Bash(openspec:*)` in every generated SKILL.md so
agents that honor the Agent Skills standard run `openspec` commands
without prompting on each call. Scope is limited to the CLI; per the
standard `allowed-tools` pre-approves rather than restricts, so every
other tool a skill uses stays available under the user's normal
permission settings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(commands): auto-approve the openspec CLI in Claude slash commands

Extend the allowed-tools pre-approval to the second surface: Claude Code
/opsx:* slash commands share the skill frontmatter contract, so the
Claude command adapter now emits `allowed-tools: Bash(openspec:*)` too.
The value is single-sourced in `src/core/shared/allowed-tools.ts` (a
leaf module both surfaces import). Other command adapters are unchanged
— no other tool's slash-command format defines a per-command
pre-approval field; on the skills side every tool already gets the
standard field via generateSkillContent and non-implementing tools
ignore the unknown key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix archive scenario drift for #1246 (#1252)

* fix archive scenario drift for #1246

* fix archive scenario drift for #1246

* remove local openspec change docs

* docs: clarify change name format (#1261)

* chore: remove stale npm lockfile (#1319)

* chore: remove stale npm lockfile

* ci: use package manager metadata for pnpm setup

* chore: scope npm lockfile ignore to root

* feat: add Trae command adapter (#1090)

* feat(tools): add Trae command adapter

- Added Trae command adapter for generating `.trae/commands/opsx-<id>.md` files
- Complete unit tests (9 test cases) and integration tests
- Updated documentation and .gitignore
- Fixed YAML escaping for carriage returns (\r)

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix: handle empty string in YAML escaping

- Add explicit check for empty string in escapeYamlValue
- Return quoted empty string '""' instead of unquoted empty scalar
- Update test to verify empty string is properly quoted

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix: address PR review feedback for Trae adapter

- Update docs/commands.md Trae entry to reflect generated opsx-* commands
- Export traeAdapter from adapters/index.ts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: align Trae command adapter docs

---------

Co-authored-by: jjxyxsjr <jjxyxsjr@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
Co-authored-by: TabishB <tabishbidiwale@gmail.com>

* feat: add Oh My Pi (OMP) tool support (#1276)

* feat: add Oh My Pi (OMP) tool support

Add ToolCommandAdapter for Oh My Pi terminal AI coding agent.

- New adapter: src/core/command-generation/adapters/oh-my-pi.ts
  - Commands: .omp/commands/opsx-<id>.md with description frontmatter
  - Hyphen transform: /opsx: -> /opsx- (filename = command name)
  - Argument injection: **Provided arguments**: $@ after **Input**: heading
  - escapeYamlValue applied to description field
- Register in CommandAdapterRegistry and adapters/index.ts
- Add oh-my-pi to AI_TOOLS with skillsDir: '.omp'
- Add to hyphen command transformer whitelist in init.ts and update.ts
- Full test coverage (10 cases) in adapters.test.ts
- Update docs/supported-tools.md with directory reference and tool ID

Closes #713

* fix: address CodeRabbit nitpicks

- Move ohMyPiAdapter import before opencodeAdapter (alphabetical order)
- Break long SHALL sentence and remove redundant 'follows after' in spec

* docs: polish Oh My Pi support

* docs: address Oh My Pi review nits

---------

Co-authored-by: TabishB <tabishbidiwale@gmail.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>

* chore: remove scheduled docs workflow (#1324)

* Fix Windows CI flake hardening (#1325)

* fix windows ci test flake hardening

* restore required test check status

* ci(release): add beta prerelease workflow (#1327)

* ci(release): add beta prerelease workflow

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: harden beta release workflow

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: TabishB <tabishbidiwale@gmail.com>

* fix empty store registration (#1328)

* i18n: 汉化 spec-structure 结构诊断与 SHALL/MUST 缺失提示(含测试断言)

把本次合并「先走英文」的部分块补回中文:
- spec-structure.ts:delta 标题 / 需求在 ## Requirements 之外 两条诊断
- validator:buildMissingShallOrMustMessage 提示文案
- 同步 validation.test / archive.test 断言与 shallIssues 过滤器回中文

全量 1887 测试通过;tsc / eslint 通过。archive.ts 与 v1.6.0 新功能英文待后续批次。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* i18n: 汉化 archive 与 specs-apply(命令输出 + 错误消息 + 测试断言)

- archive.ts:归档流程全部用户可见文案(校验/任务/spec 更新/确认/结果等 ~38 处)
- specs-apply.ts:delta 应用的结构化错误消息(重复/冲突/重命名/结构无效等 ~16 处)
- 同步 archive.test / store-root-selection 断言到中文
- 保留 ### Requirement: / ADDED·MODIFIED 等格式字面量与骨架 spec 英文

全量 1887 测试通过;tsc / eslint 通过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* i18n: 汉化 legacy-cleanup / update / 多选提示等提示文案(含测试断言)

- legacy-cleanup.ts:清理结果、project.md 迁移提示等
- update.ts:工具最新提示、--force 提示
- searchable-multi-select.ts:搜索/导航/无匹配 UI 文案
- 同步 legacy-cleanup.test / update.test 断言

全量 1887 通过;tsc / eslint 通过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* i18n: 汉化 view 仪表盘文案(含测试断言)

OpenSpec 仪表盘标题、草稿/活跃/已完成变更、规范、任务进度、需求计数等;同步 view.test。
全量 1887 通过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* i18n: 汉化 instructions 模板中的 agent 说明与错误提示

<project_context>/<rules>/<template> 等模板注释、tracks 文件缺失/无任务的错误提示。
全量 1887 通过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* i18n: 汉化剩余零散提示(校验/元数据/多选/标记/工具选择等)

- validator: skipped-header INFO、delta 章节未解析提示
- change-metadata schema、specs-apply ADDED 已存在、init 技能计数、
  file-system 标记状态、workset 工具选择、legacy 迁移提示补漏
- 同步各测试断言(含正则)

全量 1887 通过;tsc / eslint 通过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: 移除上游 changeset(引用 @fission-ai/openspec,不属于本 fork)

上游 v1.6.0-beta.1 带进来的 5 个 changeset 引用上游包名,本仓库不用 changeset
发布(走 tag + pnpm publish),导致 CI 的 Validate Release Tracking 失败。删除之。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(review): 双语化 spec-structure 结构诊断正则(review F1 major)

review-loop 双评审员一致发现:spec-structure.ts 只汉化了消息、未双语化检测正则,
导致 (A) `## 需求` 段内英文 `### Requirement:` 头被误报为「在 ## Requirements 之外」
并使 archive 抛错,(B) 错位的中文 `### 需求:` 需求静默丢失(#498 诊断对中文作者失效)。

- spec-structure.ts:REQUIREMENTS_SECTION_HEADER / DELTA_HEADER / REQUIREMENT_HEADER 双语化
- validator.ts:151:无名头正则 /^requirement:?$/i → /^(?:requirement|需求)[::]?$/i(F2)
- chinese-bilingual.test:把「验证 RequirementSchema 接受 必须/禁止」的空断言(base.schema
  不再强制关键字,任意非空文本都过)改为经 Validator 的真实识别测试(正+负)(F4);
  新增 findMainSpecStructureIssues 双语回归(混合段 / 错位中文 / 中文 delta 头)

已在编译产物上实证:Case A 无误报、Case B 正确标记。全量 1890 通过;tsc / eslint 通过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(review): 双语化归档场景漂移保护 + 对齐无空格头正则(review round 2)

Codex 跨模型评审发现两处双语盲区 + 一处测试加强:
- C1 (major, 数据丢失): specs-apply.ts parseScenarioBlocks 只认 ASCII `#### Scenario:`,
  中文 `#### 场景:` 场景计为 0,使 #1246「陈旧 MODIFIED 丢场景」保护对中文失效——
  MODIFIED 可静默删除已有中文场景。改为 `(?:Scenario|场景)[::]` 双语;
  新增归档回归测试(中文场景被丢弃 → 正确中止、老场景保留)。
- C2 (minor): spec-structure REQUIREMENT_HEADER `###\s+` → `###\s*`,与共享读取器
  requirement-blocks(`###\s*`,且测试固定支持无空格头)对齐。
- C3 (minor): 加强 over-match 测试为断言 issues 全空(可捕获误判为 delta-header);
  新增无名中文头(全/半角冒号)不被误标的用例。

已实证:parseScenarioBlocks 现能解析中文场景。全量 1892 通过;tsc / eslint 通过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(review): 场景漂移保护改为围栏感知 + 收紧名称捕获(review round 3)

Codex 第三轮在 parseScenarioBlocks 上又发现两处边界(均已实证):
- Codex#1 (major): 未做围栏屏蔽——需求正文中围栏代码块内的 `#### 场景:` 示例
  被计为真实场景,MODIFIED 保留真实场景时会误判「丢了示例场景」→ 误中止归档。
  改用 buildCodeFenceMask,头匹配与边界扫描都跳过围栏行;新增归档回归(围栏内示例
  不计、归档成功)。
- Codex#2 (minor): `#### 场景:<空白>` 会以空名匹配,违反无名不匹配。名称捕获
  `(.+)` → `(\S.*?)`,要求至少一个非空白字符。
- 抽出 SCENARIO_HEADER_RE 常量,消除双处重复字面量。

实证:围栏内示例不计、纯空白名不计、EN+ZH 混合仍正确。全量 1893 通过;tsc/eslint 通过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Clay Good <hi@claygood.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
Co-authored-by: TabishB <tabishbidiwale@gmail.com>
Co-authored-by: Danilo <danilopopeye@users.noreply.github.com>
Co-authored-by: zhangsan582 <1553977725@qq.com>
Co-authored-by: Ercan Erdoğan <ercanerdogan@gmail.com>
Co-authored-by: shin <112563017+jjxyxsjr@users.noreply.github.com>
Co-authored-by: jjxyxsjr <jjxyxsjr@users.noreply.github.com>
Co-authored-by: xianzheTM <ylxianzhe@outlook.com>
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.

Line breaks in headers cause validation issues

4 participants