paperlint 14.2.0 → 14.4.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/CLAUDE.md +6 -1
- package/dist/skills/build-label-page/scripts/label-page-core.d.ts +27 -2
- package/docs/testing.md +12 -0
- package/eslint.config.mjs +17 -0
- package/package.json +2 -1
- package/skills/build-label-page/SKILL.md +6 -4
- package/skills/build-label-page/SKILL.md.spec.ts +5 -3
- package/skills/build-label-page/references/checklist.md +3 -3
- package/skills/build-label-page/references/closed-choice-generator.md +58 -18
- package/skills/build-label-page/scripts/fixtures/v7-cards-items.json +138 -0
- package/skills/build-label-page/scripts/label-page-blocks.test.ts +276 -1
- package/skills/build-label-page/scripts/label-page-browser.test.ts +513 -10
- package/skills/build-label-page/scripts/label-page-core.js +105 -9
- package/skills/build-label-page/scripts/label-page-core.test.ts +25 -1
- package/skills/build-label-page/scripts/label-page-core.ts +178 -10
- package/skills/build-label-page/scripts/label-page-html.test.ts +29 -0
- package/skills/build-label-page/scripts/label-page-presets.test.ts +1 -1
- package/skills/build-label-page/scripts/page-template.html +300 -60
package/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- vigiles:sha256:
|
|
1
|
+
<!-- vigiles:sha256:d595fb538eb0a81e compiled from CLAUDE.md.spec.ts -->
|
|
2
2
|
|
|
3
3
|
# CLAUDE.md
|
|
4
4
|
|
|
@@ -106,6 +106,11 @@ Before writing machinery, **run the thing you are about to replace and paste its
|
|
|
106
106
|
|
|
107
107
|
**Guidance only** — Before adding a CLI command or changing a delivery channel (skill links, hook wiring, the composite action, the npm package), read how comparable tools did it. "We need another command for X" has, in every tool examined, been the CONFIG not declaring something. [Details](docs/prior-art/README.md)
|
|
108
108
|
|
|
109
|
+
### Browser Tests Find By Data Test
|
|
110
|
+
|
|
111
|
+
**Enforced by:** `eslint/playwright/no-raw-locators`
|
|
112
|
+
**Why:** A browser test finds an element only by `getByTestId` on `data-test`, and reads its values from the JSON in `data-test-meta`, never from CSS classes or visible text; content text is compared with the fixture's value. The older browser tests sit in `eslint-suppressions.json`, which may only shrink. [Details](docs/testing.md)
|
|
113
|
+
|
|
109
114
|
### New Code Is Functional
|
|
110
115
|
|
|
111
116
|
**Enforced by:** `eslint/functional/no-let`
|
|
@@ -83,20 +83,45 @@ interface CodeLine {
|
|
|
83
83
|
readonly n: number;
|
|
84
84
|
readonly text: string;
|
|
85
85
|
readonly mark: boolean;
|
|
86
|
+
readonly gutter?: string;
|
|
87
|
+
readonly context?: boolean;
|
|
88
|
+
}
|
|
89
|
+
/** One entry of a term's note: a verbatim quote or snippet, and the link to where it is from. */
|
|
90
|
+
interface NoteEntry {
|
|
91
|
+
readonly title?: string;
|
|
92
|
+
readonly says?: string;
|
|
93
|
+
readonly quote?: string;
|
|
94
|
+
readonly code?: string;
|
|
95
|
+
readonly example?: string;
|
|
96
|
+
readonly url: string;
|
|
97
|
+
readonly url_label: string;
|
|
98
|
+
}
|
|
99
|
+
interface Term {
|
|
100
|
+
readonly term: string;
|
|
101
|
+
readonly note: readonly NoteEntry[];
|
|
86
102
|
}
|
|
87
103
|
/** The content blocks (#380), each optional on any kind. */
|
|
88
104
|
interface Blocks {
|
|
89
105
|
readonly situation?: {
|
|
90
|
-
readonly summary
|
|
106
|
+
readonly summary?: string;
|
|
91
107
|
readonly question: string;
|
|
92
108
|
};
|
|
93
109
|
readonly excerpt?: {
|
|
94
110
|
readonly file?: string;
|
|
111
|
+
readonly lang?: string;
|
|
95
112
|
readonly url?: string;
|
|
113
|
+
readonly gutter_header?: string;
|
|
96
114
|
readonly lines: readonly CodeLine[];
|
|
115
|
+
readonly annotation?: string;
|
|
97
116
|
readonly caption?: string;
|
|
98
117
|
readonly mark_label?: string;
|
|
99
118
|
};
|
|
119
|
+
readonly quote?: {
|
|
120
|
+
readonly text: string;
|
|
121
|
+
readonly source?: string;
|
|
122
|
+
};
|
|
123
|
+
readonly terms?: readonly Term[];
|
|
124
|
+
readonly answer_notes?: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
100
125
|
readonly references?: {
|
|
101
126
|
readonly summary: string;
|
|
102
127
|
readonly entries: readonly {
|
|
@@ -135,7 +160,7 @@ interface Blocks {
|
|
|
135
160
|
type Card = {
|
|
136
161
|
readonly [F in (typeof PAGE_FIELDS)[number]]?: string;
|
|
137
162
|
} & Blocks & {
|
|
138
|
-
readonly body
|
|
163
|
+
readonly body?: string;
|
|
139
164
|
readonly rules?: readonly RuleRef[];
|
|
140
165
|
readonly unit?: Unit;
|
|
141
166
|
readonly readings?: {
|
package/docs/testing.md
CHANGED
|
@@ -149,3 +149,15 @@ file matches, and it transpiles without type-checking, so `npm run check` runs
|
|
|
149
149
|
really occur stays, with the test that reaches it. `scripts/coverage-config.test.ts` fails on any
|
|
150
150
|
coverage-ignore comment and pins `.c8rc.json`'s `exclude` list.
|
|
151
151
|
7. **Say what an assertion guards** in a comment directly above it (`// Guards: …`).
|
|
152
|
+
8. **A browser test finds an element by `data-test` and reads its values from `data-test-meta`.**
|
|
153
|
+
The page marks every element a test touches with `data-test="<kind>"` and puts the element's
|
|
154
|
+
values in `data-test-meta` as JSON (`{"line":279,"marked":true}`). The test sets
|
|
155
|
+
`selectors.setTestIdAttribute("data-test")`, locates only with `getByTestId`, parses the meta and
|
|
156
|
+
compares it with one `deepEqual`. CSS classes stay for styling and visible text stays for people,
|
|
157
|
+
so a restyle or a reworded label does not turn a test red. Text that IS the content (an
|
|
158
|
+
annotation, a quote) is compared with the fixture's value, never with a literal typed into the
|
|
159
|
+
test. Accessibility state (`aria-expanded`) may be asserted on purpose, but it never locates
|
|
160
|
+
anything. `playwright/no-raw-locators`, `playwright/prefer-locator`, `playwright/no-wait-for-selector`
|
|
161
|
+
and `playwright/no-eval` hold the locating half; the older browser tests sit in
|
|
162
|
+
`eslint-suppressions.json`, which may only shrink. Example: `annotatedFacts` and
|
|
163
|
+
`annotatedExpected` in `skills/build-label-page/scripts/label-page-browser.test.ts`.
|
package/eslint.config.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import jsReasonRules from "#eslint-rules/js-needs-reason";
|
|
|
19
19
|
|
|
20
20
|
import portRules from "#eslint-rules/install-path-literals";
|
|
21
21
|
import n from "eslint-plugin-n";
|
|
22
|
+
import playwright from "eslint-plugin-playwright";
|
|
22
23
|
import tseslint from "typescript-eslint";
|
|
23
24
|
import boundaries from "eslint-plugin-boundaries";
|
|
24
25
|
import functional from "eslint-plugin-functional";
|
|
@@ -697,6 +698,22 @@ export default [
|
|
|
697
698
|
files: TEST_FILES,
|
|
698
699
|
rules: { "max-lines-per-function": ["error", MAX_LINES_TESTS] },
|
|
699
700
|
},
|
|
701
|
+
// Browser tests find an element by its test id (`data-test`, set in the page and in
|
|
702
|
+
// `selectors.setTestIdAttribute`) and read its values from `data-test-meta`: a CSS selector or a
|
|
703
|
+
// visible text breaks when the look changes. The rules fire under vitest as well as under
|
|
704
|
+
// Playwright's own runner (measured on label-page-browser.test.ts and candidate_page.test.ts).
|
|
705
|
+
// The plugin's `testIdAttribute` option belongs to `prefer-native-locators`, which this block does
|
|
706
|
+
// not enable, so there is nothing to configure.
|
|
707
|
+
{
|
|
708
|
+
files: TEST_FILES,
|
|
709
|
+
plugins: { playwright },
|
|
710
|
+
rules: {
|
|
711
|
+
"playwright/no-raw-locators": "error",
|
|
712
|
+
"playwright/prefer-locator": "error",
|
|
713
|
+
"playwright/no-wait-for-selector": "error",
|
|
714
|
+
"playwright/no-eval": "error",
|
|
715
|
+
},
|
|
716
|
+
},
|
|
700
717
|
// 🔴 DEBT, NOT A PERMISSION. The novelty-check scripts and their harnesses were moved from a consumer
|
|
701
718
|
// repository AS THEY WERE (the 2026-11-02 decision): untyped JSON in, `any` throughout. Making them pass the
|
|
702
719
|
// type-safety rules below means rewriting them, which is paperlint#339 (TypeScript rewrite of the Python
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paperlint",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.4.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",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"eslint-plugin-boundaries": "^7.2.0",
|
|
56
56
|
"eslint-plugin-functional": "^10.0.1",
|
|
57
57
|
"eslint-plugin-n": "^18.3.0",
|
|
58
|
+
"eslint-plugin-playwright": "^2.12.2",
|
|
58
59
|
"eslint-plugin-project-structure": "^3.14.5",
|
|
59
60
|
"eslint-plugin-sonarjs": "^4.2.1",
|
|
60
61
|
"jsonc-parser": "^3.3.1",
|
|
@@ -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:ed201d81c5934675 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
|
|
|
@@ -57,7 +57,7 @@ building. In one line each:
|
|
|
57
57
|
passing proves nothing about this.
|
|
58
58
|
2. **Navigation never relies on in-page scrolling or toasts.** "Next unanswered" is a filter: it hides
|
|
59
59
|
every finished item and group, and a note at the top lists what is left and what each still needs.
|
|
60
|
-
3. **An item is done only when every question on it is answered.** The card
|
|
60
|
+
3. **An item is done only when every question on it is answered.** The card asks the missing
|
|
61
61
|
question; the progress counter counts complete items only.
|
|
62
62
|
4. **Blind.** Neutral ids (`G001`…), order shuffled with a fixed seed, no model answers, verdicts or
|
|
63
63
|
source labels on the page. The generator refuses to build when an item carries a field outside an
|
|
@@ -106,8 +106,10 @@ building. In one line each:
|
|
|
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
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,
|
|
110
|
-
|
|
109
|
+
the generic content blocks (a code excerpt with a marked line, a note under it and an extra
|
|
110
|
+
gutter column; the words asked about as a quote; tap-to-see notes on its terms; a sub-line under
|
|
111
|
+
each answer; references, a list, a table, none of them folded), every word on them from
|
|
112
|
+
`items.json`, with the question right under the excerpt. `"preset":
|
|
111
113
|
"linter-config-witness"` is a narrow preset for comparing linter configs; the blocks underneath
|
|
112
114
|
are generic. Written by hand, the page meets the requirements above.
|
|
113
115
|
4. **Read it as the labeller, then look at it.** Read the page text once as a person who knows
|
|
@@ -68,7 +68,7 @@ building. In one line each:
|
|
|
68
68
|
passing proves nothing about this.
|
|
69
69
|
2. **Navigation never relies on in-page scrolling or toasts.** "Next unanswered" is a filter: it hides
|
|
70
70
|
every finished item and group, and a note at the top lists what is left and what each still needs.
|
|
71
|
-
3. **An item is done only when every question on it is answered.** The card
|
|
71
|
+
3. **An item is done only when every question on it is answered.** The card asks the missing
|
|
72
72
|
question; the progress counter counts complete items only.
|
|
73
73
|
4. **Blind.** Neutral ids (\`G001\`…), order shuffled with a fixed seed, no model answers, verdicts or
|
|
74
74
|
source labels on the page. The generator refuses to build when an item carries a field outside an
|
|
@@ -117,8 +117,10 @@ building. In one line each:
|
|
|
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
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,
|
|
121
|
-
|
|
120
|
+
the generic content blocks (a code excerpt with a marked line, a note under it and an extra
|
|
121
|
+
gutter column; the words asked about as a quote; tap-to-see notes on its terms; a sub-line under
|
|
122
|
+
each answer; references, a list, a table, none of them folded), every word on them from
|
|
123
|
+
\`items.json\`, with the question right under the excerpt. \`"preset":
|
|
122
124
|
"linter-config-witness"\` is a narrow preset for comparing linter configs; the blocks underneath
|
|
123
125
|
are generic. Written by hand, the page meets the requirements above.
|
|
124
126
|
4. **Read it as the labeller, then look at it.** Read the page text once as a person who knows
|
|
@@ -41,8 +41,8 @@ filter works in any frame, because the next thing to answer is simply the first
|
|
|
41
41
|
|
|
42
42
|
## 3. An item counts as done only when every question on it is answered
|
|
43
43
|
|
|
44
|
-
**Do.** Done means all questions answered. The card
|
|
45
|
-
|
|
44
|
+
**Do.** Done means all questions answered. The card asks the one still missing in its question
|
|
45
|
+
box, and "Next unanswered" lists what each item still needs. The progress counter counts complete items only, so "7 of 12" means 7 items with every
|
|
46
46
|
question answered. A group is finished when all its items are and its familiarity question (rule 5)
|
|
47
47
|
is answered.
|
|
48
48
|
|
|
@@ -50,7 +50,7 @@ is answered.
|
|
|
50
50
|
and surfaces weeks later as a gap in the data. A counter that counts partial items reports progress
|
|
51
51
|
that the analysis cannot use.
|
|
52
52
|
|
|
53
|
-
**Check.** Answer one question of a two-question item: the counter stays put and the card
|
|
53
|
+
**Check.** Answer one question of a two-question item: the counter stays put and the card asks the
|
|
54
54
|
other question.
|
|
55
55
|
|
|
56
56
|
## 4. Blind: the page tells the labeller nothing about the answer or the source
|
|
@@ -21,7 +21,7 @@ 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 (folded into a toggle once the practice is done), the practice before
|
|
24
|
+
the "How to answer" screen (folded into a toggle once the practice is done; it is page-level, not part of any card), the practice before
|
|
25
25
|
the real items, one question per screen, each card's place (`labels.place`, by default
|
|
26
26
|
"G007 · 7 of 20"), the card just
|
|
27
27
|
answered kept under "Next unanswered" until the next answer (so a mis-tap can be changed), and a
|
|
@@ -111,7 +111,7 @@ Any card may carry these blocks, on any `kind`, and each one is optional. The pa
|
|
|
111
111
|
"mark_label": "◀ the line in question", // optional; default «◀ this line»
|
|
112
112
|
},
|
|
113
113
|
"references": {
|
|
114
|
-
"summary": "What the guideline says", // the
|
|
114
|
+
"summary": "What the guideline says", // the block's heading
|
|
115
115
|
"entries": [
|
|
116
116
|
{
|
|
117
117
|
"label": "Guideline", // optional row label
|
|
@@ -155,24 +155,61 @@ Any card may carry these blocks, on any `kind`, and each one is optional. The pa
|
|
|
155
155
|
}
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
-
- **Order on the card.**
|
|
159
|
-
1. its place (`labels.place`)
|
|
160
|
-
2. the
|
|
161
|
-
3. the
|
|
162
|
-
4. the
|
|
163
|
-
5. the
|
|
164
|
-
6. the
|
|
165
|
-
7.
|
|
166
|
-
8.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
158
|
+
- **Order on the card.** Each card is a bordered panel, with wide space before the next. It shows, in this order:
|
|
159
|
+
1. its place (`labels.place`), then its `title`;
|
|
160
|
+
2. the `quote` and its source;
|
|
161
|
+
3. the body, on a `labels.body` row when that label is set;
|
|
162
|
+
4. the repeat sentence;
|
|
163
|
+
5. the older fields (`code`, `unit`, `rules`, `readings`, `file`, `context`);
|
|
164
|
+
6. the situation's summary;
|
|
165
|
+
7. the excerpt;
|
|
166
|
+
8. **the question box**: the question, its answers with their `answer_notes`, and the wait bar of `min_seconds`;
|
|
167
|
+
9. the reason box of an answer with `ask_why`, then the status line. The status line never lists
|
|
168
|
+
what is missing (the question box asks it): it shows «Saved <time>», «Saving…», why a save did not happen, or nothing;
|
|
169
|
+
10. `references`, `more` and `table`, each in place under its heading. Nothing on a card is folded:
|
|
170
|
+
a labeller who has to open a block to answer reads half a card (#392).
|
|
171
|
+
|
|
172
|
+
A card is read once, top to bottom: the words asked about, the code they are about, the question
|
|
173
|
+
right under that code, its answers. A card without a `quote` has the same order without step 2.
|
|
174
|
+
|
|
175
|
+
- **`situation`**: `{summary?, question}`.
|
|
176
|
+
- `summary` is one sentence above the excerpt; optional.
|
|
172
177
|
- `question` replaces the text of the page's first question on this card. Other questions keep their own text.
|
|
173
178
|
- 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
179
|
- `{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 `▶`
|
|
180
|
+
- **`excerpt`**: at least one line is marked (`mark: true`). The marked line gets a `▶` before its number and keeps its background, so it reads as marked even without its colour; after its code comes `mark_label`, unless the excerpt has an `annotation`. A line's `text` may be empty, for a blank line.
|
|
181
|
+
- `lang` follows the file name: «path · Python».
|
|
182
|
+
- Where two lines' numbers are not consecutive, a `⋮` row says what is left out (`labels.hidden`, `labels.hidden_one`). A line with `context: true` is drawn faint: one shown only to place the code, such as an enclosing block's opener.
|
|
183
|
+
- `gutter_header` and each line's `gutter` add a column between the number and the code: one word on top, one value per line («depth», `4`). The values are the consumer's, never computed by the page.
|
|
184
|
+
- `annotation` is one short fact in a box right under the first marked line, inside the code, with an arrow pointing at the line, as a code-review comment sits.
|
|
185
|
+
|
|
186
|
+
### The approved card: a quote, notes on terms, answers that say what they mean
|
|
187
|
+
|
|
188
|
+
`scripts/fixtures/v7-cards-items.json` is a page of three such cards (#392). The fields:
|
|
189
|
+
|
|
190
|
+
```jsonc
|
|
191
|
+
{
|
|
192
|
+
"quote": { "text": "Docstrings use Google-style format", "source": "`.github/copilot-instructions.md`, line 67" },
|
|
193
|
+
"situation": { "question": "Do these words forbid line 48?" },
|
|
194
|
+
"excerpt": { "file": "src/tables/merge.py", "lang": "Python", "annotation": "Ruff rule `D413` reports: …", "lines": [ … ] },
|
|
195
|
+
"terms": [
|
|
196
|
+
{
|
|
197
|
+
"term": "Google-style", // the card's own words that open the note
|
|
198
|
+
"note": [
|
|
199
|
+
// one or more entries; each has `quote` or `code` (verbatim), and its source link
|
|
200
|
+
{ "title": "Google style", "quote": "Each section begins with a heading line, which ends with a colon.",
|
|
201
|
+
"example": "(e.g. `Args:`)", "url": "https://google.github.io/styleguide/pyguide.html", "url_label": "Google Python Style Guide" },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
"answer_notes": { "forbid": { "forbid": "like a config with rule D413 on, which flags it" } }, // question → answer → sub-line
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
- **`quote`** shows the words being asked about at the top of the card, above its code, `source` as a small line under them. A card needs `body` or `quote`.
|
|
210
|
+
- **`terms`**: each `term` must occur in the quote, its source or the annotation, inside one backtick span or outside all of them; the build refuses one it cannot find there, and a note entry with neither `quote` nor `code`. A term opens from its first occurrence in the quote, else the source, else the annotation. On the page it is a dotted button: a tap (or hover, where there is hover) opens one floating note anchored to it, at most 280 px wide (300 with a snippet), flipped above the term when there is no room below, kept inside the viewport. A tap outside closes it and does nothing else, so it never answers a card; so does Esc. Answer buttons carry no terms.
|
|
211
|
+
- **`answer_notes`** puts a sub-line under an answer's label on this card: what the answer means here. A question or an answer the page does not have is refused.
|
|
212
|
+
- Whether a note pushes the labeller toward one answer is not checked: the consumer lists each note for the study's author.
|
|
176
213
|
- **`references.entries[].fields`**:
|
|
177
214
|
- A field without `style` is a paragraph («label: text» when it has a label). A long one shows three lines and a «more».
|
|
178
215
|
- `style: "code"` is a code block on a `label` row.
|
|
@@ -189,6 +226,9 @@ Any card may carry these blocks, on any `kind`, and each one is optional. The pa
|
|
|
189
226
|
| `line_part` | the `unit` row's label when the line holds several requirements | `{line} · part {part} of {of}` |
|
|
190
227
|
| `repeat` | the repeat sentence | `Card {index} of {of}` |
|
|
191
228
|
| `practice_example` | text before each answer's example on a practice button | empty |
|
|
229
|
+
| `hidden` | a `⋮` row for lines left out | `lines {from}–{to}` |
|
|
230
|
+
| `hidden_one` | a `⋮` row for one line left out | `line {from}` |
|
|
231
|
+
| `wait` | the bar under the answers while `min_seconds` runs | `⏳ You can answer in {s} s` |
|
|
192
232
|
|
|
193
233
|
- The leak guard reads every string these blocks render, one string at a time.
|
|
194
234
|
|
|
@@ -199,7 +239,7 @@ A preset is opt-in. An items file names it with `"preset": "<name>"`. The preset
|
|
|
199
239
|
- **`linter-config-witness`** is narrow: two linter configs of one requirement disagree on a line of code. The blocks underneath are generic.
|
|
200
240
|
- **Input fields on a card:**
|
|
201
241
|
- `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,
|
|
242
|
+
- `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, under «What the linter rule does (official docs)».
|
|
203
243
|
- `more_like_this: {count, files}`, which becomes the list «N more file(s) with the same situation».
|
|
204
244
|
- `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
245
|
- **Its labels**: «Card {n} of {total}», «Requirement», «Card {index} of {of} for this requirement», and «Example of this answer, about other code: ».
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
{
|
|
2
|
+
"study": "demo-words-forbid",
|
|
3
|
+
"kind": "labels",
|
|
4
|
+
"title": "Do the words forbid it",
|
|
5
|
+
"seed": 3,
|
|
6
|
+
"min_seconds": 10,
|
|
7
|
+
"groups": { "all": "Cards" },
|
|
8
|
+
"labels": { "place": "Card {n} of {total}", "wait": "Save · available after {s} seconds on this card" },
|
|
9
|
+
"questions": [
|
|
10
|
+
{
|
|
11
|
+
"id": "forbid",
|
|
12
|
+
"text": "Do these words forbid the marked line?",
|
|
13
|
+
"short": "do the words forbid it?",
|
|
14
|
+
"options": [
|
|
15
|
+
{ "value": "forbid", "label": "Forbid", "icon": "⛔", "example": "«No print calls» and the line print(x)" },
|
|
16
|
+
{ "value": "allow", "label": "Allow", "icon": "✅", "example": "«No print calls» and the line log.info(x)" },
|
|
17
|
+
{ "value": "unsettled", "label": "The words don't settle it", "icon": "↔️", "example": "«Keep functions short» and a 30-line function" },
|
|
18
|
+
{ "value": "elsewhere", "label": "The words are about something else", "icon": "🔀", "example": "«Use tabs» and a line about a database query" },
|
|
19
|
+
{ "value": "cannot", "label": "I can't judge (say why)", "icon": "❔", "example": "a line in a language you do not read", "ask_why": "What stops you?" }
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"practice": [
|
|
24
|
+
{
|
|
25
|
+
"quote": { "text": "No print calls in library code", "source": "`CONTRIBUTING.md`, line 4" },
|
|
26
|
+
"situation": { "question": "Do these words forbid line 3?" },
|
|
27
|
+
"excerpt": { "file": "lib/sum.py", "lang": "Python", "lines": [
|
|
28
|
+
{ "n": 2, "text": "def total(xs):", "mark": false },
|
|
29
|
+
{ "n": 3, "text": " print(xs)", "mark": true }
|
|
30
|
+
] },
|
|
31
|
+
"expected": { "forbid": "forbid" }, "why": "print is a print call, and lib/ is library code."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"quote": { "text": "No print calls in library code", "source": "`CONTRIBUTING.md`, line 4" },
|
|
35
|
+
"situation": { "question": "Do these words forbid line 3?" },
|
|
36
|
+
"excerpt": { "file": "lib/sum.py", "lang": "Python", "lines": [
|
|
37
|
+
{ "n": 2, "text": "def total(xs):", "mark": false },
|
|
38
|
+
{ "n": 3, "text": " log.debug(xs)", "mark": true }
|
|
39
|
+
] },
|
|
40
|
+
"expected": { "forbid": "allow" }, "why": "A log call is not a print call."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"quote": { "text": "Use tabs for indentation", "source": "`STYLE.md`, line 2" },
|
|
44
|
+
"situation": { "question": "Do these words forbid line 8?" },
|
|
45
|
+
"excerpt": { "file": "db/query.py", "lang": "Python", "lines": [
|
|
46
|
+
{ "n": 8, "text": "rows = db.execute(sql)", "mark": true }
|
|
47
|
+
] },
|
|
48
|
+
"expected": { "forbid": "elsewhere" }, "why": "The words are about indentation; the line is a query."
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"items": [
|
|
52
|
+
{
|
|
53
|
+
"source_id": "card-docstring", "group": "all",
|
|
54
|
+
"quote": { "text": "Docstrings use Google-style format", "source": "`.github/copilot-instructions.md`, line 67" },
|
|
55
|
+
"situation": { "question": "Do these words forbid line 48?" },
|
|
56
|
+
"excerpt": {
|
|
57
|
+
"file": "src/tables/merge.py", "lang": "Python",
|
|
58
|
+
"annotation": "Ruff rule `D413` reports: “Missing blank line after last section (\"Returns\")”",
|
|
59
|
+
"lines": [
|
|
60
|
+
{ "n": 37, "text": "def merge_tables(", "mark": false, "context": true },
|
|
61
|
+
{ "n": 40, "text": " \"\"\"", "mark": false },
|
|
62
|
+
{ "n": 41, "text": " Merge the tables picked by the flags.", "mark": false },
|
|
63
|
+
{ "n": 42, "text": "", "mark": false },
|
|
64
|
+
{ "n": 43, "text": " picks: For each table name, whether to keep its rows of each kind", "mark": false },
|
|
65
|
+
{ "n": 44, "text": " (kept, dropped), and which of the two to merge first.", "mark": false },
|
|
66
|
+
{ "n": 45, "text": " tables: Every table that can be merged.", "mark": false },
|
|
67
|
+
{ "n": 46, "text": " Columns needed: \"name\", \"kind\"", "mark": false },
|
|
68
|
+
{ "n": 47, "text": "", "mark": false },
|
|
69
|
+
{ "n": 48, "text": " Returns:", "mark": true },
|
|
70
|
+
{ "n": 49, "text": " One table with the rows of every table picked.", "mark": false },
|
|
71
|
+
{ "n": 50, "text": " \"\"\"", "mark": false },
|
|
72
|
+
{ "n": 51, "text": " kinds_needed = {", "mark": false }
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
"terms": [
|
|
76
|
+
{ "term": "Ruff", "note": [{ "quote": "An extremely fast Python linter and code formatter, written in Rust.", "url": "https://docs.astral.sh/ruff/", "url_label": "Ruff docs" }] },
|
|
77
|
+
{ "term": "D413", "note": [{ "title": "Ruff rule D413", "quote": "Checks for missing blank lines after the last section of a multiline docstring", "url": "https://docs.astral.sh/ruff/rules/missing-blank-line-after-last-section/", "url_label": "docs" }] },
|
|
78
|
+
{ "term": "Google-style", "note": [
|
|
79
|
+
{ "title": "Google style", "quote": "Each section begins with a heading line, which ends with a colon.", "example": "(e.g. `Args:`, `Returns:`, `Raises:`)", "url": "https://google.github.io/styleguide/pyguide.html#383-functions-and-methods", "url_label": "Google Python Style Guide, 3.8.3" },
|
|
80
|
+
{ "title": "NumPy style, the other common one", "quote": "Each heading should be underlined in hyphens", "example": "(e.g. `Returns` with `-------` on the line under it)", "url": "https://numpydoc.readthedocs.io/en/latest/format.html#sections", "url_label": "numpydoc format guide" }
|
|
81
|
+
] },
|
|
82
|
+
{ "term": ".github/copilot-instructions.md", "note": [{ "quote": "Repository-wide custom instructions apply to all requests made in the context of a repository.", "url": "https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/add-custom-instructions/add-repository-instructions", "url_label": "GitHub Docs" }] }
|
|
83
|
+
],
|
|
84
|
+
"answer_notes": { "forbid": { "forbid": "like a config with rule D413 on, which flags it", "allow": "like a config without D413, which passes it" } }
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"source_id": "card-call", "group": "all",
|
|
88
|
+
"quote": { "text": "Use cachedElement('elementId') instead of document.getElementById", "source": "`AGENTS.md`, line 7" },
|
|
89
|
+
"situation": { "question": "Do these words forbid line 22?" },
|
|
90
|
+
"excerpt": {
|
|
91
|
+
"file": "src/dom/cache.ts", "lang": "TypeScript",
|
|
92
|
+
"annotation": "calls `document.getElementById`",
|
|
93
|
+
"lines": [
|
|
94
|
+
{ "n": 9, "text": "export const cachedElement = (id: string) => {", "mark": false, "context": true },
|
|
95
|
+
{ "n": 19, "text": " return hit", "mark": false },
|
|
96
|
+
{ "n": 20, "text": " }", "mark": false },
|
|
97
|
+
{ "n": 21, "text": "", "mark": false },
|
|
98
|
+
{ "n": 22, "text": " const el = document.getElementById(id)", "mark": true },
|
|
99
|
+
{ "n": 23, "text": "", "mark": false },
|
|
100
|
+
{ "n": 24, "text": " if (!el) {", "mark": false },
|
|
101
|
+
{ "n": 25, "text": " throw new TypeError(`No element with id \"${id}\"`)", "mark": false }
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
"terms": [
|
|
105
|
+
{ "term": "cachedElement", "note": [{ "says": "this project's own function, defined at line 9 of the file above; its comment reads", "quote": "A cache for DOM elements", "url": "https://example.com/src/dom/cache.ts#L3-L9", "url_label": "src/dom/cache.ts" }] },
|
|
106
|
+
{ "term": "document.getElementById", "note": [{ "quote": "returns an Element object representing the element whose id property matches the specified string", "url": "https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById", "url_label": "MDN" }] },
|
|
107
|
+
{ "term": "AGENTS.md", "note": [{ "quote": "a dedicated, predictable place to provide the context and instructions to help AI coding agents work on your project.", "url": "https://agents.md/", "url_label": "agents.md" }] }
|
|
108
|
+
],
|
|
109
|
+
"answer_notes": { "forbid": { "forbid": "like a config that bans this call and checks this file", "allow": "like a config that bans this call but doesn't check this file" } }
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"source_id": "card-depth", "group": "all",
|
|
113
|
+
"quote": { "text": "Avoid deep nesting (use early returns and guard clauses)", "source": "`CLAUDE.md`, line 85" },
|
|
114
|
+
"situation": { "question": "Do these words forbid line 279?" },
|
|
115
|
+
"excerpt": {
|
|
116
|
+
"file": "src/index/FileIndex.ts", "lang": "TypeScript", "gutter_header": "depth",
|
|
117
|
+
"annotation": "nested 4 blocks deep",
|
|
118
|
+
"lines": [
|
|
119
|
+
{ "n": 22, "text": "export class FileIndex {", "mark": false, "context": true },
|
|
120
|
+
{ "n": 257, "text": " private onDeleted(uri: Uri): void {", "mark": false, "context": true },
|
|
121
|
+
{ "n": 262, "text": " try {", "mark": false, "context": true, "gutter": "1" },
|
|
122
|
+
{ "n": 276, "text": " if (entry.isDir) {", "mark": false, "gutter": "2" },
|
|
123
|
+
{ "n": 277, "text": " const prefix = `${rel}/`;", "mark": false },
|
|
124
|
+
{ "n": 278, "text": " for (const key of this.entries.keys()) {", "mark": false, "gutter": "3" },
|
|
125
|
+
{ "n": 279, "text": " if (key.startsWith(prefix)) {", "mark": true, "gutter": "4" },
|
|
126
|
+
{ "n": 280, "text": " this.entries.delete(key);", "mark": false },
|
|
127
|
+
{ "n": 281, "text": " }", "mark": false },
|
|
128
|
+
{ "n": 282, "text": " }", "mark": false }
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
"terms": [
|
|
132
|
+
{ "term": "guard clauses", "note": [{ "code": "function price(order) {\n if (!order) return 0;\n if (order.free) return 0;\n if (order.staff) return staffPrice(order);\n return listPrice(order);\n}", "url": "https://example.com/style-guide#guard-clauses", "url_label": "Style guide (example): guard clauses" }] },
|
|
133
|
+
{ "term": "CLAUDE.md", "note": [{ "quote": "CLAUDE.md files are markdown files that give Claude persistent instructions for a project, your personal workflow, or your entire organization.", "url": "https://code.claude.com/docs/en/memory", "url_label": "Claude Code docs" }] }
|
|
134
|
+
],
|
|
135
|
+
"answer_notes": { "forbid": { "forbid": "like a config with limit 3, which flags it", "allow": "like ESLint's default, limit 4, which passes it" } }
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
}
|