1. semantic-spec-diff-tool: shared Python spec parser, semantic diff
function, and openspec_spec_diff agent tool (filesystem-backed).
Includes design doc.
2. change-specs-semantic-diff-view: replace change>specs tab
side-by-side view with semantic delta view. Backend adds
semantic_diff field; frontend renders SpecDeltaView with
requirement-level added/modified/removed sections.
3. spec-browser-structured-rendering: switch spec browser dirty/refs
modes from raw Markdown to SpecContentView; add compact semantic
summary ('+2 req, ~1, -0') to spec list items.
All three validate with openspec validate --strict.- commit
- db2ff137c2fae78c89f1b9992927caf37d31c71e
- parent
- 7dc0e8a1f3a10d7ac21ce08d5e878b7f5f936e06
- author
- dami etoile <dami.etoile@gmail.com>
@@ -0,0 +1,26 @@
1+## Why2+3+The change > Specs tab currently shows side-by-side full specs plus a unified line diff — three views of the same data with no summary of what actually changed at the requirement level. For a change that touches one scenario in a 10-requirement spec, the maintainer has to scan two full documents to find the difference. The semantic delta computed by the shared parser (from the `semantic-spec-diff-tool` change) should drive this view instead.4+5+## What Changes6+7+Replace the "Diff vs current" side-by-side view in `ChangeSpecsView` with a semantic delta view. The backend `_change_detail` function computes a `semantic_diff` field per spec. The frontend renders a summary-first view: requirement-level added/modified/removed sections with scenario-level deltas inside modified requirements, unchanged requirements collapsed by default, and the unified line diff as a collapsible raw fallback at the bottom.8+9+## Capabilities10+11+### New Capabilities12+- `change-specs-semantic-diff-view`: Frontend `SpecDeltaView` component and backend `semantic_diff` field in change detail, rendering requirement-level deltas instead of side-by-side full specs13+14+### Modified Capabilities15+- `change-board`: The "Delta spec comparison" requirement is modified to include semantic diff alongside the existing unified diff in change detail responses16+17+## Impact18+19+- `dashboard/plugin_api.py` — `_change_detail` computes `semantic_diff` per spec using the shared `_semantic_spec_diff` function20+- `dashboard/dist/index.js` — new `SpecDeltaView` component; `ChangeSpecsView` updated to use it for the diff mode21+- `dashboard/dist/style.css` — styles for delta sections, added/modified/removed badges, collapsible unchanged requirements22+- `openspec/specs/change-board/spec.md` — delta spec modifying the "Delta spec comparison" requirement23+24+### Dependencies25+26+- Requires `semantic-spec-diff-tool` — the shared `_parse_spec` and `_semantic_spec_diff` functions must exist in `tools.py` before the backend can use them@@ -0,0 +1,25 @@
1+# Change Board Delta — Semantic Spec Diff in Change Detail2+3+## MODIFIED Requirements4+5+### Requirement: Change detail API6+The dashboard API SHALL expose full detail for a selected change, including a semantic diff for each delta spec.7+8+#### Scenario: Existing change detail9+- **GIVEN** a selected source contains the requested change10+- **WHEN** the dashboard requests change detail11+- **THEN** the API returns the change name, title, status, archive flag, task stats, proposal markdown, design markdown, tasks markdown, and delta spec entries12+13+#### Scenario: Delta spec comparison14+- **GIVEN** a change contains delta spec files under its `specs/` directory15+- **WHEN** the API returns change detail16+- **THEN** each delta spec entry includes relative path, proposed content, current worktree spec content when present, status, a unified diff when content changed, and a `semantic_diff` field with the structured requirement-level delta17+18+#### Scenario: Semantic diff for new spec19+- **GIVEN** a change contains a delta spec with no baseline spec20+- **WHEN** the API returns change detail21+- **THEN** the `semantic_diff` field has status `added` with all requirements in `added`22+23+#### Scenario: Missing change detail24+- **WHEN** the dashboard requests a change that does not exist in the selected source25+- **THEN** the API returns a not-found response@@ -0,0 +1,44 @@
1+# Change Specs Semantic Diff View Delta2+3+## ADDED Requirements4+5+### Requirement: Semantic delta view component6+The dashboard UI SHALL render a `SpecDeltaView` component that displays the semantic diff as a summary-first view with requirement-level sections.7+8+#### Scenario: Delta summary9+- **WHEN** the delta view renders for a modified spec10+- **THEN** it shows a summary line indicating how many requirements were added, modified, and removed11+12+#### Scenario: Added requirements section13+- **GIVEN** the semantic diff contains requirements in `added`14+- **WHEN** the delta view renders15+- **THEN** each added requirement is shown with its description and scenarios, marked with an `added` badge16+17+#### Scenario: Modified requirements section18+- **GIVEN** the semantic diff contains requirements in `modified`19+- **WHEN** the delta view renders20+- **THEN** each modified requirement shows before/after description changes and scenario-level deltas (added/modified/removed scenarios)21+22+#### Scenario: Removed requirements section23+- **GIVEN** the semantic diff contains requirements in `removed`24+- **WHEN** the delta view renders25+- **THEN** each removed requirement is shown with its description and scenarios, marked with a `removed` badge26+27+#### Scenario: Unchanged requirements collapsed28+- **GIVEN** the semantic diff contains requirements in `unchanged`29+- **WHEN** the delta view renders30+- **THEN** unchanged requirements are collapsed or hidden by default to reduce noise31+32+### Requirement: Raw diff fallback33+The dashboard UI SHALL provide the unified line diff as a collapsible section at the bottom of the semantic delta view.34+35+#### Scenario: Collapsible raw diff36+- **WHEN** the delta view renders for a modified spec37+- **THEN** a collapsible "raw diff" section appears at the bottom containing the unified line diff, collapsed by default38+39+### Requirement: Replace side-by-side view40+The dashboard UI SHALL use the semantic delta view instead of side-by-side full specs for the change > Specs diff mode.41+42+#### Scenario: Diff mode renders delta view43+- **WHEN** the user selects "Diff vs current" in the change > Specs tab44+- **THEN** the `SpecDeltaView` component renders instead of the previous side-by-side `SpecContentView` + unified diff layout@@ -0,0 +1,9 @@
1+## 1. Tasks2+3+- [ ] 1.1 Add _semantic_spec_diff call to _change_detail in plugin_api.py — compute semantic delta per change spec and include as semantic_diff field in each spec entry4+- [ ] 1.2 Add SpecDeltaView component to index.js — renders summary ('N requirements added, M modified, K removed'), expandable sections for added/modified/removed requirements with scenario-level deltas5+- [ ] 1.3 Collapse unchanged requirements by default in SpecDeltaView (noise reduction)6+- [ ] 1.4 Keep unified line diff as collapsible 'raw diff' section at the bottom of the delta view7+- [ ] 1.5 Replace current 'Diff vs current' toggle in ChangeSpecsView to use SpecDeltaView instead of side-by-side full specs8+- [ ] 1.6 Update specs/change-board spec: modify 'Delta spec comparison' requirement to include semantic diff alongside unified diff9+- [ ] 1.7 Write tests for _change_detail semantic_diff field computation@@ -0,0 +1,114 @@
1+# Design: Semantic Spec Diff Tool2+3+## Context4+5+The frontend `parseSpec` in `dashboard/dist/index.js` (lines 445–495) parses spec6+markdown into `{ title, purpose, requirements: [{ name, description, scenarios: [{ name, steps }] }] }`.7+This 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,9+not semantic.10+11+## Parser: `_parse_spec(md)`12+13+Mirror the JS `parseSpec` exactly so the dashboard and tool produce identical structure.14+15+State machine: `top → purpose → reqDesc → scenario`16+17+| 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` |27+28+Return: `{ title, purpose, requirements: [{ name, description, scenarios: [{ name, steps: [{ type, text }] }] }] }`29+30+## Diff: `_semantic_spec_diff(before_md, after_md)`31+32+1. Parse both specs.33+2. Build dicts keyed by requirement name for O(1) lookup.34+3. 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`39+4. For modified requirements, diff scenarios the same way (by scenario name).40+5. Return:41+42+```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+```60+61+Overall status: `added` if before is None/empty, `deleted` if after is None/empty,62+`unchanged` if all requirements unchanged, else `modified`.63+64+## Tool: `openspec_spec_diff`65+66+### Parameters67+68+| 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 |73+74+### Resolution75+76+- `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"`85+86+### Output87+88+```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+```99+100+## Registration101+102+Filesystem-backed — always available, no `check_fn` gating. Same pattern as103+`openspec_context`, `openspec_idea_create`, etc.104+105+## Sharing with the dashboard106+107+The `_parse_spec` and `_semantic_spec_diff` functions live in `tools.py`. The108+dashboard backend (`plugin_api.py`) imports them the same way it imports109+`registry.py` — via `importlib.util.spec_from_file_location`. This avoids110+circular imports and keeps the plugin self-contained.111+112+Alternatively, extract into a `spec_parser.py` module that both `tools.py` and113+`plugin_api.py` import directly. Preferred — cleaner separation, testable in114+isolation.@@ -0,0 +1,23 @@
1+## Why2+3+The frontend `parseSpec` function in `dashboard/dist/index.js` parses OpenSpec spec markdown into structured data (title, purpose, requirements with scenarios). This logic exists only in JavaScript — the Python backend and agent tools can't use it. Meanwhile, the dashboard's spec diff is line-level unified diff (`difflib.unified_diff`), which shows what text changed but not what requirements or scenarios changed. Agents have no way to get a structured delta at all — they must read two full specs and mentally diff them.4+5+## What Changes6+7+Extract the spec parser into a shared Python function, build a semantic diff function on top of it, and expose it as a new `openspec_spec_diff` agent tool. The tool returns a structured JSON delta at the requirement/scenario level (added, modified, removed, unchanged) plus a unified line diff as a fallback field. Filesystem-backed — works without the OpenSpec CLI binary, same as the idea and change lifecycle tools.8+9+## Capabilities10+11+### New Capabilities12+- `semantic-spec-diff`: Python spec parser, semantic diff function, and `openspec_spec_diff` agent tool for comparing specs at the requirement/scenario level13+14+### Modified Capabilities15+- `agent-tools`: New `openspec_spec_diff` tool added to the plugin's registered tools, filesystem-backed (always available, no CLI gating)16+17+## Impact18+19+- `tools.py` — new `_parse_spec()`, `_semantic_spec_diff()`, and `openspec_spec_diff()` functions20+- `schemas.py` — new `OPENSPEC_SPEC_DIFF` schema21+- `__init__.py` — register the new tool (filesystem-backed, no `check_fn`)22+- `plugin.yaml` — add `openspec_spec_diff` to `provides_tools`23+- `tests/` — new test file for parser, diff, and tool handler@@ -0,0 +1,26 @@
1+# Agent Tools Delta — Semantic Spec Diff2+3+## MODIFIED Requirements4+5+### Requirement: Artifact listing and inspection6+The plugin SHALL provide tools that map directly to OpenSpec list/show/status/instructions workflows while smoothing known CLI rough edges that would otherwise block fresh-repository agent workflows and SHALL expose a filesystem-backed semantic spec diff tool for comparing specs at the requirement and scenario level without the CLI.7+8+#### Scenario: List changes or specs9+- **WHEN** an agent calls `openspec_list` with kind `changes` or `specs`10+- **THEN** the tool invokes the OpenSpec CLI JSON list command for that kind and respects supported sort options11+12+#### Scenario: Show a change or spec13+- **WHEN** an agent calls `openspec_show` with a target name and optional flags14+- **THEN** the tool invokes the OpenSpec CLI JSON show command with the requested type, delta, requirements, scenario, or requirement filters15+16+#### Scenario: Validate artifacts17+- **WHEN** an agent calls `openspec_validate`18+- **THEN** the tool invokes the OpenSpec CLI validation command with strict JSON non-interactive output and the requested target or scope19+20+#### Scenario: Retrieve implementation instructions21+- **WHEN** an agent calls `openspec_instructions`22+- **THEN** the tool returns OpenSpec's enriched instructions for the requested artifact, schema, and optional change23+24+#### Scenario: Semantic spec diff25+- **WHEN** an agent calls `openspec_spec_diff` with `workdir`, `spec`, and optional `change`26+- **THEN** the tool returns a structured semantic delta at the requirement and scenario level plus a unified line diff fallback, without requiring the OpenSpec CLI binary@@ -0,0 +1,84 @@
1+# Semantic Spec Diff Delta2+3+## ADDED Requirements4+5+### Requirement: Shared spec parser6+The plugin SHALL provide a `_parse_spec(md)` Python function that parses OpenSpec spec markdown into structured data: title, purpose, and a list of requirements, each with a name, description, and list of scenarios, each scenario with a name and steps.7+8+#### Scenario: Parse a well-formed spec9+- **GIVEN** a spec markdown file with `# Title`, `## Purpose`, `### Requirement:` headers, and `#### Scenario:` headers with `**GIVEN**`/`**WHEN**`/`**THEN**` steps10+- **WHEN** `_parse_spec` is called with the markdown content11+- **THEN** it returns a dict with `title`, `purpose`, and `requirements` list, where each requirement has `name`, `description`, and `scenarios`, and each scenario has `name` and `steps`12+13+#### Scenario: Parse a spec with no requirements14+- **GIVEN** a spec markdown file with only a title and purpose, no `### Requirement:` headers15+- **WHEN** `_parse_spec` is called with the markdown content16+- **THEN** it returns a dict with `title`, `purpose`, and an empty `requirements` list17+18+#### Scenario: Parse empty or None input19+- **WHEN** `_parse_spec` is called with an empty string or None20+- **THEN** it returns a dict with empty `title`, empty `purpose`, and empty `requirements` list21+22+### Requirement: Semantic spec diff function23+The plugin SHALL provide a `_semantic_spec_diff(before_md, after_md)` function that compares two parsed specs and returns a structured delta at the requirement and scenario level.24+25+#### Scenario: Added requirement26+- **GIVEN** the after spec contains a requirement that the before spec does not27+- **WHEN** `_semantic_spec_diff` is called28+- **THEN** the requirement appears in `requirements.added` with its full description and scenarios29+30+#### Scenario: Removed requirement31+- **GIVEN** the before spec contains a requirement that the after spec does not32+- **WHEN** `_semantic_spec_diff` is called33+- **THEN** the requirement appears in `requirements.removed` with its full description and scenarios34+35+#### Scenario: Modified requirement description36+- **GIVEN** a requirement exists in both specs with the same name but different descriptions37+- **WHEN** `_semantic_spec_diff` is called38+- **THEN** the requirement appears in `requirements.modified` with `before` and `after` description fields39+40+#### Scenario: Scenario-level delta in modified requirement41+- **GIVEN** a requirement exists in both specs with the same name but different scenarios42+- **WHEN** `_semantic_spec_diff` is called43+- **THEN** the modified entry includes `scenarios_added`, `scenarios_modified`, and `scenarios_removed` lists44+45+#### Scenario: Unchanged requirement46+- **GIVEN** a requirement exists in both specs with identical descriptions and scenarios47+- **WHEN** `_semantic_spec_diff` is called48+- **THEN** the requirement name appears in `requirements.unchanged`49+50+#### Scenario: No baseline exists51+- **GIVEN** the before spec is None or empty (new spec being added)52+- **WHEN** `_semantic_spec_diff` is called53+- **THEN** all requirements appear in `requirements.added` and the overall status is `added`54+55+### Requirement: openspec_spec_diff agent tool56+The plugin SHALL provide an `openspec_spec_diff` tool that compares a change's delta spec against its baseline, or a worktree spec against its HEAD version, and returns the structured semantic delta plus a unified line diff fallback.57+58+#### Scenario: Diff a change spec against baseline59+- **GIVEN** a change contains a delta spec under `openspec/changes/<change>/specs/<spec>/spec.md` and a baseline spec exists at `openspec/specs/<spec>/spec.md`60+- **WHEN** an agent calls `openspec_spec_diff` with `workdir`, `spec`, and `change`61+- **THEN** the tool returns the semantic delta between the change spec and the baseline spec, including overall status, requirements delta, and a `line_diff` unified diff field62+63+#### Scenario: Diff a worktree spec against HEAD64+- **GIVEN** a baseline spec exists at `openspec/specs/<spec>/spec.md` and has been modified in the worktree65+- **WHEN** an agent calls `openspec_spec_diff` with `workdir` and `spec` (no `change`)66+- **THEN** the tool returns the semantic delta between the HEAD version (via git) and the worktree version67+68+#### Scenario: New spec with no baseline69+- **GIVEN** a change contains a delta spec but no baseline spec exists70+- **WHEN** an agent calls `openspec_spec_diff` with `workdir`, `spec`, and `change`71+- **THEN** the tool returns status `added` with all requirements in `requirements.added` and an empty `line_diff`72+73+#### Scenario: Missing change spec74+- **WHEN** an agent calls `openspec_spec_diff` with a `change` that does not contain the requested `spec`75+- **THEN** the tool returns `ok: false` with an error explaining the change spec was not found76+77+#### Scenario: Filesystem-backed availability78+- **GIVEN** no OpenSpec CLI binary is available79+- **WHEN** Hermes registers plugin tools80+- **THEN** `openspec_spec_diff` is still available because it reads spec files directly from the filesystem81+82+#### Scenario: Git unavailable for HEAD comparison83+- **GIVEN** `openspec_spec_diff` is called without `change` (HEAD comparison mode) and git is unavailable84+- **THEN** the tool returns `ok: false` with an error explaining git is required for worktree-vs-HEAD comparison@@ -0,0 +1,9 @@
1+## 1. Tasks2+3+- [ ] 1.1 Add _parse_spec(md) Python function to tools.py mirroring the frontend parseSpec parser (title, purpose, requirements with name/description/scenarios, scenarios with name/steps)4+- [ ] 1.2 Add _semantic_spec_diff(before_md, after_md) function returning structured delta: requirements.added/modified/removed/unchanged, with scenario-level diffs inside modified requirements5+- [ ] 1.3 Add openspec_spec_diff tool handler: takes workdir, spec, optional change; diffs change spec against baseline spec when change is provided, diffs worktree spec against HEAD when change is omitted6+- [ ] 1.4 Add OPENSPEC_SPEC_DIFF schema to schemas.py7+- [ ] 1.5 Register openspec_spec_diff in __init__.py (filesystem-backed, always available — no check_fn gating)8+- [ ] 1.6 Add openspec_spec_diff to provides_tools in plugin.yaml9+- [ ] 1.7 Write tests for parser, semantic diff (added/modified/removed/unchanged requirements, scenario-level deltas), and tool handler (change vs baseline, worktree vs HEAD, missing spec handling)@@ -0,0 +1,25 @@
1+## Why2+3+The spec browser uses raw `Markdown` rendering in dirty and refs diff modes — two unstructured markdown blobs side-by-side. This is inconsistent with the change > Specs tab, which uses `SpecContentView` (structured requirement/scenario rendering). The spec list in dirty/refs mode also shows only a `changed` badge with no indication of what changed at the requirement level, so maintainers must click into each spec to see the delta.4+5+## What Changes6+7+Switch the spec browser's dirty and refs modes from raw `Markdown` to `SpecContentView` for before/after rendering. Add a compact semantic summary to spec list items in dirty/refs mode — requirement-level added/modified/removed counts computed from before/after content — so maintainers see "+2 req, ~1, -0" at a glance before clicking in.8+9+## Capabilities10+11+### New Capabilities12+- `spec-browser-structured-rendering`: Structured rendering in spec browser diff modes and compact semantic summary in the spec list13+14+### Modified Capabilities15+- `spec-browser`: Requirements modified to use structured rendering in diff modes and include semantic summary in list entries16+17+## Impact18+19+- `dashboard/dist/index.js` — `SpecsView` uses `SpecContentView` instead of `Markdown` in dirty/refs modes; spec list items show semantic summary20+- `dashboard/plugin_api.py` — `_spec_browser` computes `semantic_summary` (added/modified/removed counts) per spec in dirty and refs modes21+- `openspec/specs/spec-browser/spec.md` — delta spec adding structured rendering and semantic summary requirements22+23+### Dependencies24+25+- Requires `semantic-spec-diff-tool` — the shared `_parse_spec` and `_semantic_spec_diff` functions for computing summaries@@ -0,0 +1,20 @@
1+# Spec Browser Structured Rendering Delta2+3+## ADDED Requirements4+5+### Requirement: Semantic summary in spec list6+The dashboard API SHALL include a `semantic_summary` field with requirement-level added/modified/removed counts for each spec in dirty and refs modes, and the UI SHALL display it compactly in the spec list.7+8+#### Scenario: Backend computes semantic summary9+- **GIVEN** a spec has been modified between the before and after refs10+- **WHEN** the spec browser API returns entries in dirty or refs mode11+- **THEN** each changed entry includes a `semantic_summary` field with `added`, `modified`, and `removed` requirement counts12+13+#### Scenario: Spec list displays summary14+- **GIVEN** a spec list entry has a `semantic_summary` with non-zero counts15+- **WHEN** the spec list renders in dirty or refs mode16+- **THEN** the entry displays a compact summary (e.g. "+2 req, ~1, -0") alongside the existing status badge17+18+#### Scenario: No summary in current mode19+- **WHEN** the spec browser is in current mode (no diff)20+- **THEN** spec list entries do not display a semantic summary because there is no comparison@@ -0,0 +1,40 @@
1+# Spec Browser Delta — Structured Rendering2+3+## MODIFIED Requirements4+5+### Requirement: Dirty worktree diff mode6+The dashboard API SHALL compare a git ref to current worktree specs when dirty mode is requested, and the UI SHALL render diffs using structured spec content views.7+8+#### Scenario: Dirty diff defaults9+- **WHEN** dirty mode is requested without an explicit before ref10+- **THEN** the API compares `HEAD` to the working tree11+12+#### Scenario: Dirty diff filters unchanged specs13+- **GIVEN** some specs match the selected before ref exactly14+- **WHEN** dirty mode is requested15+- **THEN** unchanged specs are omitted from the returned files list16+17+#### Scenario: Dirty diff entry18+- **GIVEN** a spec was added, modified, or deleted relative to the before ref19+- **WHEN** dirty mode is requested20+- **THEN** the API returns before content, after content, status, changed flag, unified diff, and available timestamps for that spec21+22+#### Scenario: Dirty mode structured rendering23+- **WHEN** the spec browser is in dirty mode and a changed spec is selected24+- **THEN** the before and after panels render using `SpecContentView` with structured requirement and scenario sections instead of raw markdown25+26+### Requirement: Ref-to-ref diff mode27+The dashboard API SHALL compare specs between two explicit git refs when both before and after refs are supplied, and the UI SHALL render diffs using structured spec content views.28+29+#### Scenario: Valid ref comparison30+- **GIVEN** both before and after refs are supplied31+- **WHEN** the dashboard requests the spec browser32+- **THEN** the API returns mode `refs`, both labels, all spec paths present in either ref, per-spec status, before content, after content, changed flag, and unified diff when changed33+34+#### Scenario: Incomplete ref comparison35+- **WHEN** only one of before or after is supplied outside dirty mode36+- **THEN** the API rejects the request with a 400-level error explaining that both refs are required37+38+#### Scenario: Refs mode structured rendering39+- **WHEN** the spec browser is in refs mode and a changed spec is selected40+- **THEN** the before and after panels render using `SpecContentView` with structured requirement and scenario sections instead of raw markdown@@ -0,0 +1,6 @@
1+## 1. Tasks2+3+- [ ] 1.1 Replace Markdown with SpecContentView for before/after rendering in SpecsView dirty and refs modes (index.js)4+- [ ] 1.2 Add compact semantic summary to spec list items in dirty/refs mode — compute requirement-level added/modified/removed counts from before/after content and display as a badge or inline text5+- [ ] 1.3 Backend: add a semantic_summary field (added/modified/removed counts) to spec-browser entries in dirty and refs modes (plugin_api.py _spec_browser)6+- [ ] 1.4 Update specs/spec-browser spec: add requirement for structured rendering in diff modes and semantic summary in spec list