paperlint 14.0.0 → 14.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.
@@ -18,6 +18,9 @@ type SchemaNode = ({
18
18
  kind: "string";
19
19
  required: boolean;
20
20
  pattern?: string;
21
+ } | {
22
+ kind: "text";
23
+ required: boolean;
21
24
  } | {
22
25
  kind: "integer";
23
26
  required: boolean;
@@ -71,10 +74,67 @@ interface Unit {
71
74
  readonly part: number;
72
75
  readonly of: number;
73
76
  }
77
+ interface TextField {
78
+ readonly label?: string;
79
+ readonly text: string;
80
+ readonly style?: string;
81
+ }
82
+ interface CodeLine {
83
+ readonly n: number;
84
+ readonly text: string;
85
+ readonly mark: boolean;
86
+ }
87
+ /** The content blocks (#380), each optional on any kind. */
88
+ interface Blocks {
89
+ readonly situation?: {
90
+ readonly summary: string;
91
+ readonly question: string;
92
+ };
93
+ readonly excerpt?: {
94
+ readonly file?: string;
95
+ readonly url?: string;
96
+ readonly lines: readonly CodeLine[];
97
+ readonly caption?: string;
98
+ readonly mark_label?: string;
99
+ };
100
+ readonly references?: {
101
+ readonly summary: string;
102
+ readonly entries: readonly {
103
+ readonly label?: string;
104
+ readonly title: string;
105
+ readonly fields: readonly TextField[];
106
+ readonly url?: string;
107
+ readonly url_label?: string;
108
+ }[];
109
+ };
110
+ readonly more?: {
111
+ readonly summary: string;
112
+ readonly items: readonly string[];
113
+ readonly rest?: string;
114
+ };
115
+ readonly table?: {
116
+ readonly summary: string;
117
+ readonly note?: string;
118
+ readonly rows: readonly {
119
+ readonly label: string;
120
+ readonly value: string;
121
+ readonly plain?: string;
122
+ readonly mark?: {
123
+ readonly tone: string;
124
+ readonly text: string;
125
+ };
126
+ }[];
127
+ };
128
+ readonly repeat?: {
129
+ readonly group: string;
130
+ readonly index: number;
131
+ readonly of: number;
132
+ };
133
+ }
74
134
  /** What a card shows, on an item and on a practice item alike. */
75
135
  type Card = {
76
136
  readonly [F in (typeof PAGE_FIELDS)[number]]?: string;
77
- } & {
137
+ } & Blocks & {
78
138
  readonly body: string;
79
139
  readonly rules?: readonly RuleRef[];
80
140
  readonly unit?: Unit;
@@ -104,6 +164,7 @@ interface Config {
104
164
  readonly questions: readonly Question[];
105
165
  readonly practice: readonly Practice[];
106
166
  readonly practice_round?: number;
167
+ readonly labels?: Readonly<Record<string, string>>;
107
168
  readonly items: readonly Item[];
108
169
  }
109
170
  interface KeyEntry {
@@ -124,6 +185,9 @@ declare function itemsOnPage(cfg: Config, keyPath: string): {
124
185
  declare function payloadOf(cfg: Config, page: ReturnType<typeof itemsOnPage>, described: Described): {
125
186
  title: string;
126
187
  intro: string;
188
+ labels: {
189
+ [x: string]: string;
190
+ };
127
191
  groups: {
128
192
  id: string;
129
193
  label: string;
@@ -177,6 +241,9 @@ export declare function prepareBuild(configPath: string, outDir: string): {
177
241
  payload: {
178
242
  title: string;
179
243
  intro: string;
244
+ labels: {
245
+ [x: string]: string;
246
+ };
180
247
  groups: {
181
248
  id: string;
182
249
  label: string;
@@ -0,0 +1,10 @@
1
+ /** A preset's result: the config in the generic shape, or why it cannot be made. */
2
+ export type Preset = {
3
+ readonly ok: true;
4
+ readonly value: unknown;
5
+ } | {
6
+ readonly ok: false;
7
+ readonly message: string;
8
+ };
9
+ /** The config with its preset applied; a config without `preset` comes back as it was. */
10
+ export declare function applyPreset(raw: unknown): Preset;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperlint",
3
- "version": "14.0.0",
3
+ "version": "14.1.0",
4
4
  "description": "A linter for scientific papers written in LaTeX: catches mistakes before you submit to a conference or journal.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -4,7 +4,7 @@ description: Build a phone labelling page published as a claude.ai Artifact with
4
4
  allowed-tools: [Read, Write, Edit, Grep, Glob, Bash, Artifact, ArtifactData]
5
5
  ---
6
6
 
7
- <!-- vigiles:sha256:6e828827dc6fcfc1 compiled from skills/build-label-page/SKILL.md.spec.ts -->
7
+ <!-- vigiles:sha256:64e09e4a829be441 compiled from skills/build-label-page/SKILL.md.spec.ts -->
8
8
 
9
9
  # build-label-page — a page the person labels on a phone, and the labels back in the repo
10
10
 
@@ -105,7 +105,11 @@ building. In one line each:
105
105
 
106
106
  It writes `label-page/page.html` and `label-page/key.json` (the only link from `G001` to the
107
107
  source id; it never goes on the page). `kind` (`approval`, `witness`, `labels`) is required,
108
- and each kind requires its card fields on every item and practice card. Written by hand, the page meets the requirements above.
108
+ and each kind requires its card fields on every item and practice card. Any card may also carry
109
+ the generic content blocks (a code excerpt with a marked line, references, a list, a table), every
110
+ word on them from `items.json`, with the question right under the excerpt. `"preset":
111
+ "linter-config-witness"` is a narrow preset for comparing linter configs; the blocks underneath
112
+ are generic. Written by hand, the page meets the requirements above.
109
113
  4. **Read it as the labeller, then look at it.** Read the page text once as a person who knows
110
114
  nothing about the source: if anything on it tells the answer, take it out. Read each question as
111
115
  someone who did not design the study: can they tell what each answer means from the question, its
@@ -116,7 +116,11 @@ building. In one line each:
116
116
 
117
117
  It writes \`label-page/page.html\` and \`label-page/key.json\` (the only link from \`G001\` to the
118
118
  source id; it never goes on the page). \`kind\` (\`approval\`, \`witness\`, \`labels\`) is required,
119
- and each kind requires its card fields on every item and practice card. Written by hand, the page meets the requirements above.
119
+ and each kind requires its card fields on every item and practice card. Any card may also carry
120
+ the generic content blocks (a code excerpt with a marked line, references, a list, a table), every
121
+ word on them from \`items.json\`, with the question right under the excerpt. \`"preset":
122
+ "linter-config-witness"\` is a narrow preset for comparing linter configs; the blocks underneath
123
+ are generic. Written by hand, the page meets the requirements above.
120
124
  4. **Read it as the labeller, then look at it.** Read the page text once as a person who knows
121
125
  nothing about the source: if anything on it tells the answer, take it out. Read each question as
122
126
  someone who did not design the study: can they tell what each answer means from the question, its
@@ -21,7 +21,11 @@ with the core's payload. Another page shape would be another template on the sam
21
21
  Rules 2 to 5, 7 and the page half of 8 of [`checklist.md`](checklist.md): a filter instead of
22
22
  scrolling, complete-item counting, neutral ids in a seeded order, the leak guard (an item field
23
23
  outside the allow-list stops the build), save on click with a time, the store-unavailable banner,
24
- the "How to answer" screen, the practice before the real items, one question per screen. Its look:
24
+ the "How to answer" screen (folded into a toggle once the practice is done), the practice before
25
+ the real items, one question per screen, each card's place (`labels.place`, by default
26
+ "G007 · 7 of 20"), the card just
27
+ answered kept under "Next unanswered" until the next answer (so a mis-tap can be changed), and a
28
+ closing line once every item is complete. Its look:
25
29
  one dark palette, one column, answer buttons as full-width rows with space between them, an optional
26
30
  icon per answer and never a coloured side stripe, `backticks` and the `code` field shown as code,
27
31
  code blocks that show leading and trailing whitespace (a space as ·, a tab as →) and each line's
@@ -76,10 +80,130 @@ length.
76
80
  full. Splitting the line is the study's job, before the page.
77
81
  - **`seed`**: fixes the order. Rebuilding with the same seed keeps every id, and a new item gets the
78
82
  next one; a different seed is refused once a key exists.
79
- - **`ask_why`** on an option: a prompt. That answer is incomplete, on the page and in the export,
80
- until a reason is saved. The reason saves as it is typed (after a pause, and when the page is
81
- left), so a labeller who closes the page from inside the box does not leave a drop without its
82
- reason; a store echo while typing keeps the box and its focus.
83
+ - **`ask_why`** on an option: a prompt. The page does not write that answer to the store until its
84
+ reason box holds text: the card says «Not saved yet», and the store keeps what it had. The reason
85
+ then saves with the answer as it is typed (after a pause, and when the page is left); a store echo
86
+ while typing keeps the box and its focus. An answer stored without its reason (by an older page,
87
+ or merged from chat) still counts as incomplete, on the page and in the export.
88
+
89
+ ## Content blocks
90
+
91
+ Any card may carry these blocks, on any `kind`, and each one is optional. The page knows only the shape of each block. Every label and sentence on it comes from `items.json`, so a code review, a prose check or a configuration study each write their own words. A practice card carries the same blocks as the real cards it rehearses. `scripts/fixtures/blocks-items.json` is a whole page of them: a code review, with no preset.
92
+
93
+ ```jsonc
94
+ {
95
+ "situation": {
96
+ "summary": "Reviewer A asked for a change at {line}; reviewer B approved it.",
97
+ "question": "Does «Every query is parameterised» cover {line}?",
98
+ },
99
+ "excerpt": {
100
+ "file": "app/report.py", // optional; with `url`, the name is a link
101
+ "url": "https://example.com/app/report.py#L12", // optional
102
+ "lines": [
103
+ { "n": 11, "text": "def monthly(month):", "mark": false },
104
+ {
105
+ "n": 12,
106
+ "text": " rows = db.raw(\"SELECT … m=\" + month)",
107
+ "mark": true,
108
+ },
109
+ ],
110
+ "caption": "Reviewer A wrote: concatenated SQL.", // optional, under the code
111
+ "mark_label": "◀ the line in question", // optional; default «◀ this line»
112
+ },
113
+ "references": {
114
+ "summary": "What the guideline says", // the folded block's toggle
115
+ "entries": [
116
+ {
117
+ "label": "Guideline", // optional row label
118
+ "title": "Security guide `SQL-1`",
119
+ "fields": [
120
+ { "text": "Build every query with parameters." },
121
+ { "label": "Why", "text": "Input becomes part of the query." }, // «Why: …»
122
+ {
123
+ "style": "note",
124
+ "text": "The examples below are the guide's own.",
125
+ },
126
+ {
127
+ "label": "Not like this",
128
+ "style": "code",
129
+ "text": "db.raw(\"… id=\" + id)",
130
+ },
131
+ ],
132
+ "url": "https://example.com/guide#sql-1", // optional
133
+ "url_label": "Security guide", // optional; the link text
134
+ },
135
+ ],
136
+ },
137
+ "more": {
138
+ "summary": "3 more lines like this",
139
+ "items": ["`app/export.py`", "`app/admin.py`"],
140
+ "rest": "and 1 more", // optional, after the items
141
+ },
142
+ "table": {
143
+ "summary": "What each reviewer wrote",
144
+ "note": "Reviewer A asked for the change; reviewer B approved.", // optional
145
+ "rows": [
146
+ {
147
+ "label": "A only · Comment",
148
+ "value": "use parameters", // shown as code
149
+ "plain": "asks for a parameterised query", // optional
150
+ "mark": { "tone": "yes", "text": "✓ about this line" }, // optional; tone yes or no
151
+ },
152
+ ],
153
+ },
154
+ "repeat": { "group": "guide-sql", "index": 1, "of": 2 }, // `group` never reaches the page
155
+ }
156
+ ```
157
+
158
+ - **Order on the card.** The card shows, in this order:
159
+ 1. its place (`labels.place`);
160
+ 2. the body, on a `labels.body` row when that label is set;
161
+ 3. the repeat sentence;
162
+ 4. the older fields (`code`, `unit`, `rules`, `readings`, `file`, `context`);
163
+ 5. the situation's summary;
164
+ 6. the excerpt;
165
+ 7. **the question and its answers**;
166
+ 8. `references`, `more` and `table`, each folded and closed.
167
+
168
+ The question therefore sits right under the code it names.
169
+
170
+ - **`situation`**: `{summary, question}`.
171
+ - `summary` is one sentence above the excerpt.
172
+ - `question` replaces the text of the page's first question on this card. Other questions keep their own text.
173
+ - In either text, `{line}` stands for the excerpt's first marked line. The page writes «line N: », then the line's code as code, without its indent. A code line holding a backtick reads as it is.
174
+ - `{line}` on a card without an `excerpt` is refused by path.
175
+ - **`excerpt`**: at least one line is marked (`mark: true`). The marked line gets a `▶` in the gutter and `mark_label` after its code, and keeps its background, so it reads as marked even without its colour. A line's `text` may be empty, for a blank line.
176
+ - **`references.entries[].fields`**:
177
+ - A field without `style` is a paragraph («label: text» when it has a label). A long one shows three lines and a «more».
178
+ - `style: "code"` is a code block on a `label` row.
179
+ - `style: "note"` is a quiet line.
180
+ - **`table.rows`**: shown in the order given. Each row shows its `label`, then `value` as code, `plain` under it, and `mark` last.
181
+ - **`repeat`**: shows the sentence `labels.repeat` when `of` is more than 1. `index` must lie between 1 and `of`.
182
+ - **`labels`** sits at page level, and every key in it is optional:
183
+
184
+ | key | what it is | default |
185
+ | ------------------ | --------------------------------------------------------------- | ------------------------------ |
186
+ | `place` | the card's place | `{id} · {n} of {total}` |
187
+ | `body` | a row label for the body | none |
188
+ | `line` | the label of the `unit` row | `Line` |
189
+ | `line_part` | the `unit` row's label when the line holds several requirements | `{line} · part {part} of {of}` |
190
+ | `repeat` | the repeat sentence | `Card {index} of {of}` |
191
+ | `practice_example` | text before each answer's example on a practice button | empty |
192
+
193
+ - The leak guard reads every string these blocks render, one string at a time.
194
+
195
+ ### Presets: named words for a narrow use
196
+
197
+ A preset is opt-in. An items file names it with `"preset": "<name>"`. The preset maps its own compact card fields onto the blocks above and supplies `labels`. The file's own `labels` override the preset's. The page and the core know nothing of any preset: each one lives in `scripts/label-page-presets.ts`.
198
+
199
+ - **`linter-config-witness`** is narrow: two linter configs of one requirement disagree on a line of code. The blocks underneath are generic.
200
+ - **Input fields on a card:**
201
+ - `example: {file, url?, lines, message, caught_by}`, which becomes the excerpt, with the caption «The linter says: …».
202
+ - `rules: [{linter, id, explanation}]`, whose explanation has `source` (`ruff rule`, `eslint docs`, `plugin docs`, `config message` or `none`), plus `what`, `why`, `example_bad`, `example_good` and `url`. These become the references, folded under «What the linter rule does (official docs)».
203
+ - `more_like_this: {count, files}`, which becomes the list «N more file(s) with the same situation».
204
+ - `details: {a, b}`: rows of `{key, raw, plain, matches_example}`. They become the table «Settings behind this card», with «A only / B only / A and B» rows.
205
+ - **Its labels**: «Card {n} of {total}», «Requirement», «Card {index} of {of} for this requirement», and «Example of this answer, about other code: ».
206
+ - `scripts/fixtures/linter-config-witness-items.json` is a whole page of it.
83
207
 
84
208
  ## The approval page
85
209
 
@@ -0,0 +1,214 @@
1
+ {
2
+ "study": "demo-review",
3
+ "kind": "labels",
4
+ "title": "Review comments",
5
+ "intro": "Each card shows one guideline, one changed line and one question about that line.",
6
+ "seed": 5,
7
+ "groups": { "all": "Review comments" },
8
+ "labels": {
9
+ "place": "Comment {n} of {total}",
10
+ "body": "Guideline",
11
+ "repeat": "Comment {index} of {of} on this guideline",
12
+ "practice_example": "Another case: "
13
+ },
14
+ "questions": [
15
+ {
16
+ "id": "applies",
17
+ "text": "Does the guideline cover this line?",
18
+ "short": "covered?",
19
+ "options": [
20
+ {
21
+ "value": "yes",
22
+ "label": "Yes",
23
+ "icon": "✅",
24
+ "example": "«No string-built SQL» and an f-string query"
25
+ },
26
+ {
27
+ "value": "no",
28
+ "label": "No",
29
+ "icon": "🚫",
30
+ "example": "«No string-built SQL» and a log message"
31
+ },
32
+ {
33
+ "value": "unclear",
34
+ "label": "Unclear",
35
+ "icon": "↔️",
36
+ "example": "«Short functions» and a 40-line function",
37
+ "ask_why": "What makes it unclear?"
38
+ }
39
+ ]
40
+ }
41
+ ],
42
+ "practice": [
43
+ {
44
+ "body": "Every query is parameterised",
45
+ "situation": {
46
+ "summary": "One reviewer asked for a change at {line}; the other approved it.",
47
+ "question": "Does «Every query is parameterised» cover {line}?"
48
+ },
49
+ "excerpt": {
50
+ "file": "app/orders.py",
51
+ "lines": [
52
+ { "n": 41, "text": "def total(order_id):", "mark": false },
53
+ {
54
+ "n": 42,
55
+ "text": " sql = f\"SELECT * FROM o WHERE id={order_id}\"",
56
+ "mark": true
57
+ }
58
+ ],
59
+ "caption": "Reviewer A wrote: build this query with parameters."
60
+ },
61
+ "expected": { "applies": "yes" },
62
+ "why": "The value goes into the query text."
63
+ },
64
+ {
65
+ "body": "Every query is parameterised",
66
+ "situation": {
67
+ "summary": "One reviewer flagged {line}; the other did not.",
68
+ "question": "Does «Every query is parameterised» cover {line}?"
69
+ },
70
+ "excerpt": {
71
+ "file": "app/log.py",
72
+ "lines": [
73
+ {
74
+ "n": 7,
75
+ "text": "log.info(f\"order {order_id} paid\")",
76
+ "mark": true
77
+ }
78
+ ]
79
+ },
80
+ "expected": { "applies": "no" },
81
+ "why": "A log line is not a query."
82
+ },
83
+ {
84
+ "body": "Functions stay short",
85
+ "situation": {
86
+ "summary": "One reviewer asked to split the function; the other did not.",
87
+ "question": "Does «Functions stay short» cover this function?"
88
+ },
89
+ "excerpt": {
90
+ "file": "app/cart.py",
91
+ "lines": [
92
+ { "n": 1, "text": "def checkout(cart):", "mark": true },
93
+ { "n": 2, "text": " ...", "mark": false }
94
+ ],
95
+ "caption": "The function is 40 lines long."
96
+ },
97
+ "expected": { "applies": "unclear" },
98
+ "why": "«Short» has no number."
99
+ }
100
+ ],
101
+ "items": [
102
+ {
103
+ "source_id": "review-sql-1",
104
+ "group": "all",
105
+ "body": "Every query is parameterised",
106
+ "repeat": { "group": "guide-sql", "index": 1, "of": 2 },
107
+ "situation": {
108
+ "summary": "Reviewer A asked for a change at {line}; reviewer B approved it.",
109
+ "question": "Does «Every query is parameterised» cover {line}?"
110
+ },
111
+ "excerpt": {
112
+ "file": "app/report.py",
113
+ "url": "https://example.com/shop/blob/0000000/app/report.py#L12",
114
+ "lines": [
115
+ { "n": 11, "text": "def monthly(month):", "mark": false },
116
+ {
117
+ "n": 12,
118
+ "text": " rows = db.raw(\"SELECT * FROM sales WHERE m=\" + month)",
119
+ "mark": true
120
+ },
121
+ { "n": 13, "text": "", "mark": false },
122
+ { "n": 14, "text": " return rows", "mark": false }
123
+ ],
124
+ "caption": "Reviewer A wrote: concatenated SQL.",
125
+ "mark_label": "◀ the line in question"
126
+ },
127
+ "references": {
128
+ "summary": "What the guideline says",
129
+ "entries": [
130
+ {
131
+ "label": "Guideline",
132
+ "title": "Security guide `SQL-1`",
133
+ "fields": [
134
+ { "text": "Build every query with parameters, never by joining strings." },
135
+ {
136
+ "label": "Why",
137
+ "text": "A query built from strings lets input become part of the query, and a value a user typed can then change what the query does: read other rows, change data, or drop a table. Parameters keep data and query apart in every driver the shop uses."
138
+ },
139
+ { "style": "note", "text": "The examples below are the guide's own." },
140
+ {
141
+ "label": "Not like this",
142
+ "style": "code",
143
+ "text": "db.raw(\"SELECT * FROM t WHERE id=\" + id)"
144
+ },
145
+ {
146
+ "label": "Like this",
147
+ "style": "code",
148
+ "text": "db.raw(\"SELECT * FROM t WHERE id=%s\", [id])"
149
+ }
150
+ ],
151
+ "url": "https://example.com/guide#sql-1",
152
+ "url_label": "Security guide"
153
+ }
154
+ ]
155
+ },
156
+ "more": {
157
+ "summary": "3 more lines like this",
158
+ "items": ["`app/export.py`", "`app/admin.py`"],
159
+ "rest": "and 1 more"
160
+ },
161
+ "table": {
162
+ "summary": "What each reviewer wrote",
163
+ "note": "Reviewer A asked for the change; reviewer B approved.",
164
+ "rows": [
165
+ {
166
+ "label": "A only · Comment",
167
+ "value": "use parameters",
168
+ "plain": "asks for a parameterised query",
169
+ "mark": { "tone": "yes", "text": "✓ about this line" }
170
+ },
171
+ {
172
+ "label": "B only · Approval",
173
+ "value": "LGTM",
174
+ "mark": { "tone": "no", "text": "✗ says nothing about this line" }
175
+ }
176
+ ]
177
+ }
178
+ },
179
+ {
180
+ "source_id": "review-sql-2",
181
+ "group": "all",
182
+ "body": "Every query is parameterised",
183
+ "repeat": { "group": "guide-sql", "index": 2, "of": 2 },
184
+ "situation": {
185
+ "summary": "Only one reviewer commented on {line}.",
186
+ "question": "Does «Every query is parameterised» cover {line}?"
187
+ },
188
+ "excerpt": {
189
+ "file": "app/search.py",
190
+ "lines": [
191
+ {
192
+ "n": 30,
193
+ "text": " q = `SELECT * FROM items WHERE name LIKE '${term}'`",
194
+ "mark": true
195
+ }
196
+ ]
197
+ }
198
+ },
199
+ {
200
+ "source_id": "review-short-1",
201
+ "group": "all",
202
+ "body": "Functions stay short",
203
+ "situation": {
204
+ "summary": "One reviewer asked to split the function, the other did not.",
205
+ "question": "Does «Functions stay short» cover this function?"
206
+ },
207
+ "excerpt": {
208
+ "lines": [
209
+ { "n": 1, "text": "def import_orders(path):", "mark": true }
210
+ ]
211
+ }
212
+ }
213
+ ]
214
+ }