@henryavila/mdprobe 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.
- package/README.md +404 -0
- package/bin/cli.js +335 -0
- package/dist/assets/index-DPysqH1p.js +2 -0
- package/dist/assets/index-nl9v2RuJ.css +1 -0
- package/dist/index.html +19 -0
- package/package.json +75 -0
- package/schema.json +104 -0
- package/skills/mdprobe/SKILL.md +358 -0
- package/src/anchoring.js +262 -0
- package/src/annotations.js +504 -0
- package/src/cli-utils.js +58 -0
- package/src/config.js +76 -0
- package/src/export.js +211 -0
- package/src/handler.js +229 -0
- package/src/hash.js +51 -0
- package/src/renderer.js +247 -0
- package/src/server.js +849 -0
- package/src/ui/app.jsx +152 -0
- package/src/ui/components/AnnotationForm.jsx +72 -0
- package/src/ui/components/Content.jsx +334 -0
- package/src/ui/components/ExportMenu.jsx +62 -0
- package/src/ui/components/LeftPanel.jsx +99 -0
- package/src/ui/components/Popover.jsx +94 -0
- package/src/ui/components/ReplyThread.jsx +28 -0
- package/src/ui/components/RightPanel.jsx +171 -0
- package/src/ui/components/SectionApproval.jsx +31 -0
- package/src/ui/components/ThemePicker.jsx +18 -0
- package/src/ui/hooks/useAnnotations.js +160 -0
- package/src/ui/hooks/useClientLibs.js +97 -0
- package/src/ui/hooks/useKeyboard.js +128 -0
- package/src/ui/hooks/useTheme.js +57 -0
- package/src/ui/hooks/useWebSocket.js +126 -0
- package/src/ui/index.html +19 -0
- package/src/ui/state/store.js +76 -0
- package/src/ui/styles/themes.css +1243 -0
package/schema.json
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "mdprobe annotation sidecar",
|
|
4
|
+
"description": "Schema for YAML sidecar annotation files used by mdprobe",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["version", "source", "source_hash", "annotations"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"version": {
|
|
9
|
+
"type": "integer"
|
|
10
|
+
},
|
|
11
|
+
"source": {
|
|
12
|
+
"type": "string"
|
|
13
|
+
},
|
|
14
|
+
"source_hash": {
|
|
15
|
+
"type": "string"
|
|
16
|
+
},
|
|
17
|
+
"sections": {
|
|
18
|
+
"type": "array",
|
|
19
|
+
"items": {
|
|
20
|
+
"$ref": "#/definitions/section"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"annotations": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"items": {
|
|
26
|
+
"$ref": "#/definitions/annotation"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"definitions": {
|
|
31
|
+
"position": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"required": ["startLine", "startColumn", "endLine", "endColumn"],
|
|
34
|
+
"properties": {
|
|
35
|
+
"startLine": { "type": "integer" },
|
|
36
|
+
"startColumn": { "type": "integer" },
|
|
37
|
+
"endLine": { "type": "integer" },
|
|
38
|
+
"endColumn": { "type": "integer" }
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"quote": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"required": ["exact", "prefix", "suffix"],
|
|
44
|
+
"properties": {
|
|
45
|
+
"exact": { "type": "string" },
|
|
46
|
+
"prefix": { "type": "string" },
|
|
47
|
+
"suffix": { "type": "string" }
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"selectors": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"required": ["position", "quote"],
|
|
53
|
+
"properties": {
|
|
54
|
+
"position": { "$ref": "#/definitions/position" },
|
|
55
|
+
"quote": { "$ref": "#/definitions/quote" }
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"reply": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"required": ["author", "comment", "created_at"],
|
|
61
|
+
"properties": {
|
|
62
|
+
"author": { "type": "string" },
|
|
63
|
+
"comment": { "type": "string" },
|
|
64
|
+
"created_at": { "type": "string" }
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"annotation": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"required": ["id", "selectors", "comment", "tag", "status", "author", "created_at", "updated_at", "replies"],
|
|
70
|
+
"properties": {
|
|
71
|
+
"id": { "type": "string" },
|
|
72
|
+
"selectors": { "$ref": "#/definitions/selectors" },
|
|
73
|
+
"comment": { "type": "string" },
|
|
74
|
+
"tag": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"enum": ["bug", "question", "suggestion", "nitpick"]
|
|
77
|
+
},
|
|
78
|
+
"status": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"enum": ["open", "resolved"]
|
|
81
|
+
},
|
|
82
|
+
"author": { "type": "string" },
|
|
83
|
+
"created_at": { "type": "string" },
|
|
84
|
+
"updated_at": { "type": "string" },
|
|
85
|
+
"replies": {
|
|
86
|
+
"type": "array",
|
|
87
|
+
"items": { "$ref": "#/definitions/reply" }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"section": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"required": ["heading", "status"],
|
|
94
|
+
"properties": {
|
|
95
|
+
"heading": { "type": "string" },
|
|
96
|
+
"level": { "type": "integer", "minimum": 1, "maximum": 6 },
|
|
97
|
+
"status": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"enum": ["approved", "rejected", "pending"]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mdprobe
|
|
3
|
+
description: Use mdprobe to render markdown in the browser and collect structured human feedback (annotations, section approvals) via YAML sidecar files
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# mdprobe — Markdown Viewer & Reviewer
|
|
7
|
+
|
|
8
|
+
Render markdown in the browser. Collect structured feedback from humans. Read it back as YAML.
|
|
9
|
+
|
|
10
|
+
## When to Use
|
|
11
|
+
|
|
12
|
+
- Output longer than 40-60 lines (specs, RFCs, ADRs, design docs)
|
|
13
|
+
- Tables, Mermaid diagrams, math/LaTeX, syntax-highlighted code
|
|
14
|
+
- When you need the human to **review and annotate** before you proceed
|
|
15
|
+
- When you need **section-level approval** (approved/rejected per heading)
|
|
16
|
+
|
|
17
|
+
## When NOT to Use
|
|
18
|
+
|
|
19
|
+
- Short answers or code snippets (< 40 lines)
|
|
20
|
+
- Simple text responses
|
|
21
|
+
- Interactive debugging sessions
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## View Mode — render and continue working
|
|
26
|
+
|
|
27
|
+
Write markdown to a file, launch mdprobe in the background. The human reads while you keep working.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Write your output
|
|
31
|
+
cat > output.md << 'EOF'
|
|
32
|
+
# Your spec here
|
|
33
|
+
EOF
|
|
34
|
+
|
|
35
|
+
# Launch viewer (browser opens automatically, process runs in background)
|
|
36
|
+
mdprobe output.md &
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Use `run_in_background: true` when calling via Bash tool. Add `--no-open` if you don't want the browser to auto-open.
|
|
40
|
+
|
|
41
|
+
The server watches for file changes — if you update the `.md` file, the browser hot-reloads automatically.
|
|
42
|
+
|
|
43
|
+
## Review Mode — block until human finishes
|
|
44
|
+
|
|
45
|
+
When you need the human to review and annotate before you continue:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# This BLOCKS until the human clicks "Finish Review" in the browser
|
|
49
|
+
mdprobe spec.md --once
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The process prints paths to generated `.annotations.yaml` files on exit. Exit code 0 means review complete.
|
|
53
|
+
|
|
54
|
+
### Full agent workflow
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
1. Agent writes spec.md
|
|
58
|
+
2. Agent runs: mdprobe spec.md --once (process BLOCKS here)
|
|
59
|
+
3. Human opens browser, reads the rendered markdown
|
|
60
|
+
4. Human selects text → adds annotations (bug, question, suggestion, nitpick)
|
|
61
|
+
5. Human approves/rejects sections via heading buttons
|
|
62
|
+
6. Human clicks "Finish Review"
|
|
63
|
+
7. Process unblocks, prints YAML paths to stdout
|
|
64
|
+
8. Agent reads spec.annotations.yaml
|
|
65
|
+
9. Agent addresses each annotation
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Reading Annotations
|
|
71
|
+
|
|
72
|
+
After review, load the YAML sidecar and process the feedback:
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
import { AnnotationFile } from '@henryavila/mdprobe/annotations'
|
|
76
|
+
|
|
77
|
+
const af = await AnnotationFile.load('spec.annotations.yaml')
|
|
78
|
+
|
|
79
|
+
// Query annotations
|
|
80
|
+
const open = af.getOpen() // all unresolved annotations
|
|
81
|
+
const bugs = af.getByTag('bug') // only bugs
|
|
82
|
+
const questions = af.getByTag('question')
|
|
83
|
+
const mine = af.getByAuthor('Alice')
|
|
84
|
+
const resolved = af.getResolved() // already handled
|
|
85
|
+
const one = af.getById('a1b2c3d4') // specific annotation
|
|
86
|
+
|
|
87
|
+
// Each annotation has:
|
|
88
|
+
// {
|
|
89
|
+
// id, selectors: { position: { startLine, startColumn, endLine, endColumn },
|
|
90
|
+
// quote: { exact, prefix, suffix } },
|
|
91
|
+
// comment, tag, status, author, created_at, updated_at,
|
|
92
|
+
// replies: [{ author, comment, created_at }]
|
|
93
|
+
// }
|
|
94
|
+
|
|
95
|
+
// Process feedback
|
|
96
|
+
for (const ann of open) {
|
|
97
|
+
console.log(`[${ann.tag}] Line ${ann.selectors.position.startLine}: ${ann.comment}`)
|
|
98
|
+
if (ann.replies.length > 0) {
|
|
99
|
+
for (const reply of ann.replies) {
|
|
100
|
+
console.log(` ↳ ${reply.author}: ${reply.comment}`)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Mark as handled
|
|
106
|
+
af.resolve(bugs[0].id)
|
|
107
|
+
await af.save('spec.annotations.yaml')
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Checking Section Approvals
|
|
111
|
+
|
|
112
|
+
The human can approve or reject each section (heading) of the document. Check the status:
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
const af = await AnnotationFile.load('spec.annotations.yaml')
|
|
116
|
+
|
|
117
|
+
// sections: [{ heading, level, status }]
|
|
118
|
+
// status is: 'approved', 'rejected', or 'pending'
|
|
119
|
+
for (const section of af.sections) {
|
|
120
|
+
console.log(`${section.heading}: ${section.status}`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check if all sections were approved
|
|
124
|
+
const allApproved = af.sections.every(s => s.status === 'approved')
|
|
125
|
+
|
|
126
|
+
// Find rejected sections that need rework
|
|
127
|
+
const rejected = af.sections.filter(s => s.status === 'rejected')
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Approval cascades: if the human approves a parent heading (e.g., H2), all child headings (H3, H4...) under it are also approved. Same for reject and reset.
|
|
131
|
+
|
|
132
|
+
## Export Formats
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
import { exportJSON, exportSARIF, exportReport } from '@henryavila/mdprobe/export'
|
|
136
|
+
import { readFile } from 'node:fs/promises'
|
|
137
|
+
|
|
138
|
+
const af = await AnnotationFile.load('spec.annotations.yaml')
|
|
139
|
+
const source = await readFile('spec.md', 'utf-8')
|
|
140
|
+
|
|
141
|
+
const json = exportJSON(af) // plain JS object
|
|
142
|
+
const sarif = exportSARIF(af, 'spec.md') // SARIF 2.1.0 (open annotations only)
|
|
143
|
+
const report = exportReport(af, source) // markdown review report
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
SARIF maps tags to severity: `bug` = error, `suggestion` = warning, `question`/`nitpick` = note.
|
|
147
|
+
|
|
148
|
+
## Annotation Tags
|
|
149
|
+
|
|
150
|
+
| Tag | Meaning | When the human uses it |
|
|
151
|
+
|-----|---------|----------------------|
|
|
152
|
+
| `bug` | Something is wrong | Factual errors, incorrect logic, broken examples |
|
|
153
|
+
| `question` | Needs clarification | Ambiguous requirements, missing context |
|
|
154
|
+
| `suggestion` | Improvement idea | Better approach, additional feature, alternative |
|
|
155
|
+
| `nitpick` | Minor style/wording | Typos, formatting, naming preferences |
|
|
156
|
+
|
|
157
|
+
## Interacting with Annotations
|
|
158
|
+
|
|
159
|
+
### Resolving annotations after you fix them
|
|
160
|
+
|
|
161
|
+
After addressing an annotation, mark it as resolved so the human knows it's been handled:
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
import { AnnotationFile } from '@henryavila/mdprobe/annotations'
|
|
165
|
+
|
|
166
|
+
const af = await AnnotationFile.load('spec.annotations.yaml')
|
|
167
|
+
|
|
168
|
+
for (const ann of af.getOpen()) {
|
|
169
|
+
// Process the annotation (fix the bug, answer the question, etc.)
|
|
170
|
+
|
|
171
|
+
// Mark as resolved
|
|
172
|
+
af.resolve(ann.id)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Persist changes — the human will see these as resolved in the UI
|
|
176
|
+
await af.save('spec.annotations.yaml')
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Replying to annotations
|
|
180
|
+
|
|
181
|
+
Add a reply to explain what you did, ask for clarification, or acknowledge the feedback:
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
const af = await AnnotationFile.load('spec.annotations.yaml')
|
|
185
|
+
|
|
186
|
+
const bugs = af.getByTag('bug')
|
|
187
|
+
for (const bug of bugs) {
|
|
188
|
+
af.addReply(bug.id, {
|
|
189
|
+
author: 'Agent',
|
|
190
|
+
comment: `Fixed in commit abc123. Changed line ${bug.selectors.position.startLine}.`,
|
|
191
|
+
})
|
|
192
|
+
af.resolve(bug.id)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await af.save('spec.annotations.yaml')
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Creating annotations before human review
|
|
199
|
+
|
|
200
|
+
Pre-annotate sections you're unsure about, so the human knows where to focus:
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
const af = await AnnotationFile.load('spec.annotations.yaml')
|
|
204
|
+
|
|
205
|
+
af.add({
|
|
206
|
+
selectors: {
|
|
207
|
+
position: { startLine: 42, startColumn: 1, endLine: 42, endColumn: 60 },
|
|
208
|
+
quote: { exact: 'Rate limit: 100 requests per minute', prefix: '', suffix: '' },
|
|
209
|
+
},
|
|
210
|
+
comment: 'Is 100/min enough? The load test showed spikes of 300/min.',
|
|
211
|
+
tag: 'question',
|
|
212
|
+
author: 'Agent',
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
await af.save('spec.annotations.yaml')
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Interacting via HTTP API (while server is running)
|
|
219
|
+
|
|
220
|
+
If the server is running (view mode), you can interact without touching the YAML file directly:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# Create an annotation
|
|
224
|
+
curl -X POST http://127.0.0.1:3000/api/annotations -H 'Content-Type: application/json' -d '{
|
|
225
|
+
"file": "spec.md",
|
|
226
|
+
"action": "add",
|
|
227
|
+
"data": {
|
|
228
|
+
"selectors": {
|
|
229
|
+
"position": { "startLine": 10, "startColumn": 1, "endLine": 10, "endColumn": 40 },
|
|
230
|
+
"quote": { "exact": "text to annotate", "prefix": "", "suffix": "" }
|
|
231
|
+
},
|
|
232
|
+
"comment": "This needs work",
|
|
233
|
+
"tag": "suggestion",
|
|
234
|
+
"author": "Agent"
|
|
235
|
+
}
|
|
236
|
+
}'
|
|
237
|
+
|
|
238
|
+
# Resolve an annotation
|
|
239
|
+
curl -X POST http://127.0.0.1:3000/api/annotations -H 'Content-Type: application/json' -d '{
|
|
240
|
+
"file": "spec.md",
|
|
241
|
+
"action": "resolve",
|
|
242
|
+
"data": { "id": "a1b2c3d4" }
|
|
243
|
+
}'
|
|
244
|
+
|
|
245
|
+
# Add a reply
|
|
246
|
+
curl -X POST http://127.0.0.1:3000/api/annotations -H 'Content-Type: application/json' -d '{
|
|
247
|
+
"file": "spec.md",
|
|
248
|
+
"action": "reply",
|
|
249
|
+
"data": { "id": "a1b2c3d4", "author": "Agent", "comment": "Fixed." }
|
|
250
|
+
}'
|
|
251
|
+
|
|
252
|
+
# Approve a section
|
|
253
|
+
curl -X POST http://127.0.0.1:3000/api/sections -H 'Content-Type: application/json' -d '{
|
|
254
|
+
"file": "spec.md",
|
|
255
|
+
"action": "approve",
|
|
256
|
+
"heading": "Requirements"
|
|
257
|
+
}'
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The browser auto-updates when annotations change — the human sees your replies and resolutions in real time.
|
|
261
|
+
|
|
262
|
+
### Iterative review loop
|
|
263
|
+
|
|
264
|
+
When the first review produces feedback, fix the issues and re-launch for a second pass:
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
Round 1:
|
|
268
|
+
1. Agent writes spec.md
|
|
269
|
+
2. mdprobe spec.md --once → human annotates 5 bugs, 3 questions
|
|
270
|
+
3. Agent reads feedback, fixes all 5 bugs, answers 3 questions
|
|
271
|
+
4. Agent marks all 8 as resolved, adds replies explaining fixes
|
|
272
|
+
|
|
273
|
+
Round 2:
|
|
274
|
+
5. Agent re-launches: mdprobe spec.md --once
|
|
275
|
+
6. Human sees resolved items (greyed out), reviews fixes
|
|
276
|
+
7. Human adds 1 new nitpick, approves all sections
|
|
277
|
+
8. Agent reads feedback — 1 nitpick to fix, all sections approved
|
|
278
|
+
9. Done — proceed to implementation
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
// After fixing issues from round 1:
|
|
283
|
+
const af = await AnnotationFile.load('spec.annotations.yaml')
|
|
284
|
+
|
|
285
|
+
// Mark everything as resolved with explanations
|
|
286
|
+
for (const ann of af.getOpen()) {
|
|
287
|
+
af.addReply(ann.id, {
|
|
288
|
+
author: 'Agent',
|
|
289
|
+
comment: 'Addressed in updated spec.',
|
|
290
|
+
})
|
|
291
|
+
af.resolve(ann.id)
|
|
292
|
+
}
|
|
293
|
+
await af.save('spec.annotations.yaml')
|
|
294
|
+
|
|
295
|
+
// Re-launch for round 2
|
|
296
|
+
// exec: mdprobe spec.md --once
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Drift Detection
|
|
300
|
+
|
|
301
|
+
If you modify the source `.md` after annotations were created, mdprobe warns the human that the source has changed (annotations may be stale). The hash is stored in the YAML:
|
|
302
|
+
|
|
303
|
+
```yaml
|
|
304
|
+
source_hash: "sha256:abc123..."
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Schema Validation
|
|
308
|
+
|
|
309
|
+
A JSON Schema is available for validating annotation YAML files:
|
|
310
|
+
|
|
311
|
+
```javascript
|
|
312
|
+
import schema from '@henryavila/mdprobe/schema.json'
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Recommended Patterns
|
|
318
|
+
|
|
319
|
+
### Pattern: spec review before implementation
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
# 1. Write the spec
|
|
323
|
+
cat > spec.md << 'SPEC'
|
|
324
|
+
# Feature: User Authentication
|
|
325
|
+
## Requirements
|
|
326
|
+
...
|
|
327
|
+
SPEC
|
|
328
|
+
|
|
329
|
+
# 2. Get human review (blocks until done)
|
|
330
|
+
mdprobe spec.md --once
|
|
331
|
+
|
|
332
|
+
# 3. Read feedback
|
|
333
|
+
node -e "
|
|
334
|
+
import { AnnotationFile } from '@henryavila/mdprobe/annotations'
|
|
335
|
+
const af = await AnnotationFile.load('spec.annotations.yaml')
|
|
336
|
+
console.log(JSON.stringify(af.getOpen(), null, 2))
|
|
337
|
+
"
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Pattern: background viewer while working
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
# Start viewer in background
|
|
344
|
+
mdprobe docs/ --no-open &
|
|
345
|
+
|
|
346
|
+
# Continue working — browser shows rendered docs with live reload
|
|
347
|
+
# Human reads at their own pace
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Pattern: check if human approved all sections
|
|
351
|
+
|
|
352
|
+
```javascript
|
|
353
|
+
const af = await AnnotationFile.load('spec.annotations.yaml')
|
|
354
|
+
const pending = af.sections.filter(s => s.status !== 'approved')
|
|
355
|
+
if (pending.length > 0) {
|
|
356
|
+
console.log('Sections not yet approved:', pending.map(s => s.heading))
|
|
357
|
+
}
|
|
358
|
+
```
|