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 +38 -33
- package/docs/ai-tests/comments.md +118 -0
- package/docs/how-to-set-up.md +1 -1
- package/package.json +1 -1
- package/public/anchor.js +38 -0
- package/public/comments.js +4 -16
package/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# projview
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
directory, get a soft-dark cartoonish localhost view of every `.md` / `.mmd`
|
|
5
|
-
under it, close it and nothing's left
|
|
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
|
-
##
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
+
```
|
package/docs/how-to-set-up.md
CHANGED
|
@@ -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,
|
|
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
package/public/anchor.js
ADDED
|
@@ -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
|
+
}
|
package/public/comments.js
CHANGED
|
@@ -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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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);
|