@applesnort/crosscheck 0.2.2 → 0.6.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/README.md +212 -4
- package/bin/crosscheck.mjs +444 -38
- package/lib/cache.mjs +100 -0
- package/lib/comment.mjs +138 -0
- package/lib/config.mjs +30 -6
- package/lib/lenses.mjs +35 -0
- package/lib/merge.mjs +7 -1
- package/lib/prompt.mjs +87 -1
- package/lib/run.mjs +106 -4
- package/lib/target.mjs +123 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -19,7 +19,42 @@ or your own wrapper. crosscheck owns prompt construction, routing, fan-out, dedu
|
|
|
19
19
|
and output; you own the model. `--dry-run` prints the roster and prompts without
|
|
20
20
|
spawning anything.
|
|
21
21
|
|
|
22
|
-
No dependencies, no install step,
|
|
22
|
+
No dependencies, no install step, 267 tests.
|
|
23
|
+
|
|
24
|
+
## Getting set up
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx @applesnort/crosscheck init
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Scaffolds `.crosscheckrc.json`, a `.crosscheck/lenses/` directory with a note on
|
|
31
|
+
writing lenses, and a `.github/workflows/crosscheck.yml` that reviews each pull
|
|
32
|
+
request, uploads SARIF to code scanning, and posts a summary comment. Existing
|
|
33
|
+
files are left alone unless you pass `--force`.
|
|
34
|
+
|
|
35
|
+
Set `exec` to whatever runs your model, then:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
crosscheck run --diff --dry-run
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## In CI
|
|
42
|
+
|
|
43
|
+
The scaffolded workflow reviews the diff against the base branch, and does two
|
|
44
|
+
things with the result:
|
|
45
|
+
|
|
46
|
+
- **SARIF to code scanning** — per-line annotations in the Files changed view,
|
|
47
|
+
which is where a reviewer is already looking.
|
|
48
|
+
- **A summary comment** via `--comment-file`, carrying what per-line annotations
|
|
49
|
+
cannot: which lenses were skipped and why, which died, what the baseline
|
|
50
|
+
suppressed, and how many findings verification refuted.
|
|
51
|
+
|
|
52
|
+
The comment embeds a marker so later runs **edit** it rather than stacking. A pull
|
|
53
|
+
request with eleven bot comments gets muted, and a muted reviewer finds nothing.
|
|
54
|
+
|
|
55
|
+
The workflow checks out with `fetch-depth: 0`, because reviewing a diff needs the
|
|
56
|
+
base commit. crosscheck itself never talks to a model — supply whatever your
|
|
57
|
+
`exec` command needs via the workflow's `env`.
|
|
23
58
|
|
|
24
59
|
## Configuration
|
|
25
60
|
|
|
@@ -50,7 +85,9 @@ and an unrecognised key is an error rather than a silent no-op, because a
|
|
|
50
85
|
misspelled `exec` that quietly does nothing is worse than a crash.
|
|
51
86
|
|
|
52
87
|
Accepted keys: `exec`, `lenses`, `concurrency`, `only`, `skip`, `mixed`, `out`,
|
|
53
|
-
`sarif`, `baseline`, `overlap
|
|
88
|
+
`sarif`, `baseline`, `overlap`, `preflight`, `context`, `verify`, `since`,
|
|
89
|
+
`max-dispatches`, `no-cache`, `cache-dir`, `comment-file`. `exec` accepts a string
|
|
90
|
+
or a per-lens map. Keys beginning `//` are treated as comments.
|
|
54
91
|
|
|
55
92
|
> **v0.x — the API is unstable.** The CLI commands and the `lib/` exports may
|
|
56
93
|
> change shape before 1.0. Pin an exact version if you depend on it.
|
|
@@ -59,6 +96,100 @@ Accepted keys: `exec`, `lenses`, `concurrency`, `only`, `skip`, `mixed`, `out`,
|
|
|
59
96
|
> similar to the (abandoned) `cross-check`. Installed, the command is
|
|
60
97
|
> `crosscheck`.
|
|
61
98
|
|
|
99
|
+
## Reviewing a change, not a directory
|
|
100
|
+
|
|
101
|
+
Nobody reviews a whole tree — they review a diff. Auditing paths makes cost scale
|
|
102
|
+
with repository size instead of change size, and re-reads code nobody touched.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
crosscheck run --diff # everything uncommitted
|
|
106
|
+
crosscheck run --staged # what you are about to commit
|
|
107
|
+
crosscheck run --since origin/main # the branch, for a PR
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Each lens is told which lines moved:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
- src/a.js (changed lines: 5-27, 61-84)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Ranges are widened by `--context` lines (20 by default) so a lens sees the code
|
|
117
|
+
around a change, and the prompt is explicit that changed lines are the *priority*
|
|
118
|
+
rather than the boundary — a defect elsewhere that the change causes or depends on
|
|
119
|
+
is still worth reporting, while a pre-existing one it never touches is not what the
|
|
120
|
+
review is for. An empty diff exits cleanly rather than falling back to reviewing
|
|
121
|
+
everything.
|
|
122
|
+
|
|
123
|
+
## Verification is on by default
|
|
124
|
+
|
|
125
|
+
`BLOCK` findings are refuted before they are reported. Each one gets a skeptic that
|
|
126
|
+
is handed **the file** rather than the finding's account of it, told to default to
|
|
127
|
+
refuted when it cannot name a concrete trigger. Refuted findings are removed and
|
|
128
|
+
**the count is always printed, including zero** — a finding that vanished without a
|
|
129
|
+
number is indistinguishable from one that was never found.
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
crosscheck: verifying 3 finding(s)
|
|
133
|
+
✓ confirmed src/session.js:44
|
|
134
|
+
✗ refuted src/cache.js:12
|
|
135
|
+
Refuted in verification: 1.
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
False positives cost more than misses: a panel that cries wolf twice stops being
|
|
139
|
+
read, and its true findings go unread with the rest. `--verify` extends the pass to
|
|
140
|
+
every severity; `--no-verify` skips it, at that cost.
|
|
141
|
+
|
|
142
|
+
A verifier that fails to run is **not** treated as agreement — the finding stands
|
|
143
|
+
and the failure is reported.
|
|
144
|
+
|
|
145
|
+
## Your own gate, before anything is dispatched
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{ "preflight": "scripts/check-clean-worktree.sh" }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
A non-zero exit aborts before a single model call. That lets a project enforce a
|
|
152
|
+
rule crosscheck knows nothing about — data classification, branch policy, a clean
|
|
153
|
+
worktree — without the rule having to exist upstream.
|
|
154
|
+
|
|
155
|
+
## Cost control
|
|
156
|
+
|
|
157
|
+
A tool that costs real money per run gets switched off, and a switched-off tool
|
|
158
|
+
finds nothing.
|
|
159
|
+
|
|
160
|
+
**A cheap lens should not pay for an expensive model.** `exec` may be a map:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"exec": {
|
|
165
|
+
"default": "claude -p",
|
|
166
|
+
"conventions": "llm -m claude-haiku-4-5"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Precedence is the lens's own `exec` in its frontmatter, then the map entry, then
|
|
172
|
+
`default`. A rostered lens with no command anywhere fails loudly and names itself
|
|
173
|
+
— it is never quietly skipped.
|
|
174
|
+
|
|
175
|
+
**Unchanged files are not re-reviewed.** Results are cached under
|
|
176
|
+
`.crosscheck/cache`, keyed on the lens definition, the files, and their contents.
|
|
177
|
+
The definition is part of the key deliberately: editing a lens must invalidate its
|
|
178
|
+
results, or you would be served answers from the previous prompt with no way to
|
|
179
|
+
tell. `--no-cache` disables it, `--cache-dir` relocates it. A failed lens is never
|
|
180
|
+
cached, so it is retried rather than permanently wrong.
|
|
181
|
+
|
|
182
|
+
**`--max-dispatches N` caps the run, and says what it dropped:**
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
crosscheck: BUDGET REACHED — 3 lens(es) not run: check, security-check, taint
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The unit is dispatches, not dollars. crosscheck cannot see tokens or cost —
|
|
189
|
+
`--exec` is an arbitrary command — so a monetary budget would be a number invented
|
|
190
|
+
from nothing. Truncation is always named, because a run that quietly stopped early
|
|
191
|
+
looks exactly like a run that found nothing.
|
|
192
|
+
|
|
62
193
|
## SARIF output
|
|
63
194
|
|
|
64
195
|
Findings are emitted as [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html),
|
|
@@ -200,7 +331,11 @@ lib/
|
|
|
200
331
|
prompt.mjs lens prompt construction
|
|
201
332
|
run.mjs roster planning and bounded fan-out
|
|
202
333
|
config.mjs .crosscheckrc.json discovery and validation
|
|
203
|
-
|
|
334
|
+
target.mjs diff parsing, changed line ranges
|
|
335
|
+
cache.mjs content-addressed result cache
|
|
336
|
+
comment.mjs pull-request summary comment
|
|
337
|
+
bin/crosscheck.mjs CLI: init | run | lenses | report | sarif |
|
|
338
|
+
baseline | overlap | calibrate
|
|
204
339
|
fixtures/calibration/ planted defects, ground truth, and the calibration record
|
|
205
340
|
fixtures/deception/ 20 modules that look safe and are not, or the reverse
|
|
206
341
|
PROVENANCE.md where all of this came from
|
|
@@ -246,7 +381,80 @@ in and has to be killed to recover it.
|
|
|
246
381
|
npm test # 183 tests, no dependencies
|
|
247
382
|
```
|
|
248
383
|
|
|
249
|
-
## Adding
|
|
384
|
+
## Adding your own lenses
|
|
385
|
+
|
|
386
|
+
Lens sources layer, in increasing precedence:
|
|
387
|
+
|
|
388
|
+
1. the lenses packaged with crosscheck
|
|
389
|
+
2. `./lenses` or `./.crosscheck/lenses` in your project, if present
|
|
390
|
+
3. anything named by `--lenses dir,dir` or the config's `lenses` key
|
|
391
|
+
|
|
392
|
+
A later source **adds** to the earlier ones. A lens whose `name` matches an
|
|
393
|
+
earlier one **overrides** it, and the override is printed — so customising the
|
|
394
|
+
stock `check` costs one file rather than forking all five and losing upstream
|
|
395
|
+
changes. `--no-builtin` drops the packaged set entirely.
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
mkdir -p .crosscheck/lenses
|
|
399
|
+
$EDITOR .crosscheck/lenses/chaos.md
|
|
400
|
+
npx @applesnort/crosscheck lenses # what resolved, and from where
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
A lens is a markdown file: five frontmatter keys, then the prompt.
|
|
404
|
+
|
|
405
|
+
```markdown
|
|
406
|
+
---
|
|
407
|
+
name: chaos
|
|
408
|
+
summary: adversarial user trying to break the flow
|
|
409
|
+
when: [**/*.{js,jsx,tsx,vue}]
|
|
410
|
+
owns: states reachable by misuse — double-submit, back button, hostile input
|
|
411
|
+
not-owns: correctness, security categories, architecture
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
# Lens: chaos
|
|
415
|
+
|
|
416
|
+
You are trying to break this, not review it. ...
|
|
417
|
+
|
|
418
|
+
Findings only: `file:line — SEVERITY — issue — fix`. SEVERITY is BLOCK, FIX, or
|
|
419
|
+
CONSIDER. If nothing here can be broken, reply exactly `NO FINDINGS`.
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
`when` routes it — a lens whose globs match nothing in the target is skipped, with
|
|
423
|
+
the reason printed. `not-owns` is required: a lens that never declines dilutes the
|
|
424
|
+
signal everything else depends on. The body should end by restating the output
|
|
425
|
+
contract, since that is what the parser expects back.
|
|
426
|
+
|
|
427
|
+
`crosscheck lenses` prints the resolved set with each lens's origin and globs,
|
|
428
|
+
which is the fastest way to see why something did or did not run.
|
|
429
|
+
|
|
430
|
+
### Local lenses stay local
|
|
431
|
+
|
|
432
|
+
Nothing in `./lenses` or `./.crosscheck/lenses` is packaged, uploaded, or shared
|
|
433
|
+
by crosscheck. Those files are read off your disk at dispatch time and go nowhere
|
|
434
|
+
else. That has two practical consequences worth knowing before you write any.
|
|
435
|
+
|
|
436
|
+
**A local lens can be as specific as you like.** The packaged lenses are
|
|
437
|
+
deliberately generic, which also makes them shallow: they cannot know your
|
|
438
|
+
storage conventions, your framework's failure modes, the mistake your team makes
|
|
439
|
+
every quarter, or the review standard one colleague applies better than anyone
|
|
440
|
+
else. A lens that encodes any of that will outperform a generic one on your
|
|
441
|
+
codebase, and it belongs in your repo rather than upstream. The useful ones
|
|
442
|
+
usually aren't portable.
|
|
443
|
+
|
|
444
|
+
**Distribution is where obligations start, and you are not distributing.** If you
|
|
445
|
+
adapt a lens from a published persona set, a standards document, or a colleague's
|
|
446
|
+
review checklist, keeping it local puts you in the same position as any private
|
|
447
|
+
note-taking. Those materials often carry licences requiring attribution — the UK
|
|
448
|
+
government's accessibility personas are published under the Open Government
|
|
449
|
+
Licence, for one — and that condition attaches when you *publish* a derivative,
|
|
450
|
+
not when you run one. If a local lens later becomes something you want to share,
|
|
451
|
+
that is the moment to check what it derives from and credit it.
|
|
452
|
+
|
|
453
|
+
The corollary: a lens holding a named individual's review preferences, or a
|
|
454
|
+
description of an unreleased product, is a local lens permanently. Publishing it
|
|
455
|
+
shares something about a person or a project, not just a prompt.
|
|
456
|
+
|
|
457
|
+
## Writing a good lens
|
|
250
458
|
|
|
251
459
|
A lens earns its place by finding what the others miss. Give it a remit narrow
|
|
252
460
|
enough that it declines most changes, state what it does *not* own, and make it name
|