tackbox 0.1.65 → 0.1.67

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/README.md CHANGED
@@ -24,9 +24,12 @@ it's written: hooked into the agent's edit loop it flags the finding
24
24
  before the turn ends, and the same rules gate pre-commit and CI -
25
25
  one coverage bar for hand-written and agent-written code.
26
26
 
27
- There are no per-rule disable flags. A local exception stays visible as
28
- a reasoned `// no-report: <reason>` marker at the site, and the agent
29
- hook asks for your approval before a new suppression lands.
27
+ There are no per-rule disable flags. A local exception stays visible
28
+ as a reasoned `// no-report: <reason>` marker at the site, and every
29
+ marker must be covered by a line in the committed approval manifest
30
+ (`.tackbox/approvals`). Adding that line is the act that draws the
31
+ approval ask in an agent session; an uncovered marker keeps lint,
32
+ `dev.py check`, and CI red until it is approved or reverted.
30
33
 
31
34
  ```go
32
35
  resp, err := client.Do(req)
@@ -138,6 +141,11 @@ The `path` scope and the change filters compose:
138
141
  This scope filter is unrelated to the `escapes` command's `--since`
139
142
  `<rev>`, which selects inventory entries new against a revision.
140
143
 
144
+ The approvals consistency check (see Approval manifest) is exempt
145
+ from all scope filters: it always covers the whole tree and reports
146
+ under an `approvals (whole tree):` header, scoped runs included - a
147
+ scoped CI lint cannot scope the wall away.
148
+
141
149
  ## Exit codes
142
150
 
143
151
  Across commands, `2` is a usage or setup error the command cannot run
@@ -150,11 +158,17 @@ past (argparse misuse, and the per-command cases below).
150
158
  changes the code.
151
159
  - **doctor** - `0` all checks pass, `1` at least one failed; every
152
160
  check always runs (no short-circuit).
153
- - **hook** - `0` a no-op, a clean re-lint, or a JSON decision (a
161
+ - **approvals** - `0` consistent, `2` inconsistent (uncovered
162
+ markers, orphaned entries, or unresolvable files), `1` infra.
163
+ `--draft` is a generator, not a gate: `0` when every uncovered
164
+ marker was drafted (an orphan-only tree included), `2` only when
165
+ unresolvable files leave the draft incomplete.
166
+ - **hook** - `0` a no-op, a clean event, or a JSON decision (a
154
167
  PreToolUse approval prompt or a PostToolUse Bash block); `1` a
155
168
  non-blocking infra error (unreadable stdin, a git failure); `2` a
156
- PostToolUse finding on the edited lines or a non-compiling Go
157
- package, which blocks the edit in-loop.
169
+ PostToolUse finding on the edited lines, a non-compiling Go
170
+ package, or an approvals inconsistency anywhere in the worktree,
171
+ which blocks the edit in-loop.
158
172
  - **escapes** - `0` whenever it runs, entries or not (an inventory,
159
173
  not a gate); `1` only for a bad `--since` rev.
160
174
 
@@ -339,7 +353,9 @@ By design, the ruleset is a single non-negotiable bundle. There are
339
353
  no flags to disable individual rules. Suppressing a finding requires
340
354
  the explicit per-site marker (`// no-report`, `// parse-skip`,
341
355
  `// nil-return`, `// test-skip`, `// dup-ok`) with a reason of at
342
- least 10 characters - non-empty was too cheap (`ok` / `todo` passed).
356
+ least 10 characters - non-empty was too cheap (`ok` / `todo` passed) -
357
+ plus a covering line in the approval manifest below: the reason
358
+ explains the exception, the manifest line records its approval.
343
359
 
344
360
  Capture helpers are recognized by origin, not by name: a Go call
345
361
  counts only when its callee resolves (type info / import) to the
@@ -348,7 +364,7 @@ counts only when its callee resolves (type info / import) to the
348
364
  `nl.tsym.tackbox.report.Report` call or a known logger sink (e.g.
349
365
  slf4j, `java.lang.System.Logger`) at `ERROR` / `WARNING` - tier-1.
350
366
  Every language also honors a function declared in a repo-root
351
- `.tackbox-reporters` file (`file#function: reason`) - tier-2. A
367
+ `.tackbox/reporters` file (`file#function: reason`) - tier-2. A
352
368
  declaration names a report sink - it is not an exclude: it disables no
353
369
  rule, and a declared call is honored only when the caught error flows
354
370
  into its arguments. Python resolves tier-1 by import origin too (D010),
@@ -370,6 +386,77 @@ on a non-Go file is rejected - a dead line would be silent. The format
370
386
  is language-uniform; the restriction lifts as other engines adopt the
371
387
  contract.
372
388
 
389
+ ### Suppression marker forms
390
+
391
+ Every marker is `<keyword>: <reason>` carried by the language's
392
+ ordinary comment token:
393
+
394
+ - Go, Java, JS/TS: a `//` line comment. Block comments are never
395
+ markers; the one exception is `dup-ok`, where the duplication
396
+ engine also accepts a standalone single-line `/* ... */` (see its
397
+ section).
398
+ - Python: a `#` comment.
399
+ - Markdown: the `tackbox: lang=` HTML comment (see the Markdown
400
+ engine section) is the only Markdown marker.
401
+ - Svelte: inside `<script>` blocks the `//` form works as in JS/TS;
402
+ the template adds two forms - a `//` comment inside a `{...}`
403
+ expression (line-adjacent, as ever) and an HTML comment
404
+ immediately above an element:
405
+
406
+ ```text
407
+ <!-- no-report: inline handler failure is tolerated here -->
408
+ <button onclick={...}>go</button>
409
+ ```
410
+
411
+ The HTML-comment form suppresses within that whole element
412
+ (deliberately wider than line adjacency: an inline handler can span
413
+ lines - D011 A8) and stops at the element boundary; following
414
+ siblings still report. `<style>` content takes no markers.
415
+
416
+ Placement per rule (above the `try`, standalone above the block,
417
+ directly above the statement) is each rule's own contract; the
418
+ 10-character reason floor (D009) is universal.
419
+
420
+ ## Approval manifest
421
+
422
+ Suppression markers are approved in one committed file,
423
+ `.tackbox/approvals` at the repo root - one line per approved
424
+ occurrence: an address (file plus named-scope chain) and the exact
425
+ marker text.
426
+
427
+ ```text
428
+ py/app/svc.py#Handler.process: no-report: legacy path, covered upstream
429
+ js/src/boot.ts#init.<h4f2a9c1e>: no-report: splash fallback, reported upstream
430
+ tools/gen.py: parse-skip: config validated upstream
431
+ ```
432
+
433
+ The chain walks functions, classes, methods, or Markdown headings,
434
+ joined by `.`; an entry with no `#` sits at file scope. Anonymous
435
+ scopes (lambdas, arrows, IIFEs) appear as 8-hex content hashes; Java
436
+ overloads carry a parameter-type signature; same-name siblings take
437
+ an `@k` ordinal. Repeat the line for each identical occurrence.
438
+
439
+ The check is bidirectional and always covers the whole tree: a
440
+ marker without a covering entry and an entry without a live marker
441
+ (an orphan) are both findings, reported by `tackbox lint` under the
442
+ `approvals (whole tree):` header whatever the lint scope.
443
+ `tackbox approvals` runs the same check standalone;
444
+ `tackbox approvals --draft` prints a ready entry line for every
445
+ uncovered marker - the address is computed for you, so approving a
446
+ marker you just wrote is one append away, and bootstrapping a repo
447
+ that already carries markers is: generate, review line by line,
448
+ commit.
449
+
450
+ Approving is adding the line. In an agent session the edit that adds
451
+ a manifest line draws the PreToolUse ask quoting the entry (several
452
+ lines in one edit draw one all-or-nothing ask), so the only route to
453
+ a green check passes through a visible diff and a human decision.
454
+ Writing a marker itself never asks - by any channel, Edit or shell -
455
+ it merely leaves the tree inconsistent, which every later hook
456
+ event, `dev.py check`, and CI reports until the entry lands or the
457
+ marker is reverted. Removing a manifest line is free; a marker whose
458
+ text, scope, or count changes needs its entry updated the same way.
459
+
373
460
  ## Runtime reporting helpers
374
461
 
375
462
  Direct reporting helpers ship per language; their shared runtime behavior -
@@ -387,24 +474,23 @@ specified in [docs/report-contracts.md](docs/report-contracts.md).
387
474
  Claude Code hook event on stdin and dispatches by `hook_event_name`:
388
475
 
389
476
  - **PostToolUse** on an Edit/Write re-lints the edited file (Go: its
390
- package). On a finding it exits 2 with the finding on stderr, so the
391
- model sees it and fixes it in-loop. On a **Bash** command it instead
392
- diffs the whole worktree against HEAD and blocks if the command
393
- planted a new suppression marker (on a lintable file) or a new
394
- `.tackbox-reporters` line - containment for a marker a shell wrote
395
- behind the Edit gate. Stateless: HEAD is the approval record, so an
396
- approved marker stops asking once committed (worst case, a repeated
397
- question, never a silent pass). The authoritative gate stays
398
- pre-commit / CI.
399
- - **PreToolUse** asks for approval before a new suppression marker
400
- (`// no-report`, `// parse-skip`, `// nil-return`, `// test-skip`,
401
- `// dup-ok`) or a new `.tackbox-reporters` line lands;
402
- removing one is free.
403
-
404
- Both marker gates ask only about files an engine would lint (D012): a
405
- marker in a Go `testdata/` path or a non-lintable fixture extension
406
- (a `.java.txt`) is dead text and draws no question, while the
407
- `.tackbox-reporters` gate stays unconditional.
477
+ package). On a finding it exits 2 with the finding on stderr, so
478
+ the model sees it and fixes it in-loop. Every Post event - **Bash**
479
+ included - also runs the whole-tree approvals consistency check:
480
+ an unapproved marker, an orphaned entry, or an unresolvable file
481
+ blocks with the entry named and the fix - add the manifest line,
482
+ which asks, or revert. Stateless and tree-shaped: a commit changes
483
+ nothing, and the block repeats on every event until the tree is
484
+ consistent. The authoritative gate stays pre-commit / CI.
485
+ - **PreToolUse** asks for approval before a new `.tackbox/approvals`
486
+ line or a new `.tackbox/reporters` line lands; removing one is
487
+ free. Editing markers in code draws no Pre ask - the consistency
488
+ check owns them.
489
+
490
+ Only markers in files an engine would lint participate in the check
491
+ (D012): a marker in a Go `testdata/` path or a non-lintable fixture
492
+ extension (a `.java.txt`) is dead text - no entry needed, no
493
+ question - while the `.tackbox/reporters` gate stays unconditional.
408
494
 
409
495
  The hook is a no-op unless the edit's `cwd` is a git repo with a
410
496
  `dev.py` at its root. Wire it once, globally, in
@@ -438,7 +524,7 @@ enumerates:
438
524
  - **suppression markers** (`// no-report`, `// parse-skip`,
439
525
  `// nil-return`, `// long-comment`, `// test-skip`, `// dup-ok`, plus
440
526
  the markdown `tackbox: lang=` marker), each with its reason;
441
- - **`.tackbox-reporters` declarations** - the tier-2 sinks;
527
+ - **`.tackbox/reporters` declarations** - the tier-2 sinks;
442
528
  - **notify / quiet lane choices** - the call sites of the user-lane-only
443
529
  `notify` and the telemetry-only `quiet` verbs.
444
530
 
@@ -464,7 +550,7 @@ uvx tackbox@latest escapes --since origin/main --context 5
464
550
  "text": "no-report: central boundary already captures it",
465
551
  "reason": "central boundary already captures it",
466
552
  "context": ["...", "...", "..."]},
467
- {"kind": "reporter-decl", "file": ".tackbox-reporters", "line": 2,
553
+ {"kind": "reporter-decl", "file": ".tackbox/reporters", "line": 2,
468
554
  "text": "src/app/errors.py#report_api_error: the API sink",
469
555
  "context": ["..."]},
470
556
  {"kind": "notify-site", "file": "js/foo.js", "line": 40,
@@ -481,7 +567,7 @@ uvx tackbox@latest escapes --since origin/main --context 5
481
567
  kinds, even at zero, so consumers see a stable shape.
482
568
  - `since` echoes the `--since` rev, or `null`.
483
569
  - `text` is the trimmed source line; for a marker it runs from the marker
484
- keyword to end of line (the hook's own `_markers` extraction).
570
+ keyword to end of line.
485
571
  - `reason` (markers only) is what follows the keyword's colon, trimmed -
486
572
  possibly empty (the `tackbox: lang=` marker carries none).
487
573
  - `context` is the surrounding source, `--context N` lines each side
@@ -494,7 +580,7 @@ uvx tackbox@latest escapes --since origin/main --context 5
494
580
 
495
581
  The scan covers the same lintable source set the linter would scan (the
496
582
  D012 predicate: extension match plus each engine's path filter, so a Go
497
- `testdata/` file is out), plus the root `.tackbox-reporters` (every
583
+ `testdata/` file is out), plus the root `.tackbox/reporters` (every
498
584
  non-empty line is one declaration - the file has no comment syntax).
499
585
  notify / quiet call sites are detected **textually per language**
500
586
  (`report_quiet` / `notify` in Python, `reportQuiet` / `notify` in the JS
@@ -516,6 +602,7 @@ one stderr line, exit 1.
516
602
  ## Layout
517
603
 
518
604
  ```text
605
+ .tackbox/approvals # suppression-approval manifest
519
606
  dev.py # lint / test / e2e / check (dev-script)
520
607
  hygiene.py # dev.py lint hygiene (conflict/yaml/ws/newline)
521
608
  go.mod # Go module
@@ -42,7 +42,7 @@ function parseModule(file, code) {
42
42
  }
43
43
 
44
44
  // hasBinding: the AST defines a function or const-arrow named `name`. Existence
45
- // check for `.tackbox-reporters` symbol validation; a deep walk keeps it parser
45
+ // check for `.tackbox/reporters` symbol validation; a deep walk keeps it parser
46
46
  // shape-agnostic across espree / ts / svelte.
47
47
  function hasBinding(ast, name) {
48
48
  const seen = new Set()
@@ -86,16 +86,16 @@ function validateDeclarations(decls) {
86
86
  try {
87
87
  code = fs.readFileSync(abs, 'utf8')
88
88
  } catch (e) {
89
- throw new Error(`.tackbox-reporters: cannot read ${d.file}: ${e.message}`, { cause: e })
89
+ throw new Error(`.tackbox/reporters: cannot read ${d.file}: ${e.message}`, { cause: e })
90
90
  }
91
91
  let ast
92
92
  try {
93
93
  ast = parseModule(d.file, code)
94
94
  } catch (e) {
95
- throw new Error(`.tackbox-reporters: cannot parse ${d.file}: ${e.message}`, { cause: e })
95
+ throw new Error(`.tackbox/reporters: cannot parse ${d.file}: ${e.message}`, { cause: e })
96
96
  }
97
97
  if (!hasBinding(ast, d.fn)) {
98
- throw new Error(`.tackbox-reporters: no top-level function ${d.fn} in ${d.file}`)
98
+ throw new Error(`.tackbox/reporters: no top-level function ${d.fn} in ${d.file}`)
99
99
  }
100
100
  }
101
101
  }
package/js/README.md CHANGED
@@ -119,11 +119,30 @@ Full constraints per rule:
119
119
  bare `fit` / `fdescribe` / `ftest`, disables the rest of the suite. No
120
120
  escape hatch; the focused test must be removed.
121
121
 
122
+ ### Svelte template markers
123
+
124
+ `svelte-eslint-parser` drives `.svelte` files; the `//` marker form
125
+ works inside `<script>` blocks and `{...}` expressions exactly as in
126
+ plain JS/TS. The template adds one form - an HTML comment
127
+ immediately above an element:
128
+
129
+ ```text
130
+ <!-- no-report: inline handler failure is tolerated here -->
131
+ <button onclick={...}>go</button>
132
+ ```
133
+
134
+ It suppresses a marker-honoring rule's finding anywhere inside that
135
+ element - element-wide on purpose, since an inline handler can span
136
+ lines (D011 A8) - and stops at the element boundary: following
137
+ siblings still report. Recognition reads the enclosing element's
138
+ preceding `SvelteHTMLComment` sibling; `/* ... */` block comments
139
+ are never markers.
140
+
122
141
  ## Reporter recognition
123
142
 
124
143
  A call counts as a reporter only when its callee resolves to one of the
125
144
  reporter names imported from `tackbox` / `tackbox/report` (tier-1), or
126
- to a function declared in a repo-root `.tackbox-reporters` file
145
+ to a function declared in a repo-root `.tackbox/reporters` file
127
146
  (tier-2). A bare identifier that merely shares the name is not trusted.
128
147
 
129
148
  Names: `reportError`, `reportWarn`, `reportQuiet`, `reportApiError`,
@@ -141,7 +160,7 @@ Tier-1 covers named, renamed, default- or namespace-member, and CJS
141
160
  to tier-1 calls; declared sinks carry only the argument-flow contract
142
161
  (the caught error must flow into the call).
143
162
 
144
- `.tackbox-reporters` lines are `file#function: reason`. The `tackbox`
163
+ `.tackbox/reporters` lines are `file#function: reason`. The `tackbox`
145
164
  CLI parses and validates the file. When you consume this ESLint plugin
146
165
  directly (without the CLI), populate `settings.tackbox.reporters` (a
147
166
  list of `"file#function"` strings) in your own config; symbol
@@ -151,7 +170,7 @@ mode.
151
170
  ## Report helper
152
171
 
153
172
  ```js
154
- import { init, reportError, reportWarn, setupGlobalHandlers, flush } from 'tackbox/report'
173
+ import { init, reportError, reportWarn, flush } from 'tackbox/report'
155
174
 
156
175
  init({
157
176
  dsn: import.meta.env.VITE_SENTRY_DSN || '',
@@ -159,7 +178,6 @@ init({
159
178
  verify: true, // confirm connectivity at startup
160
179
  debug: false,
161
180
  })
162
- setupGlobalHandlers()
163
181
  // ... on shutdown:
164
182
  await flush(2000)
165
183
 
@@ -176,6 +194,22 @@ and stays log-only. `init({ verify: true })` sends one healthcheck
176
194
  event with `fingerprint: ["report.startup"]` and flushes; glitchtip
177
195
  groups all startups under one issue, no spam.
178
196
 
197
+ ### Global handlers
198
+
199
+ The helper installs no process-global hook. To route uncaught errors and
200
+ promise rejections into the report lane, the app owns the listeners:
201
+
202
+ ```js
203
+ window.addEventListener('error', e => {
204
+ reportError('uncaught global error', e.error || e.message,
205
+ { source: 'window.error' }, 'global.uncaught')
206
+ })
207
+ window.addEventListener('unhandledrejection', e => {
208
+ reportError('unhandled promise rejection', e.reason,
209
+ { source: 'window.unhandledrejection' }, 'global.unhandled')
210
+ })
211
+ ```
212
+
179
213
  ## Bundled API
180
214
 
181
215
  - `init(opts)`, `flush(timeout)`, `verify(timeout)`, `isReady()`
@@ -188,22 +222,20 @@ groups all startups under one issue, no spam.
188
222
  the user lost connectivity)
189
223
  - `reportSynthError(msg, tags, dedupKey)`
190
224
  - `reportPanic(name, recovered)`
191
- - `setupGlobalHandlers()` wires `window.error` and
192
- `window.unhandledrejection` to `reportError`
193
225
 
194
- The `tackbox:error` custom event is the user lane: it is dispatched on the
195
- window before the init + rate-window gate (and is never rate-limited) after
196
- each `reportError` / `reportWarn` / `notify` / `reportPanic` call, so a
197
- single top-level component can render a toast. `reportQuiet` does not
198
- dispatch it. The event `detail` carries `{ msg, cause, tags, dedupKey,
199
- level }`; the listener coalesces on `dedupKey`. Capture is gated behind init
200
- and the per-`dedupKey` rate window; the user lane is not.
226
+ Sink ordering, the never-suppressed user lane, and the per-`dedupKey` capture
227
+ rate window are the cross-language runtime contract:
228
+ [`../docs/report-contracts.md`](../docs/report-contracts.md). JS-specific: the
229
+ user lane is the `tackbox:error` custom event, dispatched on the window after
230
+ each `reportError` / `reportWarn` / `notify` / `reportPanic` call (`reportQuiet`
231
+ does not dispatch it). The event `detail` carries `{ msg, cause, tags, dedupKey,
232
+ level }`; the listener coalesces on `dedupKey`.
201
233
 
202
234
  Platform limit: a `tackbox:error` listener that throws is not observable from
203
235
  `dispatchEvent` - the browser routes a listener failure to `window.onerror` by
204
236
  design. So the JS user lane cannot capture its own listener's failure the way
205
237
  Go, Python, and Java capture a throwing `report.notifier`. A module-level
206
238
  re-entrancy guard stops the one loop this opens (a throwing listener reaching
207
- `window.onerror`, which `setupGlobalHandlers` turns back into
239
+ `window.onerror`, which an app-owned global handler turns back into
208
240
  `reportError` -> dispatch): a dispatch already in progress on the stack skips
209
241
  the nested one and logs locally instead. Sequential dispatches are unaffected.
@@ -10,7 +10,7 @@
10
10
  // scripts) is still flagged, and a misplaced / duplicate / malformed
11
11
  // marker is a finding that leaves the file strict-ASCII.
12
12
  //
13
- // <!-- tackbox: lang=ru personal experimental repo -->
13
+ // <!-- tackbox lang ru personal experimental repo -->
14
14
  //
15
15
  // The marker is read from micromark HTML-comment tokens, not params.lines:
16
16
  // markdownlint masks HTML-comment interiors in `lines`, so the raw code is
package/js/report.js CHANGED
@@ -128,16 +128,6 @@ function reportPanic(name, recovered) {
128
128
  })
129
129
  }
130
130
 
131
- function setupGlobalHandlers() {
132
- if (typeof window === 'undefined') return
133
- window.addEventListener('error', e => {
134
- reportError('uncaught global error from window', e.error || e.message, { source: 'window.error' }, 'global.uncaught')
135
- })
136
- window.addEventListener('unhandledrejection', e => {
137
- reportError('unhandled promise rejection from window', e.reason, { source: 'window.unhandledrejection' }, 'global.unhandled')
138
- })
139
- }
140
-
141
131
  function maskDSN(dsn) {
142
132
  // no-report: malformed user DSN, opaque marker is the recovery
143
133
  try {
@@ -154,9 +144,9 @@ function dispatchEventSafely(name, detail) {
154
144
  if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return
155
145
  // Re-entrancy guard: a throwing `tackbox:error` listener surfaces via
156
146
  // window.onerror (the DOM routes listener failures there, not to dispatchEvent),
157
- // which setupGlobalHandlers turns back into reportError -> dispatch on the same
158
- // stack. Skip the nested dispatch so that cannot loop; sequential dispatches
159
- // are unaffected (D005 deliver-always intact).
147
+ // which an app-owned global handler can turn back into reportError -> dispatch
148
+ // on the same stack. Skip the nested dispatch so that cannot loop; sequential
149
+ // dispatches are unaffected (D005 deliver-always intact).
160
150
  if (dispatching) {
161
151
  console.warn('[tackbox] report: nested tackbox:error dispatch skipped (listener-failure re-entry)')
162
152
  return
@@ -183,5 +173,4 @@ module.exports = {
183
173
  reportSynthError,
184
174
  notify,
185
175
  reportPanic,
186
- setupGlobalHandlers,
187
176
  }
@@ -4,7 +4,7 @@ const fs = require('fs')
4
4
  // Canonical reporter names. A call counts as a reporter only when its
5
5
  // callee resolves (scope analysis) to an import of `tackbox`/`tackbox/report`
6
6
  // carrying one of these names (tier-1), or to a function declared in
7
- // `.tackbox-reporters` (tier-2). A bare identifier that merely shares the
7
+ // `.tackbox/reporters` (tier-2). A bare identifier that merely shares the
8
8
  // name is not trusted - name-only matching is dead.
9
9
  const REPORTER_NAMES = new Set([
10
10
  'reportError',
@@ -151,7 +151,7 @@ function isTier1ReporterCall(context, call) {
151
151
  return tier1ReporterName(context, call) !== null
152
152
  }
153
153
 
154
- // --- tier-2: .tackbox-reporters declarations -----------------------------
154
+ // --- tier-2: .tackbox/reporters declarations -----------------------------
155
155
 
156
156
  function declaredReporters(context) {
157
157
  const s = context.settings && context.settings.tackbox && context.settings.tackbox.reporters
@@ -280,7 +280,7 @@ function argFlows(call, errName) {
280
280
  }
281
281
 
282
282
  // resolvesToDeclaredReporter: `call`'s callee resolves to a function declared in
283
- // `.tackbox-reporters` for its origin file. Pure origin recognition - the single
283
+ // `.tackbox/reporters` for its origin file. Pure origin recognition - the single
284
284
  // declared-reporter resolver. no-swallow layers an argument-flow gate on top
285
285
  // (the caught err must reach the call).
286
286
  function resolvesToDeclaredReporter(context, call) {
@@ -297,7 +297,7 @@ function isDeclaredReporterCall(context, call, errName) {
297
297
  }
298
298
 
299
299
  // isInDeclaredReporterBody: `node` is lexically inside a function declared in
300
- // `.tackbox-reporters` for this file - no-console-error does not apply there
300
+ // `.tackbox/reporters` for this file - no-console-error does not apply there
301
301
  // (the declared function is itself the reporter).
302
302
  function isInDeclaredReporterBody(context, node) {
303
303
  const decls = declaredReporters(context)
@@ -384,11 +384,38 @@ function blockHasReport(context, block, errName) {
384
384
  return found
385
385
  }
386
386
 
387
- // hasMarkerAbove returns true when the comment block directly above node
388
- // carries `// <prefix>: <reason>` (reason at least MIN_REASON chars, D009) on
389
- // any of its lines - not only the line immediately above, so a long reason can
390
- // be followed by human context. A blank line breaks the block (adjacency
391
- // required).
387
+ // markerText reports whether a comment's raw text is `<prefix>: <reason>` with
388
+ // reason at least MIN_REASON chars (D009) - the shared marker shape.
389
+ function markerText(raw, prefix) {
390
+ const text = raw.trim()
391
+ if (!text.startsWith(prefix + ':')) return false
392
+ return text.slice(prefix.length + 1).trim().length >= MIN_REASON
393
+ }
394
+
395
+ // precedingSvelteSibling returns the template node immediately before `el` among
396
+ // its parent's children (Program.body at the top level, SvelteElement.children
397
+ // when nested), skipping whitespace-only text; null when there is none.
398
+ function precedingSvelteSibling(el) {
399
+ const parent = el.parent
400
+ if (!parent) return null
401
+ const sibs = parent.children || parent.body
402
+ if (!Array.isArray(sibs)) return null
403
+ for (let k = sibs.indexOf(el) - 1; k >= 0; k--) {
404
+ const s = sibs[k]
405
+ if (s.type === 'SvelteText' && typeof s.value === 'string' && s.value.trim() === '') continue
406
+ return s
407
+ }
408
+ return null
409
+ }
410
+
411
+ // hasMarkerAbove returns true when a suppression marker `<prefix>: <reason>`
412
+ // (reason at least MIN_REASON chars, D009) sits above node. Two forms: a `//`
413
+ // comment block directly above node - any of its contiguous lines, so a long
414
+ // reason can be followed by human context, a blank line breaking the block - and,
415
+ // in a Svelte template, an HTML comment `<!-- ... -->` immediately above an
416
+ // enclosing element, which covers the whole element (residual A8: an inline
417
+ // handler can span lines). getAllComments omits SvelteHTMLComment nodes, so the
418
+ // template form is read off the element's preceding sibling.
392
419
  function hasMarkerAbove(context, node, prefix) {
393
420
  if (!node || !node.loc) return false
394
421
  const sourceCode = context.sourceCode || context.getSourceCode()
@@ -397,10 +424,12 @@ function hasMarkerAbove(context, node, prefix) {
397
424
  if (c.type === 'Line') byEndLine.set(c.loc.end.line, c)
398
425
  }
399
426
  for (let line = node.loc.start.line - 1; byEndLine.has(line); line--) {
400
- const text = byEndLine.get(line).value.trim()
401
- if (!text.startsWith(prefix + ':')) continue
402
- const reason = text.slice(prefix.length + 1).trim()
403
- if (reason.length >= MIN_REASON) return true
427
+ if (markerText(byEndLine.get(line).value, prefix)) return true
428
+ }
429
+ for (let cur = node.parent; cur; cur = cur.parent) {
430
+ if (cur.type !== 'SvelteElement') continue
431
+ const sib = precedingSvelteSibling(cur)
432
+ if (sib && sib.type === 'SvelteHTMLComment' && markerText(sib.value, prefix)) return true
404
433
  }
405
434
  return false
406
435
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tackbox",
3
- "version": "0.1.65",
3
+ "version": "0.1.67",
4
4
  "description": "ESLint and Markdown lint plugins plus direct error-reporting helpers for JavaScript and TypeScript.",
5
5
  "license": "MIT",
6
6
  "main": "./js/eslint-plugin.js",