paperlint 14.0.0 → 14.2.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 +105 -25
- package/dist/skills/build-label-page/scripts/label-page-presets.d.ts +10 -0
- package/package.json +1 -1
- package/skills/build-label-page/SKILL.md +20 -2
- package/skills/build-label-page/SKILL.md.spec.ts +19 -1
- package/skills/build-label-page/references/closed-choice-generator.md +171 -5
- 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-answerers.test.ts +381 -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 +637 -2
- package/skills/build-label-page/scripts/label-page-core.js +360 -70
- package/skills/build-label-page/scripts/label-page-core.test.ts +78 -11
- package/skills/build-label-page/scripts/label-page-core.ts +585 -107
- package/skills/build-label-page/scripts/label-page-html.test.ts +22 -0
- 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 +13 -2
- package/skills/build-label-page/scripts/label-page.ts +13 -2
- package/skills/build-label-page/scripts/page-template.html +438 -44
|
@@ -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;
|
|
@@ -86,6 +146,7 @@ type Card = {
|
|
|
86
146
|
type Item = Card & {
|
|
87
147
|
readonly source_id: string;
|
|
88
148
|
readonly group: string;
|
|
149
|
+
readonly answerers?: readonly string[];
|
|
89
150
|
};
|
|
90
151
|
type Practice = Card & {
|
|
91
152
|
readonly expected: Readonly<Record<string, string>>;
|
|
@@ -99,17 +160,22 @@ interface Config {
|
|
|
99
160
|
readonly seed: number;
|
|
100
161
|
readonly groups: Readonly<Record<string, string>>;
|
|
101
162
|
readonly labeller_knows?: readonly string[];
|
|
163
|
+
readonly answerers?: readonly string[];
|
|
164
|
+
readonly min_seconds?: number;
|
|
102
165
|
readonly familiarity?: boolean;
|
|
103
166
|
readonly familiarity_question?: string;
|
|
104
167
|
readonly questions: readonly Question[];
|
|
105
168
|
readonly practice: readonly Practice[];
|
|
106
169
|
readonly practice_round?: number;
|
|
170
|
+
readonly labels?: Readonly<Record<string, string>>;
|
|
107
171
|
readonly items: readonly Item[];
|
|
108
172
|
}
|
|
109
173
|
interface KeyEntry {
|
|
110
174
|
readonly source_id: string;
|
|
111
175
|
readonly group: string;
|
|
112
176
|
readonly on_page: boolean;
|
|
177
|
+
/** Who the item is shown to, when not every answerer of the page. */
|
|
178
|
+
readonly answerers?: readonly string[];
|
|
113
179
|
}
|
|
114
180
|
/** Rule → the linter's description (and a ruff rule's family), for every rule a card names. */
|
|
115
181
|
type Described = ReadonlyMap<string, RuleText>;
|
|
@@ -122,18 +188,6 @@ declare function itemsOnPage(cfg: Config, keyPath: string): {
|
|
|
122
188
|
};
|
|
123
189
|
/** What a page shows: the questions, the practice and the items, blind, with their neutral ids. */
|
|
124
190
|
declare function payloadOf(cfg: Config, page: ReturnType<typeof itemsOnPage>, described: Described): {
|
|
125
|
-
title: string;
|
|
126
|
-
intro: string;
|
|
127
|
-
groups: {
|
|
128
|
-
id: string;
|
|
129
|
-
label: string;
|
|
130
|
-
}[];
|
|
131
|
-
familiarity_question: string;
|
|
132
|
-
familiarity: {
|
|
133
|
-
value: string;
|
|
134
|
-
label: string;
|
|
135
|
-
}[];
|
|
136
|
-
allowed: JsonObject;
|
|
137
191
|
questions: readonly Question[];
|
|
138
192
|
practice: {
|
|
139
193
|
round: number;
|
|
@@ -155,9 +209,11 @@ declare function payloadOf(cfg: Config, page: ReturnType<typeof itemsOnPage>, de
|
|
|
155
209
|
})[];
|
|
156
210
|
};
|
|
157
211
|
items: ({
|
|
212
|
+
answerers?: readonly string[];
|
|
158
213
|
rules?: never;
|
|
159
214
|
id: string;
|
|
160
215
|
} | {
|
|
216
|
+
answerers?: readonly string[];
|
|
161
217
|
rules: {
|
|
162
218
|
description?: string;
|
|
163
219
|
family?: string;
|
|
@@ -166,6 +222,23 @@ declare function payloadOf(cfg: Config, page: ReturnType<typeof itemsOnPage>, de
|
|
|
166
222
|
}[];
|
|
167
223
|
id: string;
|
|
168
224
|
})[];
|
|
225
|
+
answerers?: readonly string[];
|
|
226
|
+
min_seconds?: number;
|
|
227
|
+
title: string;
|
|
228
|
+
intro: string;
|
|
229
|
+
labels: {
|
|
230
|
+
[x: string]: string;
|
|
231
|
+
};
|
|
232
|
+
groups: {
|
|
233
|
+
id: string;
|
|
234
|
+
label: string;
|
|
235
|
+
}[];
|
|
236
|
+
familiarity_question: string;
|
|
237
|
+
familiarity: {
|
|
238
|
+
value: string;
|
|
239
|
+
label: string;
|
|
240
|
+
}[];
|
|
241
|
+
allowed: JsonObject;
|
|
169
242
|
};
|
|
170
243
|
export type Payload = ReturnType<typeof payloadOf>;
|
|
171
244
|
/**
|
|
@@ -175,18 +248,6 @@ export type Payload = ReturnType<typeof payloadOf>;
|
|
|
175
248
|
*/
|
|
176
249
|
export declare function prepareBuild(configPath: string, outDir: string): {
|
|
177
250
|
payload: {
|
|
178
|
-
title: string;
|
|
179
|
-
intro: string;
|
|
180
|
-
groups: {
|
|
181
|
-
id: string;
|
|
182
|
-
label: string;
|
|
183
|
-
}[];
|
|
184
|
-
familiarity_question: string;
|
|
185
|
-
familiarity: {
|
|
186
|
-
value: string;
|
|
187
|
-
label: string;
|
|
188
|
-
}[];
|
|
189
|
-
allowed: JsonObject;
|
|
190
251
|
questions: readonly Question[];
|
|
191
252
|
practice: {
|
|
192
253
|
round: number;
|
|
@@ -208,9 +269,11 @@ export declare function prepareBuild(configPath: string, outDir: string): {
|
|
|
208
269
|
})[];
|
|
209
270
|
};
|
|
210
271
|
items: ({
|
|
272
|
+
answerers?: readonly string[];
|
|
211
273
|
rules?: never;
|
|
212
274
|
id: string;
|
|
213
275
|
} | {
|
|
276
|
+
answerers?: readonly string[];
|
|
214
277
|
rules: {
|
|
215
278
|
description?: string;
|
|
216
279
|
family?: string;
|
|
@@ -219,6 +282,23 @@ export declare function prepareBuild(configPath: string, outDir: string): {
|
|
|
219
282
|
}[];
|
|
220
283
|
id: string;
|
|
221
284
|
})[];
|
|
285
|
+
answerers?: readonly string[];
|
|
286
|
+
min_seconds?: number;
|
|
287
|
+
title: string;
|
|
288
|
+
intro: string;
|
|
289
|
+
labels: {
|
|
290
|
+
[x: string]: string;
|
|
291
|
+
};
|
|
292
|
+
groups: {
|
|
293
|
+
id: string;
|
|
294
|
+
label: string;
|
|
295
|
+
}[];
|
|
296
|
+
familiarity_question: string;
|
|
297
|
+
familiarity: {
|
|
298
|
+
value: string;
|
|
299
|
+
label: string;
|
|
300
|
+
}[];
|
|
301
|
+
allowed: JsonObject;
|
|
222
302
|
};
|
|
223
303
|
dropped: number;
|
|
224
304
|
};
|
|
@@ -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
|
@@ -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:dce0979d7f187ae7 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.
|
|
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
|
|
@@ -138,6 +142,20 @@ building. In one line each:
|
|
|
138
142
|
same pass, and add the line "labels → `data/<study>-labels-<date>.json`" to the paper's
|
|
139
143
|
`NOTES.md` for the numbers that use them.
|
|
140
144
|
|
|
145
|
+
## Two answerers, and a time on screen
|
|
146
|
+
|
|
147
|
+
Two optional top-level keys of `items.json`, both left out by default:
|
|
148
|
+
|
|
149
|
+
- `"answerers": ["author", "second"]` — two or more people answer the same published page, each
|
|
150
|
+
under their own name (picked on the page) and without seeing the other's answers; an item's own
|
|
151
|
+
`answerers` shows it to that subset only. The store's ids become `<answerer>~<id>`, and the
|
|
152
|
+
labels file keeps each answerer's answers under `by_answerer`.
|
|
153
|
+
- `"min_seconds": 30` — a card's answer saves only after the card has been on screen that long;
|
|
154
|
+
the seconds at its first save are stored and exported as `seconds`.
|
|
155
|
+
|
|
156
|
+
Details and the export's shape: [`references/closed-choice-generator.md`](references/closed-choice-generator.md),
|
|
157
|
+
"Two answerers, and a minimum time on screen".
|
|
158
|
+
|
|
141
159
|
## When the page misbehaves
|
|
142
160
|
|
|
143
161
|
Look at what the store holds (`ArtifactData` `list`) before changing anything: the page may be fine
|
|
@@ -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.
|
|
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
|
|
@@ -149,6 +153,20 @@ building. In one line each:
|
|
|
149
153
|
same pass, and add the line "labels → \`data/<study>-labels-<date>.json\`" to the paper's
|
|
150
154
|
\`NOTES.md\` for the numbers that use them.
|
|
151
155
|
|
|
156
|
+
## Two answerers, and a time on screen
|
|
157
|
+
|
|
158
|
+
Two optional top-level keys of \`items.json\`, both left out by default:
|
|
159
|
+
|
|
160
|
+
- \`"answerers": ["author", "second"]\` — two or more people answer the same published page, each
|
|
161
|
+
under their own name (picked on the page) and without seeing the other's answers; an item's own
|
|
162
|
+
\`answerers\` shows it to that subset only. The store's ids become \`<answerer>~<id>\`, and the
|
|
163
|
+
labels file keeps each answerer's answers under \`by_answerer\`.
|
|
164
|
+
- \`"min_seconds": 30\` — a card's answer saves only after the card has been on screen that long;
|
|
165
|
+
the seconds at its first save are stored and exported as \`seconds\`.
|
|
166
|
+
|
|
167
|
+
Details and the export's shape: [\`references/closed-choice-generator.md\`](references/closed-choice-generator.md),
|
|
168
|
+
"Two answerers, and a minimum time on screen".
|
|
169
|
+
|
|
152
170
|
## When the page misbehaves
|
|
153
171
|
|
|
154
172
|
Look at what the store holds (\`ArtifactData\` \`list\`) before changing anything: the page may be fine
|
|
@@ -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
|
|
@@ -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.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
|
@@ -122,6 +246,48 @@ author cannot read it is an exclusion made after the draw and goes unrecorded. T
|
|
|
122
246
|
units whose fate is obvious. Each dropped unit carries its reason in the labels file. The author's words, with the date, then go into
|
|
123
247
|
`author_check` of the plan; a dropped unit is replaced by the next one in the draw.
|
|
124
248
|
|
|
249
|
+
## Two answerers, and a minimum time on screen
|
|
250
|
+
|
|
251
|
+
Both keys are optional and top-level. Left out, the page, its store documents and the export are
|
|
252
|
+
exactly as above.
|
|
253
|
+
|
|
254
|
+
- **`answerers`**: two or more names (letters, digits, `-` and `_`), for a study where two people
|
|
255
|
+
answer the same published page, each under their own name and without seeing the other's
|
|
256
|
+
answers. The page first asks "Who is answering?"; the choice is kept in the browser's
|
|
257
|
+
localStorage when it has one (otherwise it asks again on every visit), and "Switch" beside
|
|
258
|
+
"Answering as …" asks again. Every document the page writes is named `<answerer>~<id>`
|
|
259
|
+
(`second~G004`, `second~r1-P2`, `second~python`): `~` is outside the name pattern, so no answerer,
|
|
260
|
+
item, group or practice id can hold it. The page reads only the current answerer's documents, so
|
|
261
|
+
their progress, done-state and practice are their own. An item may carry its own `answerers`, a
|
|
262
|
+
subset of the page's: it is shown only to those. A name an item lists that the page does not, a
|
|
263
|
+
name twice, or an item's list on a page without one stops the build.
|
|
264
|
+
- **`min_seconds`**: an item's answer saves only after its card has been on screen that long. The
|
|
265
|
+
cards are a list, so "on screen" is measured by an `IntersectionObserver`: half the card in view,
|
|
266
|
+
or half the viewport filled by a card taller than that, while the tab is visible. A locked card
|
|
267
|
+
shows "You can answer in N s" under its disabled buttons. The answer document gains `seconds`:
|
|
268
|
+
how long the card had been on screen at its first save, kept on every later save. The practice
|
|
269
|
+
cards are not held.
|
|
270
|
+
|
|
271
|
+
`key.json` carries `answerers`, each item's own subset (`ids.<id>.answerers`, only where it has one)
|
|
272
|
+
and `min_seconds`. With `answerers`, `export` refuses a document id without `<answerer>~`, an
|
|
273
|
+
answerer the key does not list, and an answer to an item not shown to its answerer, as it refuses
|
|
274
|
+
an unknown item. The labels file keeps each answerer's answers apart:
|
|
275
|
+
|
|
276
|
+
```text
|
|
277
|
+
{ "answerers": ["author", "second"],
|
|
278
|
+
"items": [{ "id": "G001", "source_id": "case-1000", "group": "python",
|
|
279
|
+
"answerers": ["author", "second"],
|
|
280
|
+
"by_answerer": { "author": { "answers": { "label": "yes" }, "complete": true,
|
|
281
|
+
"seconds": 41, "…": "the fields of an item above" },
|
|
282
|
+
"second": { "…": "…" } } }],
|
|
283
|
+
"by_answerer": { "author": { "groups": {}, "complete_items": 1, "total_items": 1, "…": "…" },
|
|
284
|
+
"second": { "…": "…" } } }
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
An answerer with no document for an item is still listed, with every answer `null`. Each
|
|
288
|
+
answerer's practice is scored into its own file, `<study>-practice-r<round>-<answerer>-<date>.json`.
|
|
289
|
+
`seconds` appears only when the page was built with `min_seconds`.
|
|
290
|
+
|
|
125
291
|
## The export
|
|
126
292
|
|
|
127
293
|
`export` reads the store's three collections (`answers`, `groups`, `practice`) from `dump.json`, in
|