projview 0.8.2 → 1.0.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.
package/README.md CHANGED
@@ -1,8 +1,11 @@
1
1
  # projview
2
2
 
3
- ephemeral local previewer for **markdown** + **mermaid** files. run it in any
4
- directory, get a soft-dark cartoonish localhost view of every `.md` / `.mmd`
5
- under it, close it and nothing's left behind.
3
+ a fast, zero-config local viewer for **markdown** + **mermaid** files. run it in
4
+ any directory, get a soft-dark cartoonish localhost view of every `.md` / `.mmd`
5
+ under it - rendered diagrams, search, live reload - close it and nothing's left
6
+ behind.
7
+
8
+ that's the product. point it at a docs folder, read comfortably, ctrl-c, gone.
6
9
 
7
10
  ## install
8
11
 
@@ -26,10 +29,28 @@ projview ./docs --port 5000
26
29
  projview /path/to/notes --no-open
27
30
  ```
28
31
 
29
- ## use it from scripts or an AI
32
+ ## what you get
33
+
34
+ | | |
35
+ | --- | --- |
36
+ | tree nav | every `.md` / `.mmd` under the root (skips `node_modules`, `dist`, dot-dirs) |
37
+ | rendering | markdown + syntax-highlighted code + **mermaid diagrams** (with pan/zoom) |
38
+ | search | `⌘K` palette - fuzzy filenames + full-text content with line snippets |
39
+ | live reload | edit a file, the browser updates |
40
+ | ephemeral | in-memory by default; nothing written to disk, nothing left behind |
41
+
42
+ see [samples/architecture.md](samples/architecture.md) for a mermaid demo.
43
+
44
+ ---
45
+
46
+ ## beyond viewing (optional)
47
+
48
+ the viewer is the point - everything below is opt-in and entirely ignorable.
30
49
 
31
- projview also has a **machine interface** - subcommands that print JSON to stdout
32
- and exit (no server, no browser), so an agent or script can query your docs:
50
+ ### machine interface
51
+
52
+ subcommands that print JSON to stdout and exit (no server, no browser), if a
53
+ script or agent wants to query the same docs:
33
54
 
34
55
  ```bash
35
56
  projview search "<query>" [path] # ranked: filename matches + in-content hits w/ line snippets
@@ -37,7 +58,6 @@ projview tree [path] # the file tree as JSON
37
58
  projview comment add <file> "…" # leave a comment (--heading <id> to anchor a section)
38
59
  projview comments [file] # list comments as JSON
39
60
  projview usage [path] # views + searches recorded by the viewer
40
- projview search "mermaid" --demo # quick test on the bundled docs
41
61
  ```
42
62
 
43
63
  ```jsonc
@@ -48,31 +68,18 @@ projview search "mermaid" --demo # quick test on the bundled docs
48
68
  "snippets": [ { "line": 68, "text": "### live reload" } ] } ] }
49
69
  ```
50
70
 
51
- ## what it does
52
-
53
- | step | detail |
54
- | --- | --- |
55
- | walk | recursively finds `.md` / `.mmd`, skips `node_modules`, `dist`, dot-dirs |
56
- | index | builds an in-memory tree (nothing written to disk by default) |
57
- | serve | spawns a localhost site + opens your browser |
58
- | live | edits to files reload in the browser automatically |
59
- | comment | leave notes on a file, a section, or a text selection (browser or CLI) |
60
-
61
- > ephemeral by default - ctrl-c and it's gone. opt into persistence below.
71
+ ### comments
62
72
 
63
- ## comments
73
+ notes on a whole file, a heading, or a selected passage - from the viewer (the
74
+ 💬 button) or the CLI / HTTP API. text comments **re-anchor** as the doc changes
75
+ and flag themselves *orphaned* only if the quoted text truly disappears. they
76
+ need a persistent store to survive a restart; writes are **localhost-only** (an
77
+ exposed instance is read-only).
64
78
 
65
- Leave notes on a whole file, a heading/section, or a selected passage - from the
66
- viewer (the 💬 button, bottom-right) or via the CLI / HTTP API. Text comments
67
- **re-anchor** as the doc changes and only flag themselves *orphaned* if the quoted
68
- text truly disappears. Comments persist when you opt into a store
69
- (`--persist` / `--store-url`); writes are **localhost-only** (an exposed instance
70
- is read-only).
79
+ ### persistence
71
80
 
72
- ## persistence (opt-in)
73
-
74
- By default the index is in-memory and nothing is written to disk. Opt in for a
75
- faster warm start, or a queryable store an AI can read:
81
+ by default the index is in-memory. opt in for a faster warm start, persistent
82
+ comments, or a queryable store:
76
83
 
77
84
  ```bash
78
85
  projview docs --persist # sqlite cache in ~/.projview/cache
@@ -87,7 +94,5 @@ projview docs --store-url "$DATABASE_URL" # your own postgres (need
87
94
  | `superlite` (JSON) | `--store superlite --persist` |
88
95
  | `custom` (sqlite/postgres URL) | `--store-url <url>` (or `DATABASE_URL`) |
89
96
 
90
- On re-run it reloads the cache and only re-reads files whose mtime changed.
91
- Connection strings come from env/`~/.projview` - never commit credentials.
92
-
93
- see [samples/architecture.md](samples/architecture.md) for a mermaid demo.
97
+ on re-run it reloads the cache and only re-reads files whose mtime changed.
98
+ connection strings come from env/`~/.projview` - never commit credentials.
@@ -0,0 +1,118 @@
1
+ # comments - AI test
2
+
3
+ An AI-driven test of projview's **comment machine interface** (`projview comment
4
+ add` / `comments` / `comment resolve|rm`, JSON on stdout). The scenario: an agent
5
+ reviews a doc set, leaves anchored review notes, and a second reader pulls them
6
+ back - no browser involved.
7
+
8
+ ## experimental parameters
9
+
10
+ | parameter | value |
11
+ | --- | --- |
12
+ | tool | `projview` (global install) |
13
+ | version | `0.8.2` |
14
+ | commands | `comment add`, `comments`, `comment resolve`, `comment rm`, `usage` |
15
+ | dataset | the bundled demo docs (`--demo`) |
16
+ | store | a persistent sqlite file (`--store-url sqlite://…`) - comments need a store |
17
+ | output | JSON per comment: `{ id, file, anchor, body, author, createdAt, resolved }` |
18
+
19
+ > Comments ride a **persistent store**. With the default ephemeral store the
20
+ > agent is warned the note won't survive exit, so pass `--persist` or
21
+ > `--store-url`.
22
+
23
+ ## method
24
+
25
+ An agent leaves review notes targeting different anchor types, then the notes are
26
+ listed, one resolved, and one deleted:
27
+
28
+ | # | file | anchor | aspect under test |
29
+ | --- | --- | --- | --- |
30
+ | 1 | `README.md` | file | whole-file note + agent self-tag (`--author claude`) |
31
+ | 2 | `getting-started.md` | heading | section anchor to a stable `pv-` heading id |
32
+ | 3 | lifecycle | — | list → resolve → delete over the CLI |
33
+
34
+ ## results
35
+
36
+ ### 1. leave an anchored, agent-tagged comment
37
+
38
+ ```bash
39
+ DB="sqlite:///tmp/review.sqlite"
40
+ projview comment add README.md "clear intro - add a 'what it is NOT' line" \
41
+ --author claude --store-url "$DB" --demo
42
+ ```
43
+
44
+ ```jsonc
45
+ { "id": "be15fdb7-…", "file": "README.md",
46
+ "anchor": { "type": "file" },
47
+ "body": "clear intro - add a 'what it is NOT' line",
48
+ "author": "claude", "resolved": false }
49
+ ```
50
+
51
+ ✅ The agent self-tags via `--author claude`, so AI notes are distinguishable
52
+ from human ones. Without `--author`, the author defaults to the git/OS user.
53
+
54
+ ### 2. anchor to a section (heading)
55
+
56
+ ```bash
57
+ projview comment add getting-started.md "could link forward to the api reference" \
58
+ --heading pv-headings-h1-h6 --author claude --store-url "$DB" --demo
59
+ ```
60
+
61
+ `anchor` becomes `{ "type": "heading", "id": "pv-headings-h1-h6" }`. Heading ids
62
+ are the `pv-`-prefixed slugs projview renders, so a section note survives edits to
63
+ the section body.
64
+
65
+ ✅ Three anchor flavours exist: `file`, `heading`, and `text` (a quote selector,
66
+ created from the viewer - it re-anchors on edits and flags itself *orphaned* only
67
+ if the quoted text truly disappears).
68
+
69
+ ### 3. read them back
70
+
71
+ ```bash
72
+ projview comments --store-url "$DB" --demo # all comments, as a JSON array
73
+ projview comments README.md --store-url "$DB" --demo # filtered to one file
74
+ ```
75
+
76
+ An agent can diff, summarise, or act on the returned objects programmatically.
77
+
78
+ ### 4. resolve + delete
79
+
80
+ ```bash
81
+ projview comment resolve <id> --store-url "$DB" --demo # -> { …, "resolved": true }
82
+ projview comment rm <id> --store-url "$DB" --demo # -> { "deleted": "<id>" }
83
+ ```
84
+
85
+ ✅ Full lifecycle over the CLI: create → list → resolve/reopen → delete. A
86
+ missing id exits non-zero with a clear message.
87
+
88
+ ### 5. usage
89
+
90
+ ```bash
91
+ projview usage --store-url "$DB" --demo
92
+ # { "total": 0, "byType": {}, "topFiles": [], "recentSearches": [] }
93
+ ```
94
+
95
+ `usage` is empty here because the **viewer** (not the CLI) records `view` /
96
+ `search` events; run the server and browse, then `usage` aggregates what was
97
+ looked at.
98
+
99
+ ## observations & limitations
100
+
101
+ - **Authorship**: explicit `--author` wins; otherwise the git `user.name` (or the
102
+ OS username). UI-created comments default to the server's git/OS user.
103
+ - **Persistence is required** for CLI comments to survive - the default store is
104
+ ephemeral and `comment add` warns when it isn't writable beyond the session.
105
+ - **Anchors**: `file` and `heading` are creatable from the CLI; `text` quote
106
+ anchors come from the viewer (they need a live text selection).
107
+ - **Writes are localhost-only** over HTTP - an exposed (`0.0.0.0`) instance is
108
+ read-only (`403` on write). The CLI writes the store directly, so it isn't gated.
109
+
110
+ ## reproduce
111
+
112
+ ```bash
113
+ npm i -g projview@latest
114
+ DB="sqlite:///tmp/review.sqlite"
115
+ projview comment add README.md "a note" --author claude --store-url "$DB" --demo
116
+ projview comments --store-url "$DB" --demo
117
+ projview usage --store-url "$DB" --demo
118
+ ```
@@ -67,7 +67,7 @@ npm install
67
67
  npm run demo # preview the bundled docs/ (alias for --demo)
68
68
  npm start # index the current directory
69
69
  node bin/projview.js ./docs # or call the CLI directly
70
- npm test # run the test suite (node:test, 49 tests)
70
+ npm test # run the test suite (node:test, 54 tests)
71
71
  ```
72
72
 
73
73
  See the [project README](README.md) for the architecture and the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projview",
3
- "version": "0.8.2",
3
+ "version": "1.0.0",
4
4
  "description": "Ephemeral local previewer for markdown + mermaid files in any directory",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,38 @@
1
+ /* text-quote re-anchoring - the pure part.
2
+ *
3
+ * findQuote() locates a {exact, prefix, suffix} selector in a flat string and
4
+ * returns the best start offset, disambiguating repeated text by surrounding
5
+ * context. It touches no DOM, so it's unit-testable directly (no jsdom). The
6
+ * caller (comments.js) maps the offset back to a DOM Range. */
7
+
8
+ export const commonPrefix = (a, b) => {
9
+ let i = 0;
10
+ while (i < a.length && i < b.length && a[i] === b[i]) i++;
11
+ return i;
12
+ };
13
+ export const commonSuffix = (a, b) => {
14
+ let i = 0;
15
+ while (i < a.length && i < b.length && a[a.length - 1 - i] === b[b.length - 1 - i]) i++;
16
+ return i;
17
+ };
18
+
19
+ // Best start offset of anchor.exact in `text`, or -1 if it isn't present.
20
+ // When `exact` repeats, the candidate whose surrounding text best matches
21
+ // prefix/suffix wins (ties -> earliest).
22
+ export function findQuote(text, anchor) {
23
+ const exact = anchor.exact || '';
24
+ if (!exact) return -1;
25
+ const prefix = anchor.prefix || '';
26
+ const suffix = anchor.suffix || '';
27
+ let best = -1;
28
+ let bestScore = -1;
29
+ let idx = text.indexOf(exact);
30
+ while (idx !== -1) {
31
+ const before = text.slice(Math.max(0, idx - prefix.length), idx);
32
+ const after = text.slice(idx + exact.length, idx + exact.length + suffix.length);
33
+ const score = commonSuffix(before, prefix) + commonPrefix(after, suffix);
34
+ if (score > bestScore) { bestScore = score; best = idx; }
35
+ idx = text.indexOf(exact, idx + 1);
36
+ }
37
+ return best;
38
+ }
@@ -10,6 +10,7 @@
10
10
  import { $, esc } from './dom.js';
11
11
  import { state } from './store.js';
12
12
  import { toast } from './toast.js';
13
+ import { findQuote } from './anchor.js';
13
14
 
14
15
  const contentEl = $('content');
15
16
  const viewerEl = $('viewer');
@@ -155,24 +156,11 @@ function pointAt(segs, offset) {
155
156
  return last ? { node: last.node, offset: last.node.nodeValue.length } : null;
156
157
  }
157
158
 
158
- const commonPrefix = (a, b) => { let i = 0; while (i < a.length && i < b.length && a[i] === b[i]) i++; return i; };
159
- const commonSuffix = (a, b) => { let i = 0; while (i < a.length && i < b.length && a[a.length - 1 - i] === b[b.length - 1 - i]) i++; return i; };
160
-
161
- // Resolve a {exact, prefix, suffix} quote selector to a Range, disambiguating
162
- // repeats by surrounding context. Returns null if `exact` isn't present.
159
+ // Resolve a {exact, prefix, suffix} quote selector to a DOM Range (using the
160
+ // pure finder in anchor.js). Returns null if `exact` isn't present (orphan).
163
161
  function resolveText(anchor, ctx) {
164
- const { text } = ctx;
165
162
  const exact = anchor.exact || '';
166
- if (!exact) return null;
167
- let best = -1, bestScore = -1;
168
- let idx = text.indexOf(exact);
169
- while (idx !== -1) {
170
- const before = text.slice(Math.max(0, idx - (anchor.prefix?.length || 0)), idx);
171
- const after = text.slice(idx + exact.length, idx + exact.length + (anchor.suffix?.length || 0));
172
- const score = commonSuffix(before, anchor.prefix || '') + commonPrefix(after, anchor.suffix || '');
173
- if (score > bestScore) { bestScore = score; best = idx; }
174
- idx = text.indexOf(exact, idx + 1);
175
- }
163
+ const best = exact ? findQuote(ctx.text, anchor) : -1;
176
164
  if (best === -1) return null;
177
165
  const a = pointAt(ctx.segs, best);
178
166
  const b = pointAt(ctx.segs, best + exact.length);