1# Design: Semantic Spec Diff Tool23## Context45The frontend `parseSpec` in `dashboard/dist/index.js` (lines 445–495) parses spec6markdown into `{ title, purpose, requirements: [{ name, description, scenarios: [{ name, steps }] }] }`.7This logic needs to exist in Python so the backend (`plugin_api.py`) and agent tools8(`tools.py`) can use it. The diff itself is currently `difflib.unified_diff` — line-level,9not semantic.1011## Parser: `_parse_spec(md)`1213Mirror the JS `parseSpec` exactly so the dashboard and tool produce identical structure.1415State machine: `top → purpose → reqDesc → scenario`1617| Line pattern | Action |18|---|---|19| `# Title` (not `## `) | Set title, state → `top` |20| `## Purpose` | state → `purpose` |21| `## Requirements` | state → `top` (skip header) |22| `### Requirement: Name` | Push new requirement, state → `reqDesc` |23| `#### Scenario: Name` | Push new scenario to current requirement, state → `scenario` |24| (in purpose) | Append to `purpose` |25| (in reqDesc) | Append to current requirement `description` |26| (in scenario) | Parse `**GIVEN**`/`**WHEN**`/`**THEN**` bullets into `steps` |2728Return: `{ title, purpose, requirements: [{ name, description, scenarios: [{ name, steps: [{ type, text }] }] }] }`2930## Diff: `_semantic_spec_diff(before_md, after_md)`31321. Parse both specs.332. Build dicts keyed by requirement name for O(1) lookup.343. Classify each requirement:35 - In after only → `added`36 - In before only → `removed`37 - In both, same description + same scenarios → `unchanged`38 - In both, different → `modified`394. For modified requirements, diff scenarios the same way (by scenario name).405. Return:4142```python43{44 "status": "added" | "modified" | "deleted" | "unchanged",45 "requirements": {46 "added": [{ name, description, scenarios }],47 "modified": [{48 "name",49 "before": { description, scenarios },50 "after": { description, scenarios },51 "scenarios_added": [...],52 "scenarios_modified": [...],53 "scenarios_removed": [...],54 }],55 "removed": [{ name, description, scenarios }],56 "unchanged": ["req name", ...],57 },58}59```6061Overall status: `added` if before is None/empty, `deleted` if after is None/empty,62`unchanged` if all requirements unchanged, else `modified`.6364## Tool: `openspec_spec_diff`6566### Parameters6768| Param | Required | Description |69|---|---|---|70| `workdir` | yes (or `identifier`) | Project directory |71| `spec` | yes | Spec name (e.g. `agent-tools`) |72| `change` | no | Change id — when provided, diff change spec vs baseline |7374### Resolution7576- `change` provided:77 - Change spec: `openspec/changes/<change>/specs/<spec>/spec.md`78 - Baseline: `openspec/specs/<spec>/spec.md`79 - If change spec missing → `ok: false, error: "change spec not found"`80 - If baseline missing → status `added`, all requirements in `added`81- `change` omitted:82 - Worktree spec: `openspec/specs/<spec>/spec.md` (current content)83 - Baseline: `git show HEAD:openspec/specs/<spec>/spec.md`84 - If git unavailable → `ok: false, error: "git required for HEAD comparison"`8586### Output8788```json89{90 "ok": true,91 "spec": "agent-tools",92 "change": "add-idea-tools",93 "status": "modified",94 "baseline_exists": true,95 "requirements": { "added": [...], "modified": [...], "removed": [...], "unchanged": [...] },96 "line_diff": "--- before/...\n+++ after/...\n@@ ..."97}98```99100## Registration101102Filesystem-backed — always available, no `check_fn` gating. Same pattern as103`openspec_context`, `openspec_idea_create`, etc.104105## Sharing with the dashboard106107The `_parse_spec` and `_semantic_spec_diff` functions live in `tools.py`. The108dashboard backend (`plugin_api.py`) imports them the same way it imports109`registry.py` — via `importlib.util.spec_from_file_location`. This avoids110circular imports and keeps the plugin self-contained.111112Alternatively, extract into a `spec_parser.py` module that both `tools.py` and113`plugin_api.py` import directly. Preferred — cleaner separation, testable in114isolation.