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 +117 -30
- package/bin/tackbox-eslint.js +4 -4
- package/js/README.md +46 -14
- package/js/markdownlint-rules/no-non-ascii.js +1 -1
- package/js/report.js +3 -14
- package/js/rules/_shared.js +42 -13
- package/package.json +1 -1
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
|
|
28
|
-
a reasoned `// no-report: <reason>` marker at the site, and
|
|
29
|
-
|
|
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
|
-
- **
|
|
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
|
|
157
|
-
package,
|
|
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
|
|
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
|
|
391
|
-
model sees it and fixes it in-loop.
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/bin/tackbox-eslint.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
195
|
-
window
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
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
|
|
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
|
|
158
|
-
// stack. Skip the nested dispatch so that cannot loop; sequential
|
|
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
|
}
|
package/js/rules/_shared.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (
|
|
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