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 CHANGED
@@ -1,4 +1,4 @@
1
- <!-- vigiles:sha256:58c66e374c084cc5 compiled from CLAUDE.md.spec.ts -->
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: string;
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: string;
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.2.0",
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:dce0979d7f187ae7 compiled from skills/build-label-page/SKILL.md.spec.ts -->
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 names the missing
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, references, a list, a table), every
110
- word on them from `items.json`, with the question right under the excerpt. `"preset":
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 names the missing
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, references, a list, a table), every
121
- word on them from \`items.json\`, with the question right under the excerpt. \`"preset":
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 shows which one is missing ("Still needs:
45
- boundary"). The progress counter counts complete items only, so "7 of 12" means 7 items with every
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 names the
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 folded block's toggle
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.** 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.
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 `▶` 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.
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, folded under «What the linter rule does (official docs)».
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
+ }