markback 0.3.0__tar.gz → 0.3.1__tar.gz
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.
- markback-0.3.1/AGENTS.md +213 -0
- {markback-0.3.0 → markback-0.3.1}/PKG-INFO +55 -7
- {markback-0.3.0 → markback-0.3.1}/README.md +54 -6
- {markback-0.3.0 → markback-0.3.1}/SPEC.md +3 -3
- markback-0.3.1/markback/_headers.py +14 -0
- {markback-0.3.0 → markback-0.3.1}/markback/linter.py +15 -1
- {markback-0.3.0 → markback-0.3.1}/markback/parser.py +28 -24
- {markback-0.3.0 → markback-0.3.1}/markback/writer.py +41 -39
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/CHANGELOG.md +10 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/PUBLISH.md +5 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/package.json +1 -1
- markback-0.3.1/packages/markback-vscode/publish.ps1 +79 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/src/preview/inject.js +58 -1
- markback-0.3.1/packages/markback-vscode/test/wordDelete.test.js +79 -0
- {markback-0.3.0 → markback-0.3.1}/pyproject.toml +1 -1
- markback-0.3.1/scripts/gen_headers.py +94 -0
- markback-0.3.1/shared/headers.json +7 -0
- markback-0.3.1/skills/README.md +26 -0
- markback-0.3.1/skills/markback/SKILL.md +432 -0
- markback-0.3.1/skills/markback/references/tooling.md +132 -0
- markback-0.3.1/tests/test_headers_registry.py +56 -0
- {markback-0.3.0 → markback-0.3.1}/tests/test_linter.py +30 -0
- {markback-0.3.0 → markback-0.3.1}/tests/test_parser.py +71 -0
- markback-0.3.0/AGENTS.md +0 -135
- {markback-0.3.0 → markback-0.3.1}/.gitignore +0 -0
- {markback-0.3.0 → markback-0.3.1}/.ishipped/card.md +0 -0
- {markback-0.3.0 → markback-0.3.1}/.ishipped/markback-icon.png +0 -0
- {markback-0.3.0 → markback-0.3.1}/IMPLEMENTATION_NOTES.md +0 -0
- {markback-0.3.0 → markback-0.3.1}/LICENSE +0 -0
- {markback-0.3.0 → markback-0.3.1}/markback/__init__.py +0 -0
- {markback-0.3.0 → markback-0.3.1}/markback/cli.py +0 -0
- {markback-0.3.0 → markback-0.3.1}/markback/types.py +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/.vscode/launch.json +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/.vscode/tasks.json +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/.vscodeignore +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/LICENSE +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/README.md +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/esbuild.js +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/icon.png +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/media/SHOTLIST.md +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/package-lock.json +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/src/actionState.ts +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/src/author.ts +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/src/commandLayer.ts +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/src/commentControlPlane.ts +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/src/excerpt.ts +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/src/extension.ts +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/src/focusCollision.ts +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/src/ids.ts +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/src/markdownItPlugin.ts +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/src/output.ts +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/src/preview/inject.css +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/src/projection.ts +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/src/rangeCodec.ts +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/src/sidecarPath.ts +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/src/sidecarRepository.ts +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/test/actionState.test.js +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/test/excerpt.test.js +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/test/focusCollision.test.js +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/test/ids.test.js +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/test/projection.test.js +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/test/rangeCodec.test.js +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/test/sidecarPath.test.js +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/test/sidecarRepository.test.js +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/test-integration/runTest.js +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/test-integration/suite/actions.test.js +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/test-integration/suite/commentMultiAndCancel.test.js +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/test-integration/suite/commentSelection.test.js +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/test-integration/suite/index.js +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/test-integration/suite/inlineExcerpt.test.js +0 -0
- {markback-0.3.0 → markback-0.3.1}/packages/markback-vscode/tsconfig.json +0 -0
- {markback-0.3.0 → markback-0.3.1}/scripts/bump-version.sh +0 -0
- {markback-0.3.0 → markback-0.3.1}/scripts/publish-npm.sh +0 -0
- {markback-0.3.0 → markback-0.3.1}/scripts/publish-pypi.sh +0 -0
- {markback-0.3.0 → markback-0.3.1}/scripts/publish.sh +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/__init__.py +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/fixtures/compact_source.mb +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/fixtures/errors/content_with_source.mb +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/fixtures/errors/empty_feedback.mb +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/fixtures/errors/malformed_uri.mb +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/fixtures/errors/missing_feedback.mb +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/fixtures/errors/multiple_feedback.mb +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/fixtures/essay.label.txt +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/fixtures/essay.txt +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/fixtures/external_source.mb +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/fixtures/freeform_feedback.mb +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/fixtures/json_feedback.mb +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/fixtures/label_list.mb +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/fixtures/minimal.mb +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/fixtures/minimal.mb.mb +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/fixtures/multi_record.mb +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/fixtures/with_uri.mb +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/test_actions.py +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/test_cli.py +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/test_eol.py +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/test_types.py +0 -0
- {markback-0.3.0 → markback-0.3.1}/tests/test_writer.py +0 -0
markback-0.3.1/AGENTS.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Writing Markback (.mb) files
|
|
2
|
+
|
|
3
|
+
Markback pairs content with single-line feedback using `<<<` as the delimiter.
|
|
4
|
+
|
|
5
|
+
This is the quickstart. For the full read/write manual — including the traps
|
|
6
|
+
that cost real time — load `skills/markback/SKILL.md`. `SPEC.md` is the
|
|
7
|
+
authority.
|
|
8
|
+
|
|
9
|
+
## Minimal record
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Some content here.
|
|
13
|
+
<<< positive
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Record with headers
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
@id item-001
|
|
20
|
+
@by reviewer@example.com
|
|
21
|
+
@file ./file.txt
|
|
22
|
+
@input ./prompt.txt
|
|
23
|
+
@tag review p1
|
|
24
|
+
|
|
25
|
+
Inline content goes here.
|
|
26
|
+
<<< good; quality=high
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Headers: `@id`, `@reply-to`, `@by`, `@action`, `@tag`, `@input`, `@file`. All
|
|
30
|
+
optional, and that is also their canonical order.
|
|
31
|
+
|
|
32
|
+
## Rules
|
|
33
|
+
|
|
34
|
+
- `<<<` must be followed by one space then feedback text
|
|
35
|
+
- a blank line is **required** between headers and inline content — omit it and
|
|
36
|
+
a content line starting with `@` is parsed as a header, and the content is
|
|
37
|
+
silently dropped
|
|
38
|
+
- `@file` + inline content can coexist (file is provenance, content is snapshot)
|
|
39
|
+
- full records are separated by `---`, written directly after the preceding
|
|
40
|
+
`<<<` line with no blank line before it
|
|
41
|
+
- files must be UTF-8 with LF line endings
|
|
42
|
+
- `@id` values are plain strings (no URI validation)
|
|
43
|
+
|
|
44
|
+
## Compact format (one record per line)
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
@file ./images/001.jpg <<< approved; scene=beach
|
|
48
|
+
@file ./images/002.jpg <<< rejected; too dark
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
No `---` separator needed between compact records. `@id` can go on the line above:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
@id item-001
|
|
55
|
+
@file ./file.txt <<< good
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Do not leave a blank line before those headers — in 0.3.0 that makes the parser
|
|
59
|
+
discard the record's own `@id`, `@by`, and `@tag` and inherit the previous
|
|
60
|
+
record's instead ([#15](https://github.com/dandriscoll/markback/issues/15)).
|
|
61
|
+
Pack compact records together, or separate them with `---`.
|
|
62
|
+
|
|
63
|
+
## Multi-record file
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
@id first
|
|
67
|
+
|
|
68
|
+
First content.
|
|
69
|
+
<<< positive
|
|
70
|
+
---
|
|
71
|
+
@id second
|
|
72
|
+
|
|
73
|
+
Second content.
|
|
74
|
+
<<< negative; needs work
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Multi-segment section
|
|
78
|
+
|
|
79
|
+
Several comments on one source, without repeating the headers. Write successive
|
|
80
|
+
content + `<<<` pairs with no `---` between them; the first segment's `@file`,
|
|
81
|
+
`@by`, `@tag`, and `@input` carry to the rest.
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
@file ./essay.txt
|
|
85
|
+
@by dan
|
|
86
|
+
|
|
87
|
+
the lazy fox
|
|
88
|
+
<<< awkward
|
|
89
|
+
|
|
90
|
+
weak ending
|
|
91
|
+
<<< needs punch
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
That is two records, both on `./essay.txt`. A `---` ends the section. `@id`,
|
|
95
|
+
`@reply-to`, and `@action` are per-record and never inherited.
|
|
96
|
+
|
|
97
|
+
## File-level headers (% prefix)
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
%markback 2
|
|
101
|
+
%scope issue-A issue-B
|
|
102
|
+
%covers ./gen/batch3/*.txt
|
|
103
|
+
|
|
104
|
+
@file ./gen/batch3/file2.txt <<< issue-B; tone is off
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- `%markback 2` — version declaration
|
|
108
|
+
- `%scope` — what issues are being checked (sweep pattern)
|
|
109
|
+
- `%covers` — glob of all files reviewed (absence = clean for scope)
|
|
110
|
+
|
|
111
|
+
All `%` lines go at the very top, before any record.
|
|
112
|
+
|
|
113
|
+
## Tags
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
@id item-001
|
|
117
|
+
@tag training positive-examples batch-2024-03
|
|
118
|
+
@file ./data/example.txt
|
|
119
|
+
<<< approved
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Space-separated. Multiple `@tag` lines merge.
|
|
123
|
+
|
|
124
|
+
## Threading and lifecycle
|
|
125
|
+
|
|
126
|
+
`@reply-to` carries the `@id` of the record being replied to:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
@id c1
|
|
130
|
+
@file ./login.py:42 <<< this branch never fires
|
|
131
|
+
---
|
|
132
|
+
@id c2
|
|
133
|
+
@reply-to c1
|
|
134
|
+
@file ./login.py:42 <<< it does — covered by test_login_edge()
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`@action <verb> <timestamp> [actor]` records a lifecycle event. Repeatable and
|
|
138
|
+
order-preserving; the well-known verbs are `created`, `viewed`, `resolved`,
|
|
139
|
+
`reopened`.
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
@action created 2026-06-17T10:00:00Z dan@example.com
|
|
143
|
+
@action resolved 2026-06-18T14:30:00Z Reviewer Two
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
A record is resolved when its most recent `resolved`/`reopened` action is
|
|
147
|
+
`resolved`.
|
|
148
|
+
|
|
149
|
+
## Feedback format
|
|
150
|
+
|
|
151
|
+
Feedback is freeform text. Optional structured convention:
|
|
152
|
+
|
|
153
|
+
| Pattern | Meaning |
|
|
154
|
+
|---------|---------|
|
|
155
|
+
| `<<< positive` | label |
|
|
156
|
+
| `<<< negative; too vague` | label + comment |
|
|
157
|
+
| `<<< good; quality=high` | label + attribute |
|
|
158
|
+
| `<<< quality=high; confidence=0.9` | attributes only |
|
|
159
|
+
| `<<< json:{"key":"value"}` | JSON mode |
|
|
160
|
+
|
|
161
|
+
Segments are separated by `; ` (semicolon + space). Segments with `=` are key-value attributes; without are labels or comments.
|
|
162
|
+
|
|
163
|
+
## Multi-line feedback
|
|
164
|
+
|
|
165
|
+
When the text right after `<<< ` is exactly `"""`, the feedback runs until a
|
|
166
|
+
line whose only content is `"""`:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
@id c1
|
|
170
|
+
@file ./login.py:42
|
|
171
|
+
<<< """
|
|
172
|
+
This branch looks dead, but I want to double-check before
|
|
173
|
+
suggesting removal.
|
|
174
|
+
"""
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Use it only when the feedback genuinely contains a newline.
|
|
178
|
+
|
|
179
|
+
## Sidecar files
|
|
180
|
+
|
|
181
|
+
Content in `name.ext`, annotation in `name.ext.mb`:
|
|
182
|
+
|
|
183
|
+
**report.pdf** — the content
|
|
184
|
+
**report.pdf.mb:**
|
|
185
|
+
```
|
|
186
|
+
@id report-001
|
|
187
|
+
<<< good; grade=B+
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Line/character ranges
|
|
191
|
+
|
|
192
|
+
`@file` and `@input` support position references:
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
@file ./code.py:42 ← line 42
|
|
196
|
+
@file ./code.py:42-50 ← lines 42–50
|
|
197
|
+
@file ./code.py:10:5-15:20 ← line 10 col 5 to line 15 col 20
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## V1 backward compatibility
|
|
201
|
+
|
|
202
|
+
V1 headers (`@uri`, `@source`, `@prior`) are automatically mapped to V2 (`@id`, `@file`, `@input`) with a W010 warning.
|
|
203
|
+
|
|
204
|
+
## Quick checklist
|
|
205
|
+
|
|
206
|
+
- [ ] every record has exactly one `<<<` line
|
|
207
|
+
- [ ] feedback is a single line, or fenced with `"""`
|
|
208
|
+
- [ ] blank line before inline content
|
|
209
|
+
- [ ] `---` between full records, no blank line before it; not needed between compact records
|
|
210
|
+
- [ ] file ends with a newline
|
|
211
|
+
|
|
212
|
+
Validate with `mb --lint file.mb`. Add `--no-canonical-check` if the file has
|
|
213
|
+
`%` headers — W008 always fires on those.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markback
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Markback — comments for anything, in plain text, in git.
|
|
5
5
|
Project-URL: Homepage, https://markback.org
|
|
6
6
|
Project-URL: Repository, https://github.com/dandriscoll/markback
|
|
@@ -49,7 +49,8 @@ work. CLI, Python, Node, a browser editor, and a VS Code extension.
|
|
|
49
49
|
- **Try it without installing** — [markback.org/try-it](https://markback.org/try-it)
|
|
50
50
|
- **Install the CLI** — `pip install markback` or `npm install markbackjs`
|
|
51
51
|
- **Read the format** — [Specification](https://markback.org/format/spec)
|
|
52
|
-
- **Comment from your editor** — [VS Code extension
|
|
52
|
+
- **Comment from your editor** — [VS Code extension](https://marketplace.visualstudio.com/items?itemName=dandriscoll.markback-vscode)
|
|
53
|
+
- **Point your coding agent at it** — [`skills/markback`](skills/markback/SKILL.md)
|
|
53
54
|
|
|
54
55
|
## Installation
|
|
55
56
|
|
|
@@ -57,7 +58,7 @@ work. CLI, Python, Node, a browser editor, and a VS Code extension.
|
|
|
57
58
|
pip install -e .
|
|
58
59
|
```
|
|
59
60
|
|
|
60
|
-
## Quick
|
|
61
|
+
## Quick start
|
|
61
62
|
|
|
62
63
|
### Parse a Markback file
|
|
63
64
|
|
|
@@ -106,7 +107,7 @@ if result.has_errors:
|
|
|
106
107
|
print(d)
|
|
107
108
|
```
|
|
108
109
|
|
|
109
|
-
## CLI
|
|
110
|
+
## CLI usage
|
|
110
111
|
|
|
111
112
|
The CLI is available via `markback` or `mb` (shorthand).
|
|
112
113
|
|
|
@@ -178,18 +179,22 @@ mb --upgrade *.mb # preview
|
|
|
178
179
|
mb --upgrade --apply --in-place *.mb # apply
|
|
179
180
|
```
|
|
180
181
|
|
|
181
|
-
## File
|
|
182
|
+
## File format
|
|
182
183
|
|
|
183
|
-
###
|
|
184
|
+
### Headers
|
|
184
185
|
|
|
185
186
|
| Header | Purpose |
|
|
186
187
|
|--------|---------|
|
|
187
188
|
| `@id` | Record identifier (plain string) |
|
|
189
|
+
| `@reply-to` | The `@id` this record replies to |
|
|
188
190
|
| `@by` | Who provided feedback |
|
|
191
|
+
| `@action` | `<verb> <timestamp> [actor]` lifecycle event; repeatable |
|
|
189
192
|
| `@tag` | Space-separated tags |
|
|
190
193
|
| `@input` | What produced the content (e.g., a prompt) |
|
|
191
194
|
| `@file` | Path to the content being annotated |
|
|
192
195
|
|
|
196
|
+
Listed in canonical order. All are optional.
|
|
197
|
+
|
|
193
198
|
### File-level headers (% prefix)
|
|
194
199
|
|
|
195
200
|
```
|
|
@@ -216,6 +221,49 @@ mb --upgrade --apply --in-place *.mb # apply
|
|
|
216
221
|
@file ./images/002.jpg <<< rejected; too dark
|
|
217
222
|
```
|
|
218
223
|
|
|
224
|
+
### Multi-segment section
|
|
225
|
+
|
|
226
|
+
Several comments on one source, without repeating the headers — write
|
|
227
|
+
successive content + `<<<` pairs with no `---` between them:
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
@file ./essay.txt
|
|
231
|
+
|
|
232
|
+
the lazy fox
|
|
233
|
+
<<< awkward
|
|
234
|
+
|
|
235
|
+
weak ending
|
|
236
|
+
<<< needs punch
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Two records, both on `./essay.txt`. A `---` ends the section.
|
|
240
|
+
|
|
241
|
+
### Threading and lifecycle
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
@id c1
|
|
245
|
+
@action created 2026-06-17T10:00:00Z dan@example.com
|
|
246
|
+
@file ./login.py:42 <<< this branch never fires
|
|
247
|
+
---
|
|
248
|
+
@id c2
|
|
249
|
+
@reply-to c1
|
|
250
|
+
@file ./login.py:42 <<< it does — covered by test_login_edge()
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Multi-line feedback
|
|
254
|
+
|
|
255
|
+
When the text right after `<<< ` is exactly `"""`, feedback runs until a line
|
|
256
|
+
whose only content is `"""`:
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
@id c1
|
|
260
|
+
@file ./login.py:42
|
|
261
|
+
<<< """
|
|
262
|
+
This branch looks dead, but I want to double-check before
|
|
263
|
+
suggesting removal.
|
|
264
|
+
"""
|
|
265
|
+
```
|
|
266
|
+
|
|
219
267
|
### Sidecar files
|
|
220
268
|
|
|
221
269
|
Content in `report.pdf`, annotation in `report.pdf.mb`:
|
|
@@ -240,7 +288,7 @@ Track issues across batches with meaningful absence:
|
|
|
240
288
|
|
|
241
289
|
Files matching `%covers` without annotations are implicitly clean for all `%scope` items.
|
|
242
290
|
|
|
243
|
-
## V1
|
|
291
|
+
## V1 backward compatibility
|
|
244
292
|
|
|
245
293
|
V1 headers (`@uri`, `@source`, `@prior`) are automatically mapped to V2 equivalents with a W010 warning. The V2 parser reads V1 files transparently.
|
|
246
294
|
|
|
@@ -16,7 +16,8 @@ work. CLI, Python, Node, a browser editor, and a VS Code extension.
|
|
|
16
16
|
- **Try it without installing** — [markback.org/try-it](https://markback.org/try-it)
|
|
17
17
|
- **Install the CLI** — `pip install markback` or `npm install markbackjs`
|
|
18
18
|
- **Read the format** — [Specification](https://markback.org/format/spec)
|
|
19
|
-
- **Comment from your editor** — [VS Code extension
|
|
19
|
+
- **Comment from your editor** — [VS Code extension](https://marketplace.visualstudio.com/items?itemName=dandriscoll.markback-vscode)
|
|
20
|
+
- **Point your coding agent at it** — [`skills/markback`](skills/markback/SKILL.md)
|
|
20
21
|
|
|
21
22
|
## Installation
|
|
22
23
|
|
|
@@ -24,7 +25,7 @@ work. CLI, Python, Node, a browser editor, and a VS Code extension.
|
|
|
24
25
|
pip install -e .
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
## Quick
|
|
28
|
+
## Quick start
|
|
28
29
|
|
|
29
30
|
### Parse a Markback file
|
|
30
31
|
|
|
@@ -73,7 +74,7 @@ if result.has_errors:
|
|
|
73
74
|
print(d)
|
|
74
75
|
```
|
|
75
76
|
|
|
76
|
-
## CLI
|
|
77
|
+
## CLI usage
|
|
77
78
|
|
|
78
79
|
The CLI is available via `markback` or `mb` (shorthand).
|
|
79
80
|
|
|
@@ -145,18 +146,22 @@ mb --upgrade *.mb # preview
|
|
|
145
146
|
mb --upgrade --apply --in-place *.mb # apply
|
|
146
147
|
```
|
|
147
148
|
|
|
148
|
-
## File
|
|
149
|
+
## File format
|
|
149
150
|
|
|
150
|
-
###
|
|
151
|
+
### Headers
|
|
151
152
|
|
|
152
153
|
| Header | Purpose |
|
|
153
154
|
|--------|---------|
|
|
154
155
|
| `@id` | Record identifier (plain string) |
|
|
156
|
+
| `@reply-to` | The `@id` this record replies to |
|
|
155
157
|
| `@by` | Who provided feedback |
|
|
158
|
+
| `@action` | `<verb> <timestamp> [actor]` lifecycle event; repeatable |
|
|
156
159
|
| `@tag` | Space-separated tags |
|
|
157
160
|
| `@input` | What produced the content (e.g., a prompt) |
|
|
158
161
|
| `@file` | Path to the content being annotated |
|
|
159
162
|
|
|
163
|
+
Listed in canonical order. All are optional.
|
|
164
|
+
|
|
160
165
|
### File-level headers (% prefix)
|
|
161
166
|
|
|
162
167
|
```
|
|
@@ -183,6 +188,49 @@ mb --upgrade --apply --in-place *.mb # apply
|
|
|
183
188
|
@file ./images/002.jpg <<< rejected; too dark
|
|
184
189
|
```
|
|
185
190
|
|
|
191
|
+
### Multi-segment section
|
|
192
|
+
|
|
193
|
+
Several comments on one source, without repeating the headers — write
|
|
194
|
+
successive content + `<<<` pairs with no `---` between them:
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
@file ./essay.txt
|
|
198
|
+
|
|
199
|
+
the lazy fox
|
|
200
|
+
<<< awkward
|
|
201
|
+
|
|
202
|
+
weak ending
|
|
203
|
+
<<< needs punch
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Two records, both on `./essay.txt`. A `---` ends the section.
|
|
207
|
+
|
|
208
|
+
### Threading and lifecycle
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
@id c1
|
|
212
|
+
@action created 2026-06-17T10:00:00Z dan@example.com
|
|
213
|
+
@file ./login.py:42 <<< this branch never fires
|
|
214
|
+
---
|
|
215
|
+
@id c2
|
|
216
|
+
@reply-to c1
|
|
217
|
+
@file ./login.py:42 <<< it does — covered by test_login_edge()
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Multi-line feedback
|
|
221
|
+
|
|
222
|
+
When the text right after `<<< ` is exactly `"""`, feedback runs until a line
|
|
223
|
+
whose only content is `"""`:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
@id c1
|
|
227
|
+
@file ./login.py:42
|
|
228
|
+
<<< """
|
|
229
|
+
This branch looks dead, but I want to double-check before
|
|
230
|
+
suggesting removal.
|
|
231
|
+
"""
|
|
232
|
+
```
|
|
233
|
+
|
|
186
234
|
### Sidecar files
|
|
187
235
|
|
|
188
236
|
Content in `report.pdf`, annotation in `report.pdf.mb`:
|
|
@@ -207,7 +255,7 @@ Track issues across batches with meaningful absence:
|
|
|
207
255
|
|
|
208
256
|
Files matching `%covers` without annotations are implicitly clean for all `%scope` items.
|
|
209
257
|
|
|
210
|
-
## V1
|
|
258
|
+
## V1 backward compatibility
|
|
211
259
|
|
|
212
260
|
V1 headers (`@uri`, `@source`, `@prior`) are automatically mapped to V2 equivalents with a W010 warning. The V2 parser reads V1 files transparently.
|
|
213
261
|
|
|
@@ -1253,18 +1253,18 @@ full-record = [headers] [content-block] feedback-line
|
|
|
1253
1253
|
headers = 1*header-line
|
|
1254
1254
|
header-line = "@" keyword SP value LF
|
|
1255
1255
|
keyword = 1*LOWER
|
|
1256
|
-
value = *VCHAR
|
|
1256
|
+
value = *(VCHAR / SP) ; freeform; may contain interior spaces; ends at LF
|
|
1257
1257
|
|
|
1258
1258
|
content-block = blank-line content
|
|
1259
1259
|
content = 1*content-line
|
|
1260
|
-
content-line = *VCHAR LF
|
|
1260
|
+
content-line = *(VCHAR / SP) LF ; any line not starting with <<<
|
|
1261
1261
|
|
|
1262
1262
|
blank-line = LF
|
|
1263
1263
|
|
|
1264
1264
|
; === Feedback ===
|
|
1265
1265
|
|
|
1266
1266
|
feedback-line = "<<<" SP feedback-content LF
|
|
1267
|
-
feedback-content = *VCHAR
|
|
1267
|
+
feedback-content = *(VCHAR / SP) ; freeform; no LF allowed
|
|
1268
1268
|
|
|
1269
1269
|
; === Compact Record ===
|
|
1270
1270
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""DO NOT EDIT — generated from shared/headers.json by scripts/gen_headers.py."""
|
|
2
|
+
|
|
3
|
+
# Header keywords the parser recognizes (others emit W002).
|
|
4
|
+
KNOWN_HEADERS = set(["id", "by", "file", "input", "tag", "reply-to", "action"])
|
|
5
|
+
|
|
6
|
+
# The order headers are emitted in canonical output (SPEC §7.1).
|
|
7
|
+
CANONICAL_ORDER = ["id", "reply-to", "by", "action", "tag", "input", "file"]
|
|
8
|
+
|
|
9
|
+
# Headers inherited by continuation segments of a multi-segment section
|
|
10
|
+
# (@id, @reply-to, and @action are per-record and never inherited).
|
|
11
|
+
SECTION_INHERITED = set(["file", "by", "tag", "input"])
|
|
12
|
+
|
|
13
|
+
# V1 header names mapped to their V2 equivalents (each emits W010).
|
|
14
|
+
V1_HEADER_MAP = {"uri": "id", "source": "file", "prior": "input"}
|
|
@@ -248,11 +248,22 @@ def lint_canonical_format(
|
|
|
248
248
|
records: list[Record],
|
|
249
249
|
original_text: str,
|
|
250
250
|
file: Optional[Path],
|
|
251
|
+
scope: Optional[list[str]] = None,
|
|
252
|
+
covers: Optional[str] = None,
|
|
253
|
+
version: Optional[int] = None,
|
|
251
254
|
) -> list[Diagnostic]:
|
|
252
255
|
"""Check if file is in canonical format."""
|
|
253
256
|
diagnostics: list[Diagnostic] = []
|
|
254
257
|
|
|
255
|
-
|
|
258
|
+
# The comparison render must carry the file's own %-headers, otherwise any
|
|
259
|
+
# canonical file with %markback/%scope/%covers mismatches and reports a
|
|
260
|
+
# false W008. version_header is on iff the file declared a %markback line.
|
|
261
|
+
canonical = write_string(
|
|
262
|
+
records,
|
|
263
|
+
scope=scope,
|
|
264
|
+
covers=covers,
|
|
265
|
+
version_header=version is not None,
|
|
266
|
+
)
|
|
256
267
|
original_normalized = original_text.replace('\r\n', '\n').replace('\r', '\n')
|
|
257
268
|
|
|
258
269
|
if original_normalized != canonical:
|
|
@@ -307,6 +318,9 @@ def lint_string(
|
|
|
307
318
|
result.records,
|
|
308
319
|
text,
|
|
309
320
|
source_file,
|
|
321
|
+
scope=result.scope,
|
|
322
|
+
covers=result.covers,
|
|
323
|
+
version=result.version,
|
|
310
324
|
))
|
|
311
325
|
|
|
312
326
|
return result
|
|
@@ -4,6 +4,7 @@ import re
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
7
|
+
from ._headers import KNOWN_HEADERS, SECTION_INHERITED, V1_HEADER_MAP
|
|
7
8
|
from .types import (
|
|
8
9
|
Action,
|
|
9
10
|
Diagnostic,
|
|
@@ -15,13 +16,6 @@ from .types import (
|
|
|
15
16
|
WarningCode,
|
|
16
17
|
)
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
# V2 known header keywords
|
|
20
|
-
KNOWN_HEADERS = {"id", "by", "file", "input", "tag", "reply-to", "action"}
|
|
21
|
-
|
|
22
|
-
# V1 header mapping for backward compatibility
|
|
23
|
-
V1_HEADER_MAP = {"uri": "id", "source": "file", "prior": "input"}
|
|
24
|
-
|
|
25
19
|
# Patterns
|
|
26
20
|
HEADER_PATTERN = re.compile(r"^@([a-z][a-z-]*)\s+(.+)$")
|
|
27
21
|
FEEDBACK_DELIMITER = "<<<"
|
|
@@ -192,21 +186,22 @@ def parse_string(
|
|
|
192
186
|
had_blank_line: bool = False
|
|
193
187
|
past_file_headers: bool = False
|
|
194
188
|
|
|
195
|
-
SECTION_INHERITED = ("file", "by", "tag", "input")
|
|
196
|
-
|
|
197
189
|
def finalize_record(feedback: str, end_line: int):
|
|
198
190
|
"""Create a record from current state, then reset for next segment."""
|
|
199
191
|
nonlocal current_headers, current_actions, current_content_lines, current_start_line
|
|
200
192
|
nonlocal pending_id, in_content, had_blank_line, section_headers
|
|
201
193
|
|
|
194
|
+
# Section-inherited headers (@file, @by, @tag, @input) fall back to the
|
|
195
|
+
# section value only when this segment did not declare its own. @id and
|
|
196
|
+
# @reply-to are per-record and never inherited.
|
|
202
197
|
record_id = current_headers.get("id") or pending_id
|
|
203
|
-
by = current_headers.get("by")
|
|
198
|
+
by = current_headers.get("by") or section_headers.get("by")
|
|
204
199
|
reply_to = current_headers.get("reply-to")
|
|
205
|
-
file_str = current_headers.get("file")
|
|
200
|
+
file_str = current_headers.get("file") or section_headers.get("file")
|
|
206
201
|
file_ref = FileRef(file_str) if file_str else None
|
|
207
|
-
input_str = current_headers.get("input")
|
|
202
|
+
input_str = current_headers.get("input") or section_headers.get("input")
|
|
208
203
|
input_ref = FileRef(input_str) if input_str else None
|
|
209
|
-
tag_str = current_headers.get("tag")
|
|
204
|
+
tag_str = current_headers.get("tag") or section_headers.get("tag")
|
|
210
205
|
tags = tag_str.split() if tag_str else []
|
|
211
206
|
|
|
212
207
|
content = None
|
|
@@ -243,8 +238,12 @@ def parse_string(
|
|
|
243
238
|
if k in SECTION_INHERITED
|
|
244
239
|
}
|
|
245
240
|
|
|
246
|
-
# Reset state for the next segment
|
|
247
|
-
|
|
241
|
+
# Reset state for the next segment. current_headers holds only what the
|
|
242
|
+
# NEXT segment declares itself; inherited values live in section_headers
|
|
243
|
+
# and are merged in at finalize. Starting empty (rather than a copy of
|
|
244
|
+
# section_headers) is what lets a blank line between records read as a
|
|
245
|
+
# record separator, and a re-declared @tag override rather than merge.
|
|
246
|
+
current_headers = {}
|
|
248
247
|
current_actions = []
|
|
249
248
|
current_content_lines = []
|
|
250
249
|
current_start_line = end_line + 1
|
|
@@ -312,7 +311,7 @@ def parse_string(
|
|
|
312
311
|
if line_type == LineType.SEPARATOR:
|
|
313
312
|
# `---` ends the section: clear inheritable headers entirely.
|
|
314
313
|
# Error only if user added headers/content since last finalize.
|
|
315
|
-
if current_content_lines or current_headers
|
|
314
|
+
if current_content_lines or current_headers:
|
|
316
315
|
add_diagnostic(
|
|
317
316
|
Severity.ERROR,
|
|
318
317
|
ErrorCode.E001,
|
|
@@ -330,10 +329,15 @@ def parse_string(
|
|
|
330
329
|
continue
|
|
331
330
|
|
|
332
331
|
if line_type == LineType.BLANK:
|
|
333
|
-
if
|
|
334
|
-
had_blank_line = True
|
|
335
|
-
elif in_content:
|
|
332
|
+
if in_content:
|
|
336
333
|
current_content_lines.append("")
|
|
334
|
+
elif current_headers:
|
|
335
|
+
# A blank AFTER this segment's own headers is the
|
|
336
|
+
# header/content separator (§3.2.1). A blank when the segment
|
|
337
|
+
# has declared no headers of its own is a between-records
|
|
338
|
+
# separator and is ignored (§3.5) — so the next @-lines are
|
|
339
|
+
# read as the following record's headers, not as content.
|
|
340
|
+
had_blank_line = True
|
|
337
341
|
continue
|
|
338
342
|
|
|
339
343
|
if line_type == LineType.COMPACT_RECORD:
|
|
@@ -383,11 +387,11 @@ def parse_string(
|
|
|
383
387
|
)
|
|
384
388
|
|
|
385
389
|
record_id = pending_id or current_headers.get("id")
|
|
386
|
-
by = current_headers.get("by")
|
|
390
|
+
by = current_headers.get("by") or section_headers.get("by")
|
|
387
391
|
reply_to = current_headers.get("reply-to")
|
|
388
|
-
input_str = current_headers.get("input")
|
|
392
|
+
input_str = current_headers.get("input") or section_headers.get("input")
|
|
389
393
|
input_ref = FileRef(input_str) if input_str else None
|
|
390
|
-
tag_str = current_headers.get("tag")
|
|
394
|
+
tag_str = current_headers.get("tag") or section_headers.get("tag")
|
|
391
395
|
tags = tag_str.split() if tag_str else []
|
|
392
396
|
|
|
393
397
|
record = Record(
|
|
@@ -415,7 +419,7 @@ def parse_string(
|
|
|
415
419
|
}
|
|
416
420
|
# The compact line itself supplied @file:
|
|
417
421
|
section_headers["file"] = str(file_ref)
|
|
418
|
-
current_headers =
|
|
422
|
+
current_headers = {}
|
|
419
423
|
current_actions = []
|
|
420
424
|
current_content_lines = []
|
|
421
425
|
current_start_line = end_line + 1
|
|
@@ -539,7 +543,7 @@ def parse_string(
|
|
|
539
543
|
continue
|
|
540
544
|
|
|
541
545
|
# Check for unterminated record at end of file
|
|
542
|
-
if current_content_lines or current_headers
|
|
546
|
+
if current_content_lines or current_headers:
|
|
543
547
|
add_diagnostic(
|
|
544
548
|
Severity.ERROR,
|
|
545
549
|
ErrorCode.E001,
|