projview 0.8.2 → 0.8.3

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,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
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projview",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
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);