seendiff 0.0.2

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.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # seendiff (Node.js port)
2
+
3
+ A Node.js/Fastify port of [seendiff](https://github.com/acme-dot-bot/seendiff) —
4
+ a local, point-in-time diff tool with persistent review state. Faithful 1:1
5
+ port of the original Python/FastAPI tool, including block/section splitting,
6
+ walkthrough mode, syntax highlighting, and search.
7
+
8
+ ## Install & run
9
+
10
+ No install needed — run directly with npx from inside any git repo:
11
+
12
+ ```bash
13
+ npx seendiff
14
+ ```
15
+
16
+ Or install globally:
17
+
18
+ ```bash
19
+ npm install -g seendiff
20
+ seendiff
21
+ ```
22
+
23
+ Or from this source tree:
24
+
25
+ ```bash
26
+ npm install
27
+ npm link
28
+ seendiff
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```
34
+ usage: seendiff [-h] [--fetch] [--clear] [--no-auto-seen] [--no-highlight]
35
+ [--section-min N] [--section-max N] [--port PORT]
36
+ [--no-browser] [--walkthrough PATH]
37
+ [--check-walkthrough PATH]
38
+ [base]
39
+
40
+ positional arguments:
41
+ base base ref (default: origin/master, with fallbacks)
42
+
43
+ options:
44
+ -h, --help show this help message and exit
45
+ --fetch run git fetch before diffing (never implicit)
46
+ --clear clear review state for this scope, then continue
47
+ --no-auto-seen
48
+ --no-highlight
49
+ --section-min N smallest review section, in changed lines (default 12)
50
+ --section-max N largest review section, in changed lines (default 48)
51
+ --port PORT
52
+ --no-browser
53
+ --walkthrough PATH open a walkthrough JSON in walkthrough mode
54
+ --check-walkthrough PATH
55
+ validate a walkthrough JSON, print diagnostics, exit 0/1
56
+ ```
57
+
58
+ Run `seendiff` from inside a git repo with local commits ahead of
59
+ `origin/master`/`origin/main`. It opens a browser tab with a diff viewer
60
+ that groups changes into reviewable blocks, tracks seen/reviewed state
61
+ per-block in a local SQLite database (survives rebases via content hashing),
62
+ and supports syntax-highlighted search across the whole change.
63
+
64
+ ## Architecture
65
+
66
+ | Module | Purpose |
67
+ | -------------------- | ------------------------------------------------------------------------------------------------------ |
68
+ | `src/git.js` | Ref resolution, diff invocation, unified-diff parsing, block grouping/splitting (the core algorithm) |
69
+ | `src/store.js` | SQLite review-state persistence via `node:sqlite` |
70
+ | `src/highlight.js` | Syntax highlighting via `highlight.js`, mapped to Pygments-compatible CSS classes |
71
+ | `src/theme.js` | Serves pre-generated theme CSS (`src/theme-base.css`), built once from real Pygments for visual parity |
72
+ | `src/walkthrough.js` | JSON walkthrough-script loading, validation, ref resolution |
73
+ | `src/server.js` | Fastify app — all API routes, caching, host-allowlist guard |
74
+ | `src/cli.js` | Argument parsing, free-port selection, browser launch |
75
+ | `bin/seendiff.js` | npx/global-install entry point |
76
+ | `static/index.html` | Frontend — copied unmodified from upstream (backend-agnostic, talks only via `fetch`) |
77
+
78
+ ### Notable porting decisions
79
+
80
+ - **`node:sqlite`** instead of a third-party driver — zero extra native
81
+ dependencies, matches the "local, no server infra" spirit of the original.
82
+ - **`highlight.js`** instead of Pygments (Python-only) for tokenization, with
83
+ a class-mapping layer so the frontend's existing Pygments-derived CSS
84
+ selectors (`.hl .k`, `.hl .nf`, `.hl .c1`, etc.) work unchanged.
85
+ - **Theme CSS is not hand-approximated** — `src/theme-base.css` was generated
86
+ by actually running Pygments' `HtmlFormatter` against the same six styles
87
+ the original uses (`default`, `github-dark`, `solarized-dark`,
88
+ `solarized-light`, `nord`, `coffee`), so highlighting is visually identical
89
+ to the Python tool, not a lookalike.
90
+ - **Custom unified-diff parser** — rather than pull in a generic npm unidiff
91
+ library, `src/git.js` parses exactly the `git diff -U0 --find-renames`
92
+ subset seendiff needs (headers, renames, binary markers, hunks), which is
93
+ easier to verify against the original's behavior line-for-line.
94
+ - **Fastify's `listen()` doesn't block** like uvicorn's `run()` does — the
95
+ CLI intentionally never resolves after a successful listen, so the process
96
+ stays alive for the life of the server.
97
+
98
+ ## Testing
99
+
100
+ ```bash
101
+ npm install
102
+ node --test test/
103
+ ```
104
+
105
+ Port-fidelity was verified by:
106
+ - Translating the original's `test_sections.py` into `test/sections.test.js`
107
+ (all 7 cases pass) — validates the block/section-splitting algorithm,
108
+ the trickiest part of the port.
109
+ - **A/B differential testing**: running the real Python server and this
110
+ Node port side-by-side against identical repos/diffs and byte-comparing
111
+ every API response. Covered a 33-file/4,093-line synthetic stress diff
112
+ (25/25 checks passed: file listing, per-file status, full row-by-row
113
+ content, search, seen/reviewed mutation, host guard, prefs) and a
114
+ targeted edge-case repo covering renames-with-edits, binary files, CRLF
115
+ line endings, no-trailing-newline files, and Unicode filenames (34/34
116
+ checks passed after two real bugs were found and fixed — see below).
117
+ - Walkthrough parity: `--check-walkthrough` diagnostics, live `/api/walkthrough`
118
+ payload (including content digest), and a subtle ordering-dependent
119
+ validation quirk in the original (a step following one with an invalid
120
+ ref skips duplicate-id detection) were confirmed to match exactly.
121
+
122
+ ## License
123
+
124
+ MIT
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../src/cli.js";
3
+
4
+ main()
5
+ .then((code) => process.exit(code ?? 0))
6
+ .catch((err) => {
7
+ process.stderr.write(`seendiff: unexpected error: ${err.stack || err.message}\n`);
8
+ process.exit(1);
9
+ });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "seendiff",
3
+ "version": "0.0.2",
4
+ "description": "Local, point-in-time diff tool with persistent review state (Node.js port)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "Anand Pilania",
9
+ "email": "pilaniaanand@gmail.com",
10
+ "url": "https://github.com/AnandPilania"
11
+ },
12
+ "engines": {
13
+ "node": ">=22.5.0"
14
+ },
15
+ "bin": {
16
+ "seendiff": "./bin/seendiff.js"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "src",
21
+ "static",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "keywords": [
26
+ "diff",
27
+ "code-review",
28
+ "git",
29
+ "cli",
30
+ "coding-agent"
31
+ ],
32
+ "dependencies": {
33
+ "@fastify/compress": "^9.2.0",
34
+ "fastify": "^5.12.1",
35
+ "highlight.js": "^11.12.0",
36
+ "open": "^10.2.0"
37
+ }
38
+ }
package/src/cli.js ADDED
@@ -0,0 +1,216 @@
1
+ import { createServer } from "node:net";
2
+ import open from "open";
3
+
4
+ import * as G from "./git.js";
5
+ import * as store from "./store.js";
6
+ import * as W from "./walkthrough.js";
7
+ import { AppState, createApp } from "./server.js";
8
+
9
+ export function freePort() {
10
+ return new Promise((resolve, reject) => {
11
+ const srv = createServer();
12
+ srv.listen(0, "127.0.0.1", () => {
13
+ const { port } = srv.address();
14
+ srv.close(() => resolve(port));
15
+ });
16
+ srv.on("error", reject);
17
+ });
18
+ }
19
+
20
+ function parseArgs(argv) {
21
+ const args = {
22
+ base: null,
23
+ fetch: false,
24
+ clear: false,
25
+ noAutoSeen: false,
26
+ noHighlight: false,
27
+ sectionMin: G.SECTION_MIN,
28
+ sectionMax: G.SECTION_MAX,
29
+ port: null,
30
+ noBrowser: false,
31
+ walkthrough: null,
32
+ checkWalkthrough: null,
33
+ help: false,
34
+ };
35
+ const rest = [...argv];
36
+ while (rest.length) {
37
+ const a = rest.shift();
38
+ switch (a) {
39
+ case "--fetch":
40
+ args.fetch = true;
41
+ break;
42
+ case "--clear":
43
+ args.clear = true;
44
+ break;
45
+ case "--no-auto-seen":
46
+ args.noAutoSeen = true;
47
+ break;
48
+ case "--no-highlight":
49
+ args.noHighlight = true;
50
+ break;
51
+ case "--section-min":
52
+ args.sectionMin = parseInt(rest.shift(), 10);
53
+ break;
54
+ case "--section-max":
55
+ args.sectionMax = parseInt(rest.shift(), 10);
56
+ break;
57
+ case "--port":
58
+ args.port = parseInt(rest.shift(), 10);
59
+ break;
60
+ case "--no-browser":
61
+ args.noBrowser = true;
62
+ break;
63
+ case "--walkthrough":
64
+ args.walkthrough = rest.shift();
65
+ break;
66
+ case "--check-walkthrough":
67
+ args.checkWalkthrough = rest.shift();
68
+ break;
69
+ case "-h":
70
+ case "--help":
71
+ args.help = true;
72
+ break;
73
+ default:
74
+ if (a.startsWith("--section-min=")) args.sectionMin = parseInt(a.split("=")[1], 10);
75
+ else if (a.startsWith("--section-max=")) args.sectionMax = parseInt(a.split("=")[1], 10);
76
+ else if (a.startsWith("--port=")) args.port = parseInt(a.split("=")[1], 10);
77
+ else if (a.startsWith("--walkthrough=")) args.walkthrough = a.split("=").slice(1).join("=");
78
+ else if (a.startsWith("--check-walkthrough=")) args.checkWalkthrough = a.split("=").slice(1).join("=");
79
+ else if (!a.startsWith("-") && args.base === null) args.base = a;
80
+ break;
81
+ }
82
+ }
83
+ return args;
84
+ }
85
+
86
+ const HELP_TEXT = `usage: seendiff [-h] [--fetch] [--clear] [--no-auto-seen] [--no-highlight]
87
+ [--section-min N] [--section-max N] [--port PORT]
88
+ [--no-browser] [--walkthrough PATH]
89
+ [--check-walkthrough PATH]
90
+ [base]
91
+
92
+ Point-in-time diff tool with persistent review state.
93
+
94
+ positional arguments:
95
+ base base ref (default: origin/master, with fallbacks)
96
+
97
+ options:
98
+ -h, --help show this help message and exit
99
+ --fetch run git fetch before diffing (never implicit)
100
+ --clear clear review state for this scope, then continue
101
+ --no-auto-seen
102
+ --no-highlight
103
+ --section-min N smallest review section, in changed lines (default ${G.SECTION_MIN})
104
+ --section-max N largest review section, in changed lines (default ${G.SECTION_MAX});
105
+ a bigger block is cut at the nearest seam in the code
106
+ --port PORT
107
+ --no-browser
108
+ --walkthrough PATH open a walkthrough JSON in walkthrough mode
109
+ --check-walkthrough PATH
110
+ validate a walkthrough JSON, print diagnostics, exit 0/1
111
+ `;
112
+
113
+ export async function main(argv = process.argv.slice(2)) {
114
+ const args = parseArgs(argv);
115
+
116
+ if (args.help) {
117
+ process.stdout.write(HELP_TEXT);
118
+ return 0;
119
+ }
120
+
121
+ if (args.sectionMin < 1 || args.sectionMax < 2 * args.sectionMin) {
122
+ process.stderr.write("seendiff: --section-max must be at least twice --section-min\n");
123
+ return 1;
124
+ }
125
+
126
+ let repo;
127
+ try {
128
+ repo = G.repoRoot(".");
129
+ if (args.fetch) G.fetch(repo);
130
+ } catch (e) {
131
+ if (e instanceof G.GitError) {
132
+ process.stderr.write(`seendiff: ${e.message}\n`);
133
+ return 1;
134
+ }
135
+ throw e;
136
+ }
137
+
138
+ let baseRef;
139
+ try {
140
+ baseRef = G.resolveBase(repo, args.base);
141
+ } catch (e) {
142
+ if (!(e instanceof G.GitError)) throw e;
143
+ if (!(args.walkthrough || args.checkWalkthrough)) {
144
+ process.stderr.write(`seendiff: ${e.message}\n`);
145
+ return 1;
146
+ }
147
+ process.stderr.write(`seendiff: ${e.message} — walkthrough mode, using an empty diff\n`);
148
+ baseRef = "HEAD";
149
+ }
150
+
151
+ if (args.checkWalkthrough) {
152
+ const files = G.expandSubmodules(
153
+ repo,
154
+ G.identityDiff(repo, G.mergeBase(repo, baseRef), "HEAD", args.sectionMin, args.sectionMax),
155
+ args.sectionMin,
156
+ args.sectionMax
157
+ );
158
+ const hunkIds = {};
159
+ for (const fd of files) hunkIds[fd.path] = new Set(fd.blocks.map((b) => b.hunkId));
160
+ const problems = W.check(args.checkWalkthrough, repo, hunkIds);
161
+ for (const p of problems) process.stderr.write(p + "\n");
162
+ if (problems.length) return 1;
163
+ const wt = W.load(args.checkWalkthrough, repo, hunkIds);
164
+ if (wt === null) {
165
+ process.stdout.write("ok: empty walkthrough (plain diff until it's written)\n");
166
+ } else {
167
+ process.stdout.write(`ok: ${JSON.stringify(wt.title)} — ${wt.acts.length} acts, ${wt.steps.length} steps\n`);
168
+ }
169
+ return 0;
170
+ }
171
+
172
+ const st = new AppState({
173
+ repo,
174
+ baseRef,
175
+ sectionMin: args.sectionMin,
176
+ sectionMax: args.sectionMax,
177
+ noHighlight: args.noHighlight,
178
+ autoSeen: !args.noAutoSeen,
179
+ wtFile: args.walkthrough,
180
+ });
181
+
182
+ if (args.clear) {
183
+ const db = store.openDb();
184
+ const mb = G.mergeBase(repo, baseRef);
185
+ const scope = store.scopeKey(repo, baseRef, G.branchName(repo), mb);
186
+ store.clearScope(db, scope);
187
+ db.close();
188
+ process.stdout.write(`cleared review state for ${scope}\n`);
189
+ }
190
+
191
+ const app = await createApp(st);
192
+ if (args.walkthrough) {
193
+ st.loadWalkthrough();
194
+ if (st.wt === null && !st.wtEmpty) {
195
+ process.stderr.write("seendiff: walkthrough failed to load:\n");
196
+ for (const p of (st.wtError || "?").split("\n")) process.stderr.write(` ${p}\n`);
197
+ return 1;
198
+ }
199
+ if (st.wt === null) {
200
+ process.stderr.write(`seendiff: empty walkthrough — plain diff, watching ${args.walkthrough}\n`);
201
+ }
202
+ }
203
+
204
+ const port = args.port || (await freePort());
205
+ const url = `http://127.0.0.1:${port}/`;
206
+ process.stdout.write(
207
+ `seendiff: ${st.branch || "detached"} vs ${baseRef} (merge base ${st.mbSha.slice(0, 12)}) — ${url}\n`
208
+ );
209
+ if (!args.noBrowser) {
210
+ setTimeout(() => {
211
+ open(url).catch(() => { });
212
+ }, 600);
213
+ }
214
+ await app.listen({ port, host: "127.0.0.1" });
215
+ return new Promise(() => { });
216
+ }