Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .windsurf/rules/specify-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Auto-generated from all feature plans. Last updated: 2026-02-07

## Active Technologies

- TypeScript 5, React 19 + `diff` npm package (diffChars, diffWords, diffLines), React hooks (003-diff-line-numbers)
- N/A (client-side only) (003-diff-line-numbers)

- TypeScript 5 (strict mode) + React 19, `diff` npm package (already installed — exports `diffChars`, `diffWords`, `diffLines`) (002-toggle-diff-options)
- localStorage (browser-native, no new dependencies) (002-toggle-diff-options)

Expand All @@ -29,9 +32,9 @@ TypeScript 5.9.3 (strict mode): Follow standard conventions

## Recent Changes

- 002-toggle-diff-options: Added TypeScript 5 (strict mode) + React 19, `diff` npm package (already installed — exports `diffChars`, `diffWords`, `diffLines`)
- 003-diff-line-numbers: Added TypeScript 5, React 19 + `diff` npm package (diffChars, diffWords, diffLines), React hooks

- 001-text-diff: Added TypeScript 5.9.3 (strict mode) + React 19.2.4, `diff` (npm — to be added as runtime dependency)
- 002-toggle-diff-options: Added TypeScript 5 (strict mode) + React 19, `diff` npm package (already installed — exports `diffChars`, `diffWords`, `diffLines`)

- 001-text-diff: Added TypeScript 5.9.3 (strict mode) + React 19.2.4, `diff` (npm — to be added as runtime dependency)

Expand Down
34 changes: 34 additions & 0 deletions specs/003-diff-line-numbers/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Specification Quality Checklist: Diff Line Numbers

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-02-08
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Notes

- All clarifications resolved. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
100 changes: 100 additions & 0 deletions specs/003-diff-line-numbers/data-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Data Model: Diff Line Numbers

**Feature**: 003-diff-line-numbers
**Date**: 2026-02-08

## New Types

### DiffLine

A single line of diff output with line number metadata.

```typescript
/** A single line in the diff output with line number metadata */
export interface DiffLine {
/** The text content of this line (without trailing newline) */
text: string;
/** The diff classification: added, removed, or unchanged */
type: DiffType;
/** Line number in the original text, undefined for added lines */
originalLineNumber: number | undefined;
/** Line number in the modified text, undefined for removed lines */
modifiedLineNumber: number | undefined;
}
```

### DiffLineResult

Extended diff result that includes line-based output alongside the existing segment-based output.

```typescript
/** Extended diff result with line-based output for rendering with line numbers */
export interface DiffLineResult extends DiffResult {
/** Line-based representation of the diff, derived from segments */
lines: DiffLine[];
}
```

## Modified Types

### DiffResult (unchanged)

The existing `DiffResult` interface is not modified. `DiffLineResult` extends it to maintain backward compatibility.

### DiffViewerProps (modified)

```typescript
export interface DiffViewerProps {
/** The computed diff result with line data, null when output should be hidden */
result: DiffLineResult | null;
/** The effective display mode (forced 'unified' on mobile) */
viewMode: ViewMode;
}
```

## Utility Functions

### segmentsToLines

Pure transformation function: `DiffSegment[] → DiffLine[]`

**Input**: `DiffSegment[]` — flat array of diff segments from `useDiff`
**Output**: `DiffLine[]` — one entry per output line with line numbers

**Algorithm**:

1. Track two counters: `originalLine` (starts at 1), `modifiedLine` (starts at 1)
2. For each segment, split `value` by `\n`
3. For each resulting sub-line (skip trailing empty from split):
- Create `DiffLine` with appropriate line numbers based on type
- Increment counters: `removed` → originalLine++, `added` → modifiedLine++, `unchanged` → both++
4. Return accumulated `DiffLine[]`

**Line number assignment rules**:

| Segment Type | originalLineNumber | modifiedLineNumber |
| ------------ | ------------------------ | ------------------------ |
| `unchanged` | current original counter | current modified counter |
| `removed` | current original counter | `undefined` |
| `added` | `undefined` | current modified counter |

## Side-by-Side Pairing

For side-by-side rendering, `DiffLine[]` is mapped into paired rows:

```typescript
interface DiffRowPair {
original: DiffLine | null;
modified: DiffLine | null;
}
```

**Pairing rules**:

- `unchanged` line → appears in both `original` and `modified`
- `removed` line → `original` gets the line, `modified` gets `null` (placeholder)
- `added` line → `original` gets `null` (placeholder), `modified` gets the line

## localStorage Schema

No changes — this feature does not add any persisted state.
73 changes: 73 additions & 0 deletions specs/003-diff-line-numbers/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Implementation Plan: Diff Line Numbers

**Branch**: `003-diff-line-numbers` | **Date**: 2026-02-08 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/003-diff-line-numbers/spec.md`

## Summary

Add line number gutters to the diff output in both unified and side-by-side views. A new transformation layer converts flat `DiffSegment[]` into line-based `DiffLine[]` with original/modified line numbers. The `DiffViewer` component is restructured from inline spans to a row-based table layout with a two-column gutter (original | modified) in unified view and a single-column gutter per side in side-by-side view. Line numbers appear for all diff methods by splitting non-line segments at newline boundaries. The gutter reuses the existing `TextInput` gutter style for visual consistency.

## Technical Context

**Language/Version**: TypeScript 5, React 19
**Primary Dependencies**: `diff` npm package (diffChars, diffWords, diffLines), React hooks
**Storage**: N/A (client-side only)
**Testing**: Vitest 4 with @testing-library/react and @testing-library/user-event
**Target Platform**: Browser SPA, static hosting
**Project Type**: Single-page React app
**Performance Goals**: Instant recomputation on input change; line splitting adds negligible overhead
**Constraints**: No new runtime dependencies, client-side only, 100% test coverage
**Scale/Scope**: Typical diff inputs (1–10,000 lines)

## Constitution Check

_GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._

| Principle | Status | Notes |
| ----------------------------- | ------- | --------------------------------------------------------------------------------------- |
| I. Client-Side Only | ✅ PASS | All logic runs in browser, no server calls |
| II. Full Test Coverage | ✅ PASS | 100% coverage required for all new/modified code |
| III. Accessibility First | ✅ PASS | Gutter is `aria-hidden`, diff content uses semantic markup, color is not sole indicator |
| IV. Type Safety | ✅ PASS | New `DiffLine` interface with explicit types, strict mode |
| V. Simplicity and Performance | ✅ PASS | No new dependencies, pure transformation function, reuses existing patterns |

## Project Structure

### Documentation (this feature)

```text
specs/003-diff-line-numbers/
├── plan.md # This file
├── research.md # Phase 0: line-splitting algorithm research
├── data-model.md # Phase 1: DiffLine type, segmentsToLines transform
├── quickstart.md # Phase 1: setup and development guide
└── tasks.md # Phase 2: task breakdown (/speckit.tasks)
```

### Source Code (repository root)

```text
src/
├── types/
│ └── diff.ts # MODIFY: add DiffLine interface
├── hooks/
│ ├── useDiff.ts # MODIFY: add line-based output
│ └── useDiff.test.ts # MODIFY: add line number tests
├── utils/
│ ├── segmentsToLines.ts # NEW: transform DiffSegment[] → DiffLine[]
│ └── segmentsToLines.test.ts # NEW: unit tests for transformation
├── components/
│ ├── DiffViewer/
│ │ ├── DiffViewer.tsx # MODIFY: row-based rendering with gutters
│ │ ├── DiffViewer.types.ts # MODIFY: accept DiffLine[] in props
│ │ └── DiffViewer.test.tsx # MODIFY: add line number tests
│ └── App/
│ ├── App.tsx # MODIFY: pass lines to DiffViewer
│ └── App.test.tsx # MODIFY: integration tests for line numbers
```

**Structure Decision**: Single React SPA. New utility function in `src/utils/` for the pure transformation logic. No new component directories — line numbers are rendered within the existing `DiffViewer` component.

## Complexity Tracking

No constitution violations. No complexity justifications needed.
72 changes: 72 additions & 0 deletions specs/003-diff-line-numbers/quickstart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Quickstart: Diff Line Numbers

**Feature**: 003-diff-line-numbers
**Date**: 2026-02-08

## Setup

```bash
git checkout 003-diff-line-numbers
npm install
```

## Development Commands

| Command | Purpose |
| -------------------------- | ----------------------------------- |
| `npm start` | Dev server at http://localhost:5173 |
| `npm run lint` | Run ESLint |
| `npm run lint:fix` | Auto-fix ESLint errors |
| `npm run lint:tsc` | TypeScript type checking |
| `npm test -- path/to/test` | Run a single test file |
| `npm run test:ci` | Run all tests with 100% coverage |
| `npm run build` | Production build |

## Quality Gates (must all pass)

```bash
npm run lint
npm run lint:tsc
npm run test:ci
npm run build
```

## Key Files

### New Files

| File | Purpose |
| ----------------------------------- | ----------------------------------------- |
| `src/utils/segmentsToLines.ts` | Pure function: DiffSegment[] → DiffLine[] |
| `src/utils/segmentsToLines.test.ts` | Unit tests for segmentsToLines |

### Modified Files

| File | Change |
| ----------------------------------------------- | ------------------------------------------------ |
| `src/types/diff.ts` | Add `DiffLine` and `DiffLineResult` interfaces |
| `src/hooks/useDiff.ts` | Return `DiffLineResult` (includes `lines` array) |
| `src/hooks/useDiff.test.ts` | Add tests for line number output |
| `src/components/DiffViewer/DiffViewer.tsx` | Row-based rendering with line number gutters |
| `src/components/DiffViewer/DiffViewer.types.ts` | Update props to accept `DiffLineResult` |
| `src/components/DiffViewer/DiffViewer.test.tsx` | Add line number rendering tests |
| `src/components/App/App.tsx` | Pass updated result to DiffViewer |
| `src/components/App/App.test.tsx` | Integration tests for line numbers |

## Implementation Order

1. **Add types** — `DiffLine` and `DiffLineResult` in `src/types/diff.ts`
2. **Implement `segmentsToLines`** — pure utility + tests in `src/utils/`
3. **Update `useDiff`** — return `DiffLineResult` with `lines` field + tests
4. **Update `DiffViewer`** — row-based rendering with gutters + tests
5. **Update `App`** — wire updated result type + integration tests
6. **Run all quality gates**

## Independent Test

1. Paste two multi-line texts with known differences
2. View unified diff — verify two-column gutter (original | modified line numbers)
3. Switch to side-by-side view — verify each column has its own gutter
4. Toggle between Characters/Words/Lines diff methods — verify line numbers remain correct
5. Try single-line input — verify gutter shows line 1
6. Try asymmetric inputs (one much longer) — verify counters increment independently
Loading