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.
- package/dist/skills/build-label-page/scripts/label-page-core.d.ts +130 -21
- package/dist/skills/build-label-page/scripts/label-page-presets.d.ts +10 -0
- package/dist/skills/build-label-page/scripts/rule-descriptions.d.ts +6 -1
- package/package.json +1 -1
- package/skills/build-label-page/SKILL.md +9 -4
- package/skills/build-label-page/SKILL.md.spec.ts +8 -3
- package/skills/build-label-page/references/checklist.md +6 -6
- package/skills/build-label-page/references/closed-choice-generator.md +167 -21
- package/skills/build-label-page/scripts/fixtures/blocks-items.json +214 -0
- package/skills/build-label-page/scripts/fixtures/linter-config-witness-items.json +448 -0
- package/skills/build-label-page/scripts/label-page-blocks.test.ts +335 -0
- package/skills/build-label-page/scripts/label-page-browser.test.ts +437 -16
- package/skills/build-label-page/scripts/label-page-core.js +271 -46
- package/skills/build-label-page/scripts/label-page-core.test.ts +246 -37
- package/skills/build-label-page/scripts/label-page-core.ts +442 -113
- package/skills/build-label-page/scripts/label-page-html.test.ts +59 -52
- package/skills/build-label-page/scripts/label-page-presets.js +222 -0
- package/skills/build-label-page/scripts/label-page-presets.test.ts +175 -0
- package/skills/build-label-page/scripts/label-page-presets.ts +257 -0
- package/skills/build-label-page/scripts/label-page.js +19 -8
- package/skills/build-label-page/scripts/label-page.ts +19 -8
- package/skills/build-label-page/scripts/page-template.html +278 -45
- package/skills/build-label-page/scripts/rule-descriptions.js +48 -30
- package/skills/build-label-page/scripts/rule-descriptions.test.ts +58 -12
- 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
|
-
|
|
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
|
-
|
|
74
|
-
readonly
|
|
75
|
-
readonly
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
101
|
-
type Described = ReadonlyMap<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:
|
|
127
|
-
|
|
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:
|
|
166
|
-
|
|
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):
|
|
17
|
+
export declare function describeRule(ref: RuleRef, fromDir: string, run?: RunProcess): Described | undefined;
|
package/package.json
CHANGED
|
@@ -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:
|
|
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
|
|
67
|
-
|
|
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).
|
|
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
|
|
78
|
-
|
|
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).
|
|
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
|
-
-
|
|
85
|
-
|
|
86
|
-
the labeller does not know well.
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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
|
|
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
|
|
41
|
-
is taken out of the item, not added to the list to make a
|
|
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 "
|
|
44
|
-
lazily…",
|
|
45
|
-
does" paragraph, or the ESLint rule's
|
|
46
|
-
installed where `items.json` is
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 `"
|
|
93
|
-
|
|
94
|
-
|
|
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
|