paperlint 13.6.1 → 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.
Files changed (25) hide show
  1. package/dist/skills/build-label-page/scripts/label-page-core.d.ts +130 -21
  2. package/dist/skills/build-label-page/scripts/label-page-presets.d.ts +10 -0
  3. package/dist/skills/build-label-page/scripts/rule-descriptions.d.ts +6 -1
  4. package/package.json +1 -1
  5. package/skills/build-label-page/SKILL.md +9 -4
  6. package/skills/build-label-page/SKILL.md.spec.ts +8 -3
  7. package/skills/build-label-page/references/checklist.md +6 -6
  8. package/skills/build-label-page/references/closed-choice-generator.md +167 -21
  9. package/skills/build-label-page/scripts/fixtures/blocks-items.json +214 -0
  10. package/skills/build-label-page/scripts/fixtures/linter-config-witness-items.json +448 -0
  11. package/skills/build-label-page/scripts/label-page-blocks.test.ts +335 -0
  12. package/skills/build-label-page/scripts/label-page-browser.test.ts +437 -16
  13. package/skills/build-label-page/scripts/label-page-core.js +271 -46
  14. package/skills/build-label-page/scripts/label-page-core.test.ts +246 -37
  15. package/skills/build-label-page/scripts/label-page-core.ts +442 -113
  16. package/skills/build-label-page/scripts/label-page-html.test.ts +59 -52
  17. package/skills/build-label-page/scripts/label-page-presets.js +222 -0
  18. package/skills/build-label-page/scripts/label-page-presets.test.ts +175 -0
  19. package/skills/build-label-page/scripts/label-page-presets.ts +257 -0
  20. package/skills/build-label-page/scripts/label-page.js +19 -8
  21. package/skills/build-label-page/scripts/label-page.ts +19 -8
  22. package/skills/build-label-page/scripts/page-template.html +278 -45
  23. package/skills/build-label-page/scripts/rule-descriptions.js +48 -30
  24. package/skills/build-label-page/scripts/rule-descriptions.test.ts +58 -12
  25. package/skills/build-label-page/scripts/rule-descriptions.ts +56 -33
@@ -1,4 +1,4 @@
1
- import { type RuleRef } from "./rule-descriptions.ts";
1
+ import { type Described as RuleText, type RuleRef } from "./rule-descriptions.ts";
2
2
  export declare class Refusal extends Error {
3
3
  readonly exitCode: number;
4
4
  constructor(exitCode: number, message: string);
@@ -13,11 +13,14 @@ type Json = null | boolean | number | string | readonly Json[] | {
13
13
  type JsonObject = {
14
14
  readonly [key: string]: Json;
15
15
  };
16
- declare const PAGE_FIELDS: readonly ["group", "title", "body", "code", "context"];
17
- type SchemaNode = {
16
+ declare const PAGE_FIELDS: readonly ["group", "title", "body", "code", "context", "file"];
17
+ 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;
@@ -40,7 +43,16 @@ type SchemaNode = {
40
43
  required: boolean;
41
44
  closed: boolean;
42
45
  fields: Readonly<Record<string, SchemaNode>>;
46
+ }) & {
47
+ why?: string;
43
48
  };
49
+ /**
50
+ * THE PAGE KINDS, and the card fields each requires on every item and practice card (#373). A fix to
51
+ * a card is a field; a kind that needs it makes it required, so a page built without it fails the
52
+ * schema with the field named instead of shipping the old card.
53
+ */
54
+ type Kind = "approval" | "witness" | "labels";
55
+ /** The declared schema (`schema` prints it): every field, those a kind requires left optional. */
44
56
  export declare const CONFIG_SCHEMA: SchemaNode;
45
57
  interface Option {
46
58
  readonly value: string;
@@ -58,28 +70,90 @@ interface Question {
58
70
  interface Unit {
59
71
  readonly id: string;
60
72
  readonly text: string;
73
+ readonly mark: string;
61
74
  readonly part: number;
62
75
  readonly of: number;
63
76
  }
64
- type Item = {
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
+ }
134
+ /** What a card shows, on an item and on a practice item alike. */
135
+ type Card = {
65
136
  readonly [F in (typeof PAGE_FIELDS)[number]]?: string;
66
- } & {
67
- readonly source_id: string;
68
- readonly group: string;
137
+ } & Blocks & {
69
138
  readonly body: string;
70
139
  readonly rules?: readonly RuleRef[];
71
140
  readonly unit?: Unit;
141
+ readonly readings?: {
142
+ readonly a: string;
143
+ readonly b: string;
144
+ };
72
145
  };
73
- interface Practice {
74
- readonly title?: string;
75
- readonly body: string;
76
- readonly code?: string;
77
- readonly context?: string;
146
+ type Item = Card & {
147
+ readonly source_id: string;
148
+ readonly group: string;
149
+ };
150
+ type Practice = Card & {
78
151
  readonly expected: Readonly<Record<string, string>>;
79
152
  readonly why: string;
80
- }
153
+ };
81
154
  interface Config {
82
155
  readonly study: string;
156
+ readonly kind: Kind;
83
157
  readonly title: string;
84
158
  readonly intro?: string;
85
159
  readonly seed: number;
@@ -90,6 +164,7 @@ interface Config {
90
164
  readonly questions: readonly Question[];
91
165
  readonly practice: readonly Practice[];
92
166
  readonly practice_round?: number;
167
+ readonly labels?: Readonly<Record<string, string>>;
93
168
  readonly items: readonly Item[];
94
169
  }
95
170
  interface KeyEntry {
@@ -97,8 +172,8 @@ interface KeyEntry {
97
172
  readonly group: string;
98
173
  readonly on_page: boolean;
99
174
  }
100
- /** Rule → the linter's description, for every rule an item on the page names. */
101
- type Described = ReadonlyMap<string, string>;
175
+ /** Rule → the linter's description (and a ruff rule's family), for every rule a card names. */
176
+ type Described = ReadonlyMap<string, RuleText>;
102
177
  /** Only what the labeller can answer goes on the page, and only that gets an id, in id order. */
103
178
  declare function itemsOnPage(cfg: Config, keyPath: string): {
104
179
  items: Item[];
@@ -110,6 +185,9 @@ declare function itemsOnPage(cfg: Config, keyPath: string): {
110
185
  declare function payloadOf(cfg: Config, page: ReturnType<typeof itemsOnPage>, described: Described): {
111
186
  title: string;
112
187
  intro: string;
188
+ labels: {
189
+ [x: string]: string;
190
+ };
113
191
  groups: {
114
192
  id: string;
115
193
  label: string;
@@ -123,8 +201,21 @@ declare function payloadOf(cfg: Config, page: ReturnType<typeof itemsOnPage>, de
123
201
  questions: readonly Question[];
124
202
  practice: {
125
203
  round: number;
126
- items: readonly (Practice & {
127
- readonly id: string;
204
+ items: ({
205
+ rules?: never;
206
+ id: string;
207
+ expected: Readonly<Record<string, string>>;
208
+ why: string;
209
+ } | {
210
+ rules: {
211
+ description?: string;
212
+ family?: string;
213
+ linter: "eslint" | "ruff";
214
+ id: string;
215
+ }[];
216
+ id: string;
217
+ expected: Readonly<Record<string, string>>;
218
+ why: string;
128
219
  })[];
129
220
  };
130
221
  items: ({
@@ -132,9 +223,10 @@ declare function payloadOf(cfg: Config, page: ReturnType<typeof itemsOnPage>, de
132
223
  id: string;
133
224
  } | {
134
225
  rules: {
226
+ description?: string;
227
+ family?: string;
135
228
  linter: "eslint" | "ruff";
136
229
  id: string;
137
- description: string;
138
230
  }[];
139
231
  id: string;
140
232
  })[];
@@ -149,6 +241,9 @@ export declare function prepareBuild(configPath: string, outDir: string): {
149
241
  payload: {
150
242
  title: string;
151
243
  intro: string;
244
+ labels: {
245
+ [x: string]: string;
246
+ };
152
247
  groups: {
153
248
  id: string;
154
249
  label: string;
@@ -162,8 +257,21 @@ export declare function prepareBuild(configPath: string, outDir: string): {
162
257
  questions: readonly Question[];
163
258
  practice: {
164
259
  round: number;
165
- items: readonly (Practice & {
166
- readonly id: string;
260
+ items: ({
261
+ rules?: never;
262
+ id: string;
263
+ expected: Readonly<Record<string, string>>;
264
+ why: string;
265
+ } | {
266
+ rules: {
267
+ description?: string;
268
+ family?: string;
269
+ linter: "eslint" | "ruff";
270
+ id: string;
271
+ }[];
272
+ id: string;
273
+ expected: Readonly<Record<string, string>>;
274
+ why: string;
167
275
  })[];
168
276
  };
169
277
  items: ({
@@ -171,9 +279,10 @@ export declare function prepareBuild(configPath: string, outDir: string): {
171
279
  id: string;
172
280
  } | {
173
281
  rules: {
282
+ description?: string;
283
+ family?: string;
174
284
  linter: "eslint" | "ruff";
175
285
  id: string;
176
- description: string;
177
286
  }[];
178
287
  id: string;
179
288
  })[];
@@ -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;
@@ -2,6 +2,11 @@ export interface RuleRef {
2
2
  readonly linter: "eslint" | "ruff";
3
3
  readonly id: string;
4
4
  }
5
+ /** What the linter says a rule does; a ruff rule also names the family it comes from. */
6
+ export interface Described {
7
+ readonly description: string;
8
+ readonly family?: string;
9
+ }
5
10
  /** A process run to its end: the exit status and what it printed. */
6
11
  export type RunProcess = (cmd: string, args: readonly string[]) => {
7
12
  readonly status: number | null;
@@ -9,4 +14,4 @@ export type RunProcess = (cmd: string, args: readonly string[]) => {
9
14
  };
10
15
  export declare const runProcess: RunProcess;
11
16
  /** The linter's own one-line description of `ref`, or undefined when it has none. */
12
- export declare function describeRule(ref: RuleRef, fromDir: string, run?: RunProcess): string | undefined;
17
+ export declare function describeRule(ref: RuleRef, fromDir: string, run?: RunProcess): Described | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperlint",
3
- "version": "13.6.1",
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:bd616b2694831fa1 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
 
@@ -63,8 +63,8 @@ building. In one line each:
63
63
  source labels on the page. The generator refuses to build when an item carries a field outside an
64
64
  allow-list; a page written by hand checks its items against the same list.
65
65
  5. **Ask only what the labeller can answer.** Only the languages and tools they know; one "how well do
66
- you know it" question per group, used later as an analysis filter (off on an approval page,
67
- `"familiarity": false`: the author has not seen those repositories); "clear-cut or real boundary"
66
+ you know it" question per group when the study filters on it (`"familiarity": true`; left
67
+ out, none is asked, as on an approval page, whose author has not seen those repositories); "clear-cut or real boundary"
68
68
  is the labeller's judgement, not the designer's.
69
69
  6. **Export into the repository right away**, one JSON per page into the study's `data/` with the
70
70
  export date, commit it, and record answers given in chat with `source: "chat"`. If the page fails,
@@ -104,7 +104,12 @@ building. In one line each:
104
104
  ```
105
105
 
106
106
  It writes `label-page/page.html` and `label-page/key.json` (the only link from `G001` to the
107
- source id; it never goes on the page). Written by hand, the page meets the requirements above.
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. 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.
108
113
  4. **Read it as the labeller, then look at it.** Read the page text once as a person who knows
109
114
  nothing about the source: if anything on it tells the answer, take it out. Read each question as
110
115
  someone who did not design the study: can they tell what each answer means from the question, its
@@ -74,8 +74,8 @@ building. In one line each:
74
74
  source labels on the page. The generator refuses to build when an item carries a field outside an
75
75
  allow-list; a page written by hand checks its items against the same list.
76
76
  5. **Ask only what the labeller can answer.** Only the languages and tools they know; one "how well do
77
- you know it" question per group, used later as an analysis filter (off on an approval page,
78
- \`"familiarity": false\`: the author has not seen those repositories); "clear-cut or real boundary"
77
+ you know it" question per group when the study filters on it (\`"familiarity": true\`; left
78
+ out, none is asked, as on an approval page, whose author has not seen those repositories); "clear-cut or real boundary"
79
79
  is the labeller's judgement, not the designer's.
80
80
  6. **Export into the repository right away**, one JSON per page into the study's \`data/\` with the
81
81
  export date, commit it, and record answers given in chat with \`source: "chat"\`. If the page fails,
@@ -115,7 +115,12 @@ building. In one line each:
115
115
  \`\`\`
116
116
 
117
117
  It writes \`label-page/page.html\` and \`label-page/key.json\` (the only link from \`G001\` to the
118
- source id; it never goes on the page). Written by hand, the page meets the requirements above.
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. 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.
119
124
  4. **Read it as the labeller, then look at it.** Read the page text once as a person who knows
120
125
  nothing about the source: if anything on it tells the answer, take it out. Read each question as
121
126
  someone who did not design the study: can they tell what each answer means from the question, its
@@ -81,11 +81,11 @@ the text of an allowed field says.
81
81
 
82
82
  - Restrict the items to the languages and tools the labeller knows (`labeller_knows`). Items of
83
83
  other groups are not put on the page and get no id.
84
- - Add one question per group, "how well do you know this language / tool" (well / somewhat / not
85
- really). It is an analysis filter: the study can report the labels with and without the groups
86
- the labeller does not know well. An approval page asks none (`"familiarity": false` in
87
- `items.json`): its question is about a drawn unit, not about repositories the author has seen,
88
- and an answer to "how well do you know the projects of this group" there means nothing.
84
+ - When the study filters on it, ask one question per group, "how well do you know this language /
85
+ tool" (well / somewhat / not really), with `"familiarity": true` in `items.json`: the study can
86
+ then report the labels with and without the groups the labeller does not know well. Left out, no
87
+ page asks it: on an approval page the question is about a drawn unit, not about repositories the
88
+ author has seen, and "how well do you know the projects of this group" there means nothing.
89
89
  - The per-item judgement "clear-cut or a real boundary case" is the labeller's. The study design
90
90
  does not decide it in advance and does not pre-fill it.
91
91
 
@@ -94,7 +94,7 @@ answer is the only way to find it afterwards. And a boundary case is boundary be
94
94
  person hesitates; a designer who tags it in advance has measured the design, not the item.
95
95
 
96
96
  **Check.** The build prints how many items were left out; the export has a familiarity value for
97
- every group, or the page was built with `"familiarity": false` and the export has no groups.
97
+ every group, or the page was built without `"familiarity": true` and the export has no groups.
98
98
 
99
99
  ## 6. Export into the repository right away
100
100
 
@@ -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
@@ -33,32 +37,173 @@ length.
33
37
  required `example`: a real case of the study, in a line, that takes that answer. `icon` (an
34
38
  emoji) is optional. Two labels that read the same, or two equal values, stop the build.
35
39
  - **`practice`**: 3 or more real cases whose answer you are sure of, each with `expected` (one answer
36
- per question) and a one-line `why`. Never an item that is also on the page to be labelled.
40
+ per question) and a one-line `why`. Never an item that is also on the page to be labelled. A
41
+ practice card carries the same fields as the real cards it rehearses (`rules`, `unit`,
42
+ `readings`, `file`).
37
43
  - **`practice_round`**: raise it after rewording a question; the practice reopens and the answers of
38
44
  the earlier round are kept but not counted.
39
45
  - **`items`**: `source_id` (the study's own, never shown), `group`, `body`, and optionally `title`,
40
- `code`, `context`, `rules`, `unit`. Nothing else: the allow-list is fixed in the generator, and a field outside it
41
- is taken out of the item, not added to the list to make a build pass.
46
+ `code`, `context`, `rules`, `unit`, `readings`, `file`. Nothing else: the allow-list is fixed in
47
+ the generator, and a field outside it is taken out of the item, not added to the list to make a
48
+ build pass.
49
+ - **`kind`** (required): what the page asks, and so which card fields every item and practice card
50
+ must carry. A card without one fails the schema, naming the field and why the kind needs it:
51
+
52
+ | kind | the labeller decides | required on every card |
53
+ | ---------- | ---------------------------------------------------- | -------------------------------------------------- |
54
+ | `approval` | keep or drop a drawn requirement | `unit` (the line, the requirement marked), `rules` |
55
+ | `witness` | why two readings of a requirement disagree on a file | `rules`, `readings`, `file` |
56
+ | `labels` | a gold label, a blind review | nothing beyond the item |
57
+
58
+ Nothing reads `context`: it stays free text. A rule, a line or two readings written there show
59
+ none of what their fields show, so on an `approval` or `witness` page they go in the fields.
60
+
42
61
  - **`rules`**: `[{linter, id}]` (`linter` is `eslint` or `ruff`) on an item read as a linter rule.
43
- The card shows "Rule ruff TID253: Checks for module-level imports that should instead be imported
44
- lazily…", the description taken at build time from the linter itself: `ruff rule <id>`'s "What it
45
- does" paragraph, or the ESLint rule's `meta.docs.description` from the ESLint and plugins
46
- installed where `items.json` is. The build refuses a rule the linter cannot describe; a
62
+ The card's "Rule" row shows "ruff `TID253` (flake8-tidy-imports): Checks for module-level
63
+ imports that should instead be imported lazily…", taken at build time from the linter itself:
64
+ `ruff rule <id>`'s "What it does" paragraph and family, or the ESLint rule's
65
+ `meta.docs.description` from the ESLint and plugins installed where `items.json` is (a plugin
66
+ rule's own name may hold a slash: `n/no-unsupported-features/node-builtins`). The build refuses a rule the linter cannot describe; a
47
67
  description is never typed into `items.json`.
48
- - **`unit`**: `{id, text, part, of}` on an item that is one requirement of a line holding several
49
- ("TypeScript only (no JavaScript); top-level await is allowed" is two). Each requirement is its
50
- own item with its own question, and its card shows "From the line (requirement 1 of 2):" and the
51
- whole line beneath, so the labeller judges one requirement at a time and sees where it came from.
52
- The build refuses a line whose parts do not add up (one missing, repeated, a different `of`, or
53
- split across groups), naming the item. `id` names one occurrence of the line (a file and line
68
+ - **`unit`**: `{id, text, mark, part, of}` on an item drawn from a source line: the whole line,
69
+ the requirement's own words in it (`mark`, shown highlighted), and which requirement of the line
70
+ it is ("TypeScript only (no JavaScript); top-level await is allowed" is two). The card's "Line"
71
+ row shows the line with the mark, and "part 1 of 2" when the line holds several. A drawn sample
72
+ may hold one part of a line and not the others; the build refuses a `mark` that is not in the
73
+ line, and parts of one line that disagree (another text or `of`, a part repeated, split across
74
+ groups), naming the item.
75
+ - **`readings`**: `{a, b}` on an item that compares two readings of one requirement: two rows,
76
+ "Reading A" on a solid accent rule and "Reading B" on a dashed one, in the same place on every
77
+ card. **`file`**: the witness file (a path, a link, what each reading did with it), on its own
78
+ "File" row. `id` names one occurrence of the line (a file and line
54
79
  number, say) and is never shown: two files holding the same words are two lines, each judged in
55
80
  full. Splitting the line is the study's job, before the page.
56
81
  - **`seed`**: fixes the order. Rebuilding with the same seed keeps every id, and a new item gets the
57
82
  next one; a different seed is refused once a key exists.
58
- - **`ask_why`** on an option: a prompt. That answer is incomplete, on the page and in the export,
59
- until a reason is saved. The reason saves as it is typed (after a pause, and when the page is
60
- left), so a labeller who closes the page from inside the box does not leave a drop without its
61
- 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.
62
207
 
63
208
  ## The approval page
64
209
 
@@ -89,9 +234,10 @@ drop, the drop carrying `ask_why`.
89
234
  }
90
235
  ```
91
236
 
92
- The page sets `"familiarity": false`: the author is judging drawn units, not repositories they
93
- have seen, so the per-group "how well do you know it" question is left out (and a
94
- `familiarity_question` beside it is refused).
237
+ The page sets `"kind": "approval"`, so every card carries its `unit` and `rules`, and leaves
238
+ `familiarity` out: the author is judging drawn units, not repositories they have
239
+ seen, so no per-group "how well do you know it" question is asked. Only a page that sets
240
+ `"familiarity": true` asks it (and a `familiarity_question` without it is refused).
95
241
 
96
242
  The items are the drawn units verbatim, in a language the author reads. Units in other languages
97
243
  are taken out before the draw, as a population rule the study's `plan.json` states ("English-language