revspec 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,331 @@
1
+ # Revspec — Design
2
+
3
+ A review tool for AI-generated spec documents. Unlike traditional spec review (human reviews, human author edits), the author here is an AI — it reads structured feedback and acts on every comment instantly. This enables multiple review rounds in a single sitting, with the human's only job being to give feedback.
4
+
5
+ ## Problem
6
+
7
+ In AI-assisted development (e.g., Claude Code's superpowers workflow), the AI generates spec documents that need human review before implementation. The current review step breaks the agentic loop — the user has to open the markdown file separately, read it, then type unstructured feedback in the terminal. There's no way to attach comments inline, highlight regions, or give feedback the AI can precisely act on.
8
+
9
+ Traditional review tools (Google Docs, PR reviews) are designed for human-to-human workflows. They optimize for discussion, consensus, and collaborative editing. But when the spec author is an AI, the workflow is fundamentally different: the human only comments, the AI always edits, and turnaround is instant.
10
+
11
+ ## Solution
12
+
13
+ A CLI tool (`revspec`) that opens a spec file in a reviewable UI, lets the user add line-anchored comments, and outputs structured JSON on close. The AI reads the JSON, addresses each comment, updates the spec, and re-invokes the review tool — completing the human-AI feedback loop in seconds rather than days.
14
+
15
+ ## Architecture
16
+
17
+ ```
18
+ ┌──────────────────────────────────────────────────────────┐
19
+ │ Protocol: JSON schema (language-agnostic) │
20
+ ├──────────────────────────────────────────────────────────┤
21
+ │ CLI: revspec <file> [--tui|--nvim|--web] │
22
+ ├──────────┬──────────────┬────────────────────────────────┤
23
+ │ Built-in │ Neovim Plugin│ Web UI │
24
+ │ TUI │ (Lua) │ (React/Express) │
25
+ │ v1 │ v2 │ v2 │
26
+ └──────────┴──────────────┴────────────────────────────────┘
27
+ ```
28
+
29
+ Three layers:
30
+
31
+ - **Protocol** — A JSON schema defining review threads. Language-agnostic. Any UI that outputs this format works with Claude Code.
32
+ - **CLI** — Node-based entry point. Manages file lifecycle (draft/final/resume/cleanup), renders the TUI, prints the output path.
33
+ - **UI Adapters** — Built-in TUI pager (v1), neovim plugin (v2), web UI (v2), future touch/iPad client. Each reads the spec + existing review threads, lets the user add messages, writes to the draft JSON.
34
+
35
+ ## Protocol
36
+
37
+ ### Review File Format (JSON)
38
+
39
+ The review file is a JSON document with threads as first-class objects. The AI reads and rewrites this file (to update anchors, add responses, set statuses). The human UI writes to a separate draft file that the CLI merges in.
40
+
41
+ ```json
42
+ {
43
+ "file": "docs/specs/2026-03-14-feature-design.md",
44
+ "threads": [
45
+ {
46
+ "id": "1",
47
+ "line": 12,
48
+ "status": "resolved",
49
+ "messages": [
50
+ { "author": "human", "text": "why webhook not polling?" },
51
+ { "author": "ai", "text": "Changed to polling. Good call." }
52
+ ]
53
+ },
54
+ {
55
+ "id": "2",
56
+ "line": 52,
57
+ "status": "pending",
58
+ "messages": [
59
+ { "author": "human", "text": "this term is ambiguous" },
60
+ { "author": "ai", "text": "I think it's clear because X" }
61
+ ]
62
+ },
63
+ {
64
+ "id": "3",
65
+ "line": 20,
66
+ "status": "open",
67
+ "messages": [
68
+ { "author": "human", "text": "this whole section needs rethinking" }
69
+ ]
70
+ }
71
+ ]
72
+ }
73
+ ```
74
+
75
+ **Top-level fields:** `file` is the spec path. `threads` contains the review threads.
76
+
77
+ **Thread model:** Each thread is an object with a line anchor, a status, and an ordered array of messages. The AI can update anchors directly (e.g., after editing the spec shifts line numbers) without appending workaround messages.
78
+
79
+ ### Thread Statuses
80
+
81
+ | Status | Meaning | Who sets it |
82
+ |--------|---------|-------------|
83
+ | `open` | Needs AI attention | Human (default on creation, or human replies) |
84
+ | `pending` | AI responded, needs human attention | AI |
85
+ | `resolved` | Done — human accepts | Human |
86
+ | `outdated` | Anchor target was removed from spec | AI |
87
+
88
+ The status tracks whose turn it is: `open` = AI's turn, `pending` = human's turn. The review is complete when every thread is either `resolved` or `outdated`. Approve blocks on `open` and `pending` threads.
89
+
90
+ ### Anchor Reanchoring
91
+
92
+ When the AI edits the spec, it updates thread anchors in the review file:
93
+
94
+ - **Line shifted** — AI updates the `line` field to the new position
95
+ - **Line modified** — AI keeps the anchor (thread is still relevant there)
96
+ - **Line removed** — AI sets status to `outdated`
97
+
98
+ ### Anchor Types
99
+
100
+ v1 supports line anchors only. The protocol reserves fields for future anchor types (span, region, quoted) but the TUI does not implement them.
101
+
102
+ | Type | Fields | Version | Use Case |
103
+ |------|--------|---------|----------|
104
+ | Line | `line` | v1 | Comment on a whole line |
105
+ | Span | `line`, `startChar`, `endChar` | v2+ | Highlight characters within a line |
106
+ | Region | `startLine`, `startChar`, `endLine`, `endChar` | v2+ | Multi-line selection |
107
+ | Quoted | `quotedText` | v3+ | Text-anchored comment (Google Docs) |
108
+
109
+ ### Thread Field Reference
110
+
111
+ | Field | Type | Required | Description |
112
+ |-------|------|----------|-------------|
113
+ | `id` | string | yes | Thread identifier |
114
+ | `status` | string | yes | `open`, `pending`, `resolved`, `outdated` |
115
+ | `messages` | array | yes | Ordered list of `{ author, text }` objects |
116
+ | `line` | integer (1-indexed) | yes (v1) | Line number anchor |
117
+
118
+ Fields reserved for v2+: `startChar`, `endChar`, `startLine`, `endLine`, `quotedText`.
119
+
120
+ ### File Naming
121
+
122
+ Review file lives next to the spec:
123
+
124
+ ```
125
+ docs/specs/2026-03-14-feature-design.md
126
+ docs/specs/2026-03-14-feature-design.review.json # the review
127
+ docs/specs/2026-03-14-feature-design.review.draft.json # in-progress (human hasn't submitted yet)
128
+ ```
129
+
130
+ No round numbers — the file is a living conversation. Git tracks history.
131
+
132
+ The human UI writes only to the draft file. The CLI merges the draft into the review file. The AI reads and rewrites the review file directly (to update anchors, add responses, set statuses).
133
+
134
+ ### Thread ID Generation
135
+
136
+ Simple incrementing strings: `"1"`, `"2"`, `"3"`, etc. The UI reads the highest existing `id` in the review + draft files and increments. The AI does the same when creating new threads. No collision risk — only one writer at a time (see Concurrency below).
137
+
138
+ ### Concurrency
139
+
140
+ Single-writer contract. The human UI and the AI never write concurrently — the workflow alternates: human reviews (CLI blocks) → CLI merges draft → AI reads and rewrites review file → AI invokes CLI again → human reviews. If this assumption is violated, the file may be corrupted.
141
+
142
+ ## CLI
143
+
144
+ ### Usage
145
+
146
+ ```bash
147
+ revspec <file.md> # opens with default UI (TUI)
148
+ revspec <file.md> --tui # explicit TUI (default)
149
+ revspec <file.md> --nvim # neovim plugin (v2)
150
+ revspec <file.md> --web # web UI (v2)
151
+ ```
152
+
153
+ ### Responsibilities
154
+
155
+ 1. Validate the spec file exists and is readable; exit 1 with error message if not
156
+ 2. Check for `.review.draft.json` — if exists, validate it's parseable JSON. If corrupted, warn the user ("Draft file corrupted, starting fresh") and delete it. If valid, pass to UI for resume
157
+ 3. Open the TUI (or chosen UI adapter), passing: spec file path, review file path, draft file path
158
+ 4. Block until the UI exits
159
+ 5. Read the draft file. If it contains `"approved": true`, this is an approval (see Approve Mechanism below). Otherwise, merge threads into `.review.json` (add new threads, append new messages to existing threads). Delete the draft.
160
+ 6. Output to stdout and exit:
161
+ - **Approved:** print `APPROVED: <review-file-path>` and exit 0
162
+ - **Has review file:** print `<review-file-path>` and exit 0
163
+ - **No review file:** print nothing and exit 0 (first round, human closed without commenting)
164
+
165
+ ### CLI→UI Interface
166
+
167
+ The CLI communicates with UI adapters via environment variables:
168
+
169
+ - `REVSPEC_FILE` — absolute path to the spec markdown file
170
+ - `REVSPEC_REVIEW` — absolute path to the review JSON file (read existing threads)
171
+ - `REVSPEC_DRAFT` — absolute path to the draft JSON file (write new threads/messages here)
172
+
173
+ For the built-in TUI (v1), these are used internally. For external adapters (neovim plugin, v2), they are set as env vars before spawning the process.
174
+
175
+ ### Tech
176
+
177
+ - Node.js (TypeScript)
178
+ - Renders the built-in TUI (v1), spawns neovim (v2), or starts Express server (v2)
179
+ - Single entry point, swappable UI backend
180
+
181
+ ## Built-in TUI (v1)
182
+
183
+ ### UX
184
+
185
+ A terminal pager that renders the spec with line numbers, status indicators, and inline comment hints. No external dependencies — ships with the CLI.
186
+
187
+ ```
188
+ ┌──────────────────────────────────────────────────────────────┐
189
+ │ docs/specs/2026-03-14-feature-design.md [Review] │
190
+ │ Threads: 1 open, 1 pending, 1 resolved │
191
+ ├──────────────────────────────────────────────────────────────┤
192
+ │ 12 The system uses polling... ✔ resolved │
193
+ │ 13 to notify downstream │
194
+ │ 14 │
195
+ │ 45 Events are sent via webhook 🔵 pending (AI replied) │
196
+ │ 46 │
197
+ │ 20 The retry logic should 💬 open │
198
+ │ 21 handle exponential backoff │
199
+ ├──────────────────────────────────────────────────────────────┤
200
+ │ j/k scroll /search c comment e expand r resolve │
201
+ │ R resolve-all a approve :w save :q submit :q! quit │
202
+ │ n/N next/prev thread │
203
+ └──────────────────────────────────────────────────────────────┘
204
+
205
+ Status indicators:
206
+ 💬 open — needs AI attention
207
+ 🔵 pending — AI replied, needs your attention
208
+ ✔ resolved — done
209
+ ⚠ outdated — anchor removed
210
+ ```
211
+
212
+ Pressing `e` on a line with a thread expands it inline:
213
+
214
+ ```
215
+ ┌─ Thread #2 (pending) ───────────────────────────┐
216
+ │ 👤 this term is ambiguous │
217
+ │ │
218
+ │ 🤖 I think it's clear because X. The term │
219
+ │ refers to the webhook delivery mechanism │
220
+ │ defined in section 3. │
221
+ │ │
222
+ │ [r]esolve [c]ontinue [q]uit │
223
+ └──────────────────────────────────────────────────┘
224
+ ```
225
+
226
+ ### Keybindings
227
+
228
+ | Key | Action |
229
+ |-----|--------|
230
+ | `j` / `k` / `↑` / `↓` | Scroll line by line |
231
+ | `Space` / `b` | Page down / up |
232
+ | `/` | Search text in spec |
233
+ | `c` | Add comment on current line / reply to existing thread |
234
+ | `e` | Expand thread on current line |
235
+ | `r` | Resolve thread on current line |
236
+ | `R` | Resolve all `pending` threads (batch resolve) |
237
+ | `a` | Approve spec — all threads must be resolved/outdated first |
238
+ | `d` | Delete most recent draft message by human on current line's thread |
239
+ | `l` | List all open/pending threads (jump to selected) |
240
+ | `n` / `N` | Jump to next / previous open/pending thread |
241
+ | `:w` | Save draft (without submitting) |
242
+ | `:q` | Submit review and quit (confirmation: "Submit N comments? [y/n]") |
243
+ | `:q!` | Quit without submitting (discard draft) |
244
+
245
+ ### Approve Mechanism
246
+
247
+ When the human presses `a`:
248
+
249
+ 1. TUI checks all threads are `resolved` or `outdated`. If not, shows an error ("N threads still open/pending").
250
+ 2. TUI writes `{ "approved": true }` to `$REVSPEC_DRAFT` and exits.
251
+ 3. CLI reads the draft, detects the `approved` flag, prints `APPROVED: <review-file-path>` to stdout.
252
+
253
+ ### Implementation
254
+
255
+ - Built into the CLI (Node.js/TypeScript)
256
+ - Uses a terminal UI library (e.g., `ink`, `blessed`, or raw ANSI escape codes)
257
+ - Reads `$REVSPEC_REVIEW` if it exists (missing file = first review, start fresh), loads existing threads
258
+ - Checks `$REVSPEC_DRAFT` for in-progress comments (resume)
259
+ - On `:q` — writes draft JSON to `$REVSPEC_DRAFT` (CLI handles merge to review file)
260
+ - On `:w` — writes draft JSON for manual save / crash recovery
261
+
262
+ ### Comment Input
263
+
264
+ When the user presses `c`:
265
+
266
+ - **On a line with no thread:** opens an inline text input below the current line. A new thread `id` is assigned.
267
+ - **On a line with an existing thread:** expands the thread first, opens a reply input at the bottom. Reply flips status back to `open`.
268
+
269
+ `Ctrl+Enter` submits the comment. `Enter` adds a newline. `Escape` cancels. The TUI shows a hint: `[Ctrl+Enter] submit [Enter] newline [Esc] cancel`.
270
+
271
+ ## Review Lifecycle
272
+
273
+ ```
274
+ 1. Claude Code generates spec → spec.md, commits it
275
+ 2. Claude Code runs: revspec spec.md
276
+ 3. CLI checks for draft → resumes or starts fresh
277
+ 4. TUI opens, human reads spec
278
+ 5. Human adds comments (c), searches (/), navigates threads (n/N)
279
+ 6. :w to save draft, :q to submit (with confirmation), :q! to discard
280
+ 7. CLI merges draft into .review.json, exits
281
+ 8. Claude Code reads .review.json, processes open threads:
282
+ - Updates spec or responds with explanation
283
+ - Sets threads to pending
284
+ - Rewrites .review.json with updated anchors/statuses/responses
285
+ - Commits spec changes
286
+ 9. Claude Code runs revspec again → human sees AI responses on pending threads
287
+ 10. Human resolves threads (r), replies to reopen (c), or batch resolves (R)
288
+ 11. When all threads are resolved/outdated → human presses a (approve)
289
+ 12. CLI exits with "APPROVED: path/to/review.json" → Claude Code proceeds to implementation plan
290
+ ```
291
+
292
+ ### Crash Recovery
293
+
294
+ If the TUI crashes or is killed, the draft file persists. Next invocation of `revspec` detects the draft and resumes with all previous comments loaded.
295
+
296
+ ## Claude Code Integration
297
+
298
+ A `/review-revspec` skill wraps the `revspec` CLI. The skill:
299
+
300
+ 1. Runs `revspec <spec-file>` (blocks while human reviews)
301
+ 2. Reads stdout — if `APPROVED:`, proceeds to implementation plan
302
+ 3. If a review file path is returned, reads the JSON, processes each thread:
303
+ - `open` threads: update spec or respond with explanation, set to `pending`
304
+ - `pending` threads with new human replies (flipped back to `open`): re-evaluate
305
+ 4. Commits spec changes, rewrites `.review.json` with updated anchors/statuses/responses
306
+ 5. Loops back to step 1 (runs `revspec` again)
307
+ 6. On `APPROVED:`, invokes the writing-plans skill
308
+
309
+ ## V2 Scope (not built in v1)
310
+
311
+ - **Neovim plugin** — Lua plugin using extmarks for comment overlays, floating windows for thread expansion, built-in diff mode for split-view review. Power-user adapter for those already in neovim. Inspired by [reviewthem.nvim](https://github.com/KEY60228/reviewthem.nvim).
312
+ - **Web UI** — Express server + React frontend, difit-inspired. Markdown rendered with line numbers, click/drag to comment, submit button finalizes.
313
+ - **Touch/region gestures** — circle-to-select on iPad, translated to region anchors by the UI.
314
+ - **Diff highlighting** — `specRevision` field in review JSON stores git commit hash. TUI/UI highlights lines changed since last review via `git diff <specRevision> -- <spec-file>`.
315
+ - **Span/region/quoted anchors** — character-level and multi-line selection in web/neovim UIs.
316
+ - **History navigation** — view previous review states via git history for context.
317
+
318
+ ## V3 Scope (not built in v1 or v2)
319
+
320
+ - **Google Docs integration** — Use Google Docs as the review UI. Upload the spec as a Google Doc (markdown import), human reviews using native Google Docs commenting, pull comments back via API and map to our JSON protocol.
321
+ - **[gws CLI](https://github.com/googleworkspace/cli)** — Rust CLI for Google Workspace APIs with structured JSON output, MCP server mode, and full Docs API access. Handles doc creation, comment retrieval, and markdown import/export. Designed for AI agent workflows.
322
+ - **Flow:** `revspec spec.md --gdocs` → upload via `gws` → open doc URL → human adds native Google Docs comments → signal done → pull comments via `gws` → map to JSON protocol → merge into `.review.json`
323
+
324
+ ## Future Considerations
325
+
326
+ - **Multi-reviewer support** — v1 is one human, one AI, one JSON file. Supporting multiple concurrent reviewers would require a database (e.g., CouchDB) for concurrency, conflict resolution, and real-time sync. This is a fundamentally different product — a collaboration platform, not a review tool. Deferred indefinitely.
327
+
328
+ ## Inspirations
329
+
330
+ - **[difit](https://github.com/yoshiko-pg/difit)** — CLI that spins up a web server for reviewing git diffs with comments. Validated the "CLI → local server → browser → structured output on close" pattern.
331
+ - **[reviewthem.nvim](https://github.com/KEY60228/reviewthem.nvim)** — Neovim plugin for adding review comments with gutter signs and JSON export.
@@ -0,0 +1,141 @@
1
+ {
2
+ "file": "/Users/tuephan/repo/spectral/docs/superpowers/specs/2026-03-14-spec-review-tool-design.md",
3
+ "threads": [
4
+ {
5
+ "id": "t1",
6
+ "line": 29,
7
+ "status": "resolved",
8
+ "messages": [
9
+ {
10
+ "author": "human",
11
+ "text": "this is good"
12
+ },
13
+ {
14
+ "author": "human",
15
+ "text": "this is good"
16
+ },
17
+ {
18
+ "author": "human",
19
+ "text": "hello"
20
+ },
21
+ {
22
+ "author": "human",
23
+ "text": "is it me you are looking for"
24
+ },
25
+ {
26
+ "author": "ai",
27
+ "text": "Thanks! The three-layer architecture (Protocol, CLI, UI Adapters) is designed to keep each concern independent. Glad it reads well."
28
+ }
29
+ ]
30
+ },
31
+ {
32
+ "id": "t2",
33
+ "line": 41,
34
+ "status": "resolved",
35
+ "messages": [
36
+ {
37
+ "author": "human",
38
+ "text": "this is bad"
39
+ },
40
+ {
41
+ "author": "ai",
42
+ "text": "Could you clarify what you'd like changed about the review file format? The JSON structure uses threads as first-class objects — is it the schema itself, the naming, or the overall approach you'd like revised?"
43
+ }
44
+ ]
45
+ },
46
+ {
47
+ "id": "t3",
48
+ "line": 86,
49
+ "status": "resolved",
50
+ "messages": [
51
+ {
52
+ "author": "human",
53
+ "text": "are these status enough"
54
+ },
55
+ {
56
+ "author": "ai",
57
+ "text": "The four statuses (`open`, `pending`, `resolved`, `outdated`) cover the core ping-pong model. We discussed simplifying from 5 to 4 statuses earlier — dropping `addressed`/`discussed` in favor of the cleaner `open`/`pending` turn-based model. If you feel a status is missing for a specific workflow, happy to add it."
58
+ }
59
+ ]
60
+ },
61
+ {
62
+ "id": "t4",
63
+ "line": 245,
64
+ "status": "resolved",
65
+ "messages": [
66
+ {
67
+ "author": "human",
68
+ "text": "gello"
69
+ },
70
+ {
71
+ "author": "ai",
72
+ "text": "Did you mean to type something else here? If this was a test comment, feel free to resolve it."
73
+ }
74
+ ]
75
+ },
76
+ {
77
+ "id": "t5",
78
+ "line": 264,
79
+ "status": "resolved",
80
+ "messages": [
81
+ {
82
+ "author": "human",
83
+ "text": "this is bad"
84
+ },
85
+ {
86
+ "author": "ai",
87
+ "text": "This section describes the `:q` submit confirmation. Would you like a different confirmation flow, or is the concern about the keybinding choice itself?"
88
+ }
89
+ ]
90
+ },
91
+ {
92
+ "id": "t6",
93
+ "line": 296,
94
+ "status": "resolved",
95
+ "messages": [
96
+ {
97
+ "author": "human",
98
+ "text": "this is good"
99
+ },
100
+ {
101
+ "author": "ai",
102
+ "text": "Great — the Claude Code integration loop is the key piece that closes the human-AI feedback cycle. The `/review-revspec` skill handles the full loop: invoke revspec → read results → process threads → re-invoke."
103
+ }
104
+ ]
105
+ },
106
+ {
107
+ "id": "t7",
108
+ "line": 279,
109
+ "status": "resolved",
110
+ "messages": [
111
+ {
112
+ "author": "human",
113
+ "text": "hello"
114
+ },
115
+ {
116
+ "author": "human",
117
+ "text": "hello again"
118
+ }
119
+ ]
120
+ },
121
+ {
122
+ "id": "t8",
123
+ "line": 15,
124
+ "status": "open",
125
+ "messages": [
126
+ {
127
+ "author": "human",
128
+ "text": "hello"
129
+ },
130
+ {
131
+ "author": "human",
132
+ "text": "hello again"
133
+ },
134
+ {
135
+ "author": "human",
136
+ "text": "dfgdfgggggggggggggggggggggggggggg"
137
+ }
138
+ ]
139
+ }
140
+ ]
141
+ }
@@ -0,0 +1,26 @@
1
+ # Claude Code Integration Notes
2
+
3
+ Deferred — to be addressed when building the `/review-revspec` skill.
4
+
5
+ ## Problem
6
+
7
+ Claude Code's Bash tool runs without TTY (no stdin, no terminal). A TUI app can't take over the terminal directly.
8
+
9
+ ## Launch strategies
10
+
11
+ | Environment | Approach |
12
+ |---|---|
13
+ | tmux session (`$TMUX` set) | `tmux split-window -v "revspec <file>"` + `tmux wait-for` — seamless, human reviews in a pane |
14
+ | No tmux | Claude Code prints "Please run: `revspec <file>`" and polls for `.review.json` changes |
15
+
16
+ ## Existing infrastructure
17
+
18
+ - `~/.config/tmux/claude-hook.sh` — generic hook dispatcher
19
+ - `~/.config/tmux/claude-hooks.d/` — per-event hook directories
20
+ - `tmux-sudo-pane` skill pattern — existing precedent for spawning interactive commands in tmux
21
+
22
+ ## Open questions
23
+
24
+ - Can we use Claude Code's Ctrl+G (`$EDITOR`) mechanism? It has TTY access but is designed for text editors, not arbitrary TUIs.
25
+ - Should we register a Claude Code hook that auto-launches revspec when a review file is created?
26
+ - Polling frequency and mechanism for the no-tmux fallback.
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "revspec",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "revspec": "./bin/revspec.ts"
7
+ },
8
+ "scripts": {
9
+ "start": "bun run bin/revspec.ts",
10
+ "test": "bun test"
11
+ },
12
+ "devDependencies": {
13
+ "@types/bun": "latest"
14
+ },
15
+ "peerDependencies": {
16
+ "typescript": "^5"
17
+ },
18
+ "dependencies": {
19
+ "@opentui/core": "^0.1.87"
20
+ }
21
+ }
@@ -0,0 +1,76 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # Usage: ./scripts/release.sh [patch|minor|major]
5
+ # Defaults to patch if no argument given
6
+
7
+ INCREMENT="${1:-patch}"
8
+
9
+ if [[ "$INCREMENT" != "patch" && "$INCREMENT" != "minor" && "$INCREMENT" != "major" ]]; then
10
+ echo "Usage: ./scripts/release.sh [patch|minor|major]"
11
+ exit 1
12
+ fi
13
+
14
+ # Ensure clean working tree
15
+ if [ -n "$(git status --porcelain)" ]; then
16
+ echo "Error: working tree is dirty. Commit or stash changes first."
17
+ exit 1
18
+ fi
19
+
20
+ # Ensure on main
21
+ BRANCH=$(git branch --show-current)
22
+ if [ "$BRANCH" != "main" ]; then
23
+ echo "Error: must be on main branch (currently on $BRANCH)"
24
+ exit 1
25
+ fi
26
+
27
+ # Run tests
28
+ echo "Running tests..."
29
+ bun test || { echo "Tests failed. Aborting release."; exit 1; }
30
+
31
+ # Bump version
32
+ OLD_VERSION=$(jq -r '.version' package.json)
33
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$OLD_VERSION"
34
+
35
+ case "$INCREMENT" in
36
+ patch) PATCH=$((PATCH + 1)) ;;
37
+ minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
38
+ major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
39
+ esac
40
+
41
+ NEW_VERSION="$MAJOR.$MINOR.$PATCH"
42
+
43
+ echo ""
44
+ echo " $OLD_VERSION → $NEW_VERSION ($INCREMENT)"
45
+ echo ""
46
+ read -p "Proceed? [y/N] " -n 1 -r
47
+ echo ""
48
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
49
+ echo "Aborted."
50
+ exit 0
51
+ fi
52
+
53
+ # Update package.json
54
+ jq --arg v "$NEW_VERSION" '.version = $v' package.json > package.json.tmp && mv package.json.tmp package.json
55
+
56
+ # Also update the --version output in bin/revspec.ts
57
+ sed -i '' "s/revspec $OLD_VERSION/revspec $NEW_VERSION/" bin/revspec.ts 2>/dev/null || \
58
+ sed -i "s/revspec $OLD_VERSION/revspec $NEW_VERSION/" bin/revspec.ts
59
+
60
+ # Commit and tag
61
+ git add package.json bin/revspec.ts
62
+ git commit -m "release: v$NEW_VERSION"
63
+ git tag "v$NEW_VERSION"
64
+
65
+ # Publish to npm
66
+ echo ""
67
+ echo "Publishing to npm..."
68
+ npm publish
69
+
70
+ # Push
71
+ git push && git push origin "v$NEW_VERSION"
72
+
73
+ echo ""
74
+ echo "✔ Released v$NEW_VERSION"
75
+ echo " npm: https://www.npmjs.com/package/revspec"
76
+ echo " git: https://github.com/icyrainz/revspec/releases/tag/v$NEW_VERSION"
@@ -0,0 +1,52 @@
1
+ import type { ReviewFile, DraftFile, Thread } from "./types";
2
+
3
+ export function mergeDraftIntoReview(
4
+ review: ReviewFile | null,
5
+ draft: DraftFile,
6
+ specFile: string = ""
7
+ ): ReviewFile {
8
+ // Build a starting ReviewFile
9
+ const base: ReviewFile = review ?? { file: specFile, threads: [] };
10
+
11
+ // If draft has no threads, return review unchanged
12
+ const draftThreads = draft.threads;
13
+ if (!draftThreads || draftThreads.length === 0) {
14
+ return { ...base, threads: [...base.threads] };
15
+ }
16
+
17
+ // Index existing threads by id for O(1) lookup
18
+ const existingById = new Map<string, Thread>(
19
+ base.threads.map((t) => [t.id, t])
20
+ );
21
+
22
+ const mergedById = new Map<string, Thread>(
23
+ base.threads.map((t) => [t.id, { ...t, messages: [...t.messages] }])
24
+ );
25
+
26
+ for (const draftThread of draftThreads) {
27
+ const existing = existingById.get(draftThread.id);
28
+ if (!existing) {
29
+ // New thread — add it wholesale
30
+ mergedById.set(draftThread.id, draftThread);
31
+ } else {
32
+ // Existing thread — append only new messages (draft has full history)
33
+ const existingCount = existing.messages.length;
34
+ const newMessages = draftThread.messages.slice(existingCount);
35
+ const merged = mergedById.get(draftThread.id)!;
36
+ merged.messages.push(...newMessages);
37
+ merged.status = draftThread.status;
38
+ }
39
+ }
40
+
41
+ // Preserve original ordering of existing threads, then append new ones
42
+ const orderedIds = [
43
+ ...base.threads.map((t) => t.id),
44
+ ...draftThreads
45
+ .filter((t) => !existingById.has(t.id))
46
+ .map((t) => t.id),
47
+ ];
48
+
49
+ const threads = orderedIds.map((id) => mergedById.get(id)!);
50
+
51
+ return { ...base, threads };
52
+ }
@@ -0,0 +1,25 @@
1
+ import { readFileSync } from "fs";
2
+ import type { ReviewFile, DraftFile } from "./types";
3
+ import { isValidReviewFile } from "./types";
4
+
5
+ export function readReviewFile(path: string): ReviewFile | null {
6
+ try {
7
+ const raw = readFileSync(path, "utf8");
8
+ const parsed: unknown = JSON.parse(raw);
9
+ if (!isValidReviewFile(parsed)) return null;
10
+ return parsed;
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ export function readDraftFile(path: string): DraftFile | null {
17
+ try {
18
+ const raw = readFileSync(path, "utf8");
19
+ const parsed: unknown = JSON.parse(raw);
20
+ if (typeof parsed !== "object" || parsed === null) return null;
21
+ return parsed as DraftFile;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }