@applesnort/crosscheck 0.7.0 → 0.7.1
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/CHANGELOG.md +56 -4
- package/README.md +5 -0
- package/bin/crosscheck.mjs +15 -3
- package/lib/config.mjs +27 -2
- package/lib/lenses.mjs +8 -1
- package/lib/sarif.mjs +4 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,11 +8,63 @@ changes land.
|
|
|
8
8
|
Entries before 0.7.0 were reconstructed from commit history after the fact, so
|
|
9
9
|
they name what each version did rather than itemising every change in it.
|
|
10
10
|
|
|
11
|
-
**Published to npm: 0.2.0, 0.2.1, 0.2.2, 0.3.0, 0.6.0.** 0.4.0, 0.5.0,
|
|
12
|
-
0.6.2 exist as versions in this repository but were never published,
|
|
13
|
-
consumer resolving `@0.6` gets 0.6.0 and none of the fixes after it.
|
|
11
|
+
**Published to npm: 0.2.0, 0.2.1, 0.2.2, 0.3.0, 0.6.0, 0.7.0.** 0.4.0, 0.5.0,
|
|
12
|
+
0.6.1 and 0.6.2 exist as versions in this repository but were never published,
|
|
13
|
+
so a consumer resolving `@0.6` gets 0.6.0 and none of the fixes after it.
|
|
14
14
|
|
|
15
|
-
## [0.7.
|
|
15
|
+
## [0.7.1] — unreleased
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **crosscheck could not run on Windows at all.** The packaged lens directory
|
|
20
|
+
was resolved with `new URL(...).pathname`, which is a URL path rather than a
|
|
21
|
+
filesystem one: on Windows it carries a leading slash before the drive letter
|
|
22
|
+
(`/C:/Users/...`). `readdirSync` threw an uncaught `ENOENT` before any command
|
|
23
|
+
that loads the built-in lenses could do anything, so `run` and `lenses` died
|
|
24
|
+
on a raw Node stack trace.
|
|
25
|
+
|
|
26
|
+
The same bug broke **every platform** whenever the install path contained a
|
|
27
|
+
space, because a URL path keeps its percent-escapes: `/Users/me/my source/`
|
|
28
|
+
arrived as `/Users/me/my%20source/` and resolved to nothing.
|
|
29
|
+
|
|
30
|
+
- Config discovery silently stopped working on Windows. `findConfig` walked
|
|
31
|
+
upward using `/` string surgery, so a Windows `cwd` never yielded a parent:
|
|
32
|
+
the search probed the starting directory and gave up. A project's
|
|
33
|
+
`.crosscheckrc.json` was ignored unless you happened to run from the repo
|
|
34
|
+
root — and nothing said so, which is the invisible behaviour this tool rejects
|
|
35
|
+
everywhere else.
|
|
36
|
+
|
|
37
|
+
- Routing lost every directory-anchored glob on Windows. `collectFiles` reported
|
|
38
|
+
paths with the platform separator, and the matcher only understood `/`.
|
|
39
|
+
Extension globs matched anyway (`[^/]*` eats a backslash), so the roster
|
|
40
|
+
filled and no `UNREVIEWED` line appeared while `**/components/**`,
|
|
41
|
+
`**/migrations/**`, `**/views/**`, `**/pages/**` and `**/schema*` quietly
|
|
42
|
+
matched nothing. Coverage narrowed and the run still reported as complete.
|
|
43
|
+
|
|
44
|
+
Paths are now reported with `/` on every platform, which also makes a
|
|
45
|
+
path-mode run agree with the same run under `--diff` — git has always emitted
|
|
46
|
+
`/`.
|
|
47
|
+
|
|
48
|
+
- SARIF `artifactLocation.uri` is a URI reference, so a Windows path was not
|
|
49
|
+
merely ugly: a backslash is not a separator there and GitHub code scanning
|
|
50
|
+
could not map a result back to its file.
|
|
51
|
+
|
|
52
|
+
- `npm test` did not work on Windows. The script globbed `test/*.test.mjs`, and
|
|
53
|
+
neither `cmd.exe` nor PowerShell expands a glob for an external command, so
|
|
54
|
+
node received the pattern as a literal. It now uses Node's own test
|
|
55
|
+
discovery.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- CI runs the suite on Windows and macOS as well as Linux. Nothing above would
|
|
60
|
+
have reached a user with a Windows job in the matrix, and nothing would catch
|
|
61
|
+
a regression without one.
|
|
62
|
+
|
|
63
|
+
- The CLI smoke-test stubs are Node scripts rather than `#!/bin/sh` ones, and no
|
|
64
|
+
longer need an execute bit. The suite could not run on Windows either, which
|
|
65
|
+
is the other half of why none of this was caught.
|
|
66
|
+
|
|
67
|
+
## [0.7.0] — 2026-08-13
|
|
16
68
|
|
|
17
69
|
### Fixed
|
|
18
70
|
|
package/README.md
CHANGED
|
@@ -38,6 +38,11 @@ Set `exec` to whatever runs your model, then:
|
|
|
38
38
|
crosscheck run --diff --dry-run
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
Node 20 or newer, on Linux, macOS or Windows. On Windows, prefer setting `exec`
|
|
42
|
+
in `.crosscheckrc.json` rather than passing it on the command line: `--exec` is
|
|
43
|
+
handed to the shell, and `cmd.exe` does not treat single quotes as quoting, so
|
|
44
|
+
the POSIX spelling `--exec 'claude -p'` arrives with the quotes still attached.
|
|
45
|
+
|
|
41
46
|
## In CI
|
|
42
47
|
|
|
43
48
|
The scaffolded workflow reviews the diff against the base branch, and does two
|
package/bin/crosscheck.mjs
CHANGED
|
@@ -57,7 +57,8 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
57
57
|
import {
|
|
58
58
|
existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync
|
|
59
59
|
} from 'node:fs';
|
|
60
|
-
import { dirname, join, relative, resolve } from 'node:path';
|
|
60
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
61
|
+
import { fileURLToPath } from 'node:url';
|
|
61
62
|
import { formatScore, score } from '../lib/calibrate.mjs';
|
|
62
63
|
import { findConfig, mergeConfig, validateConfig } from '../lib/config.mjs';
|
|
63
64
|
import { parseFrontmatter, resolveLensSet } from '../lib/lenses.mjs';
|
|
@@ -153,7 +154,11 @@ function loadLensMeta(dir) {
|
|
|
153
154
|
return meta;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
|
|
157
|
+
// fileURLToPath, not `.pathname`: a URL path is not a filesystem path. It keeps
|
|
158
|
+
// percent-escapes, so any install directory containing a space resolves to
|
|
159
|
+
// nothing, and on Windows it carries a leading slash before the drive letter.
|
|
160
|
+
const BUILTIN_LENS_DIR = resolve(
|
|
161
|
+
fileURLToPath(new URL('../lenses/', import.meta.url)));
|
|
157
162
|
|
|
158
163
|
// Lens sources, in increasing precedence: the packaged lenses, then ./lenses or
|
|
159
164
|
// .crosscheck/lenses if present, then anything named by --lenses. Layering
|
|
@@ -229,6 +234,13 @@ function loadLenses(dir) {
|
|
|
229
234
|
return lenses;
|
|
230
235
|
}
|
|
231
236
|
|
|
237
|
+
// Report one separator whatever the platform uses. git diff already speaks `/`,
|
|
238
|
+
// so without this a path-mode run on Windows routed, cached, and reported
|
|
239
|
+
// differently from the same run under --diff. Split on the platform separator
|
|
240
|
+
// rather than replacing every backslash: on POSIX a backslash is a legal
|
|
241
|
+
// character in a filename, and rewriting it would corrupt a real path.
|
|
242
|
+
const toPosix = path => sep === '/' ? path : path.split(sep).join('/');
|
|
243
|
+
|
|
232
244
|
// Expand the positional targets into a concrete file list. Directories are walked;
|
|
233
245
|
// everything is reported relative to cwd so paths in findings match what the user
|
|
234
246
|
// typed.
|
|
@@ -249,7 +261,7 @@ function collectFiles(targets) {
|
|
|
249
261
|
// ../../../ chain is harder to read than the full path, and the model has
|
|
250
262
|
// to resolve whatever we print.
|
|
251
263
|
const rel = relative(process.cwd(), path);
|
|
252
|
-
out.push(!rel || rel.startsWith('..') ? path : rel);
|
|
264
|
+
out.push(toPosix(!rel || rel.startsWith('..') ? path : rel));
|
|
253
265
|
};
|
|
254
266
|
for (const target of targets) {
|
|
255
267
|
if (!existsSync(target)) {
|
package/lib/config.mjs
CHANGED
|
@@ -130,6 +130,31 @@ export function mergeConfig(fileConfig = {}, cliOptions = {}) {
|
|
|
130
130
|
return merged;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// Both separators are accepted throughout the walk. A Windows cwd is
|
|
134
|
+
// `C:\repo\src` while a POSIX one is `/repo/src`, and this module stays pure —
|
|
135
|
+
// it must not read the host platform to decide which shape it was handed.
|
|
136
|
+
// Building the walk out of `/` alone meant the parent never resolved on
|
|
137
|
+
// Windows: the search probed the starting directory and stopped, so a project
|
|
138
|
+
// config was silently ignored unless you ran from the repo root.
|
|
139
|
+
const TRAILING_SEPARATORS = /[\\/]+$/;
|
|
140
|
+
|
|
141
|
+
function joinPath(dir, name) {
|
|
142
|
+
const base = dir.replace(TRAILING_SEPARATORS, '');
|
|
143
|
+
const separator = base.includes('\\') && !base.includes('/') ? '\\' : '/';
|
|
144
|
+
return `${base}${separator}${name}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parentOf(dir) {
|
|
148
|
+
const base = dir.replace(TRAILING_SEPARATORS, '');
|
|
149
|
+
const cut = Math.max(base.lastIndexOf('/'), base.lastIndexOf('\\'));
|
|
150
|
+
if (cut < 0) {
|
|
151
|
+
return '';
|
|
152
|
+
}
|
|
153
|
+
// The parent of `/a` is the root, not the empty string, or the walk would
|
|
154
|
+
// skip a config sitting at `/`.
|
|
155
|
+
return cut === 0 ? base.slice(0, 1) : base.slice(0, cut);
|
|
156
|
+
}
|
|
157
|
+
|
|
133
158
|
// Walk from `startDir` toward the filesystem root looking for a config file, so
|
|
134
159
|
// running from a subdirectory of a project still picks up its settings.
|
|
135
160
|
// `readFile` and `exists` are injected to keep this testable without a disk.
|
|
@@ -142,7 +167,7 @@ export function findConfig(startDir, { exists, isRoot = null } = {}) {
|
|
|
142
167
|
while (dir && !seen.has(dir)) {
|
|
143
168
|
seen.add(dir);
|
|
144
169
|
for (const name of CONFIG_FILENAMES) {
|
|
145
|
-
const candidate =
|
|
170
|
+
const candidate = joinPath(dir, name);
|
|
146
171
|
if (exists(candidate)) {
|
|
147
172
|
return candidate;
|
|
148
173
|
}
|
|
@@ -150,7 +175,7 @@ export function findConfig(startDir, { exists, isRoot = null } = {}) {
|
|
|
150
175
|
if (isRoot?.(dir)) {
|
|
151
176
|
return null;
|
|
152
177
|
}
|
|
153
|
-
const parent = dir
|
|
178
|
+
const parent = parentOf(dir);
|
|
154
179
|
if (parent === dir || parent === '') {
|
|
155
180
|
return null;
|
|
156
181
|
}
|
package/lib/lenses.mjs
CHANGED
|
@@ -150,8 +150,15 @@ export function globToRegExp(glob) {
|
|
|
150
150
|
return new RegExp(`^${out}$`);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// Routing globs are written with `/`, and a Windows path arrives with `\`.
|
|
154
|
+
// Extension globs matched either way — `[^/]*` eats a backslash — so a Windows
|
|
155
|
+
// run filled its roster and announced no coverage hole while every
|
|
156
|
+
// directory-anchored glob quietly stopped matching. Normalising here is safe
|
|
157
|
+
// even where a backslash is a legal filename character: the worst case is that
|
|
158
|
+
// one extra lens reads one extra file.
|
|
153
159
|
export function matchesAny(file, globs) {
|
|
154
|
-
|
|
160
|
+
const path = String(file ?? '').replace(/\\/g, '/');
|
|
161
|
+
return (globs ?? []).some(g => globToRegExp(g).test(path));
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
// lenses: [{name, when: [globs], ...}], files: [paths in scope]
|
package/lib/sarif.mjs
CHANGED
|
@@ -80,7 +80,10 @@ function resultFor(finding) {
|
|
|
80
80
|
message: { text },
|
|
81
81
|
locations: [{
|
|
82
82
|
physicalLocation: {
|
|
83
|
-
|
|
83
|
+
// A URI reference, not a filesystem path: a backslash is not a
|
|
84
|
+
// separator here, and a consumer will not map the result back to a
|
|
85
|
+
// file.
|
|
86
|
+
artifactLocation: { uri: finding.file.replace(/\\/g, '/') },
|
|
84
87
|
region: { startLine: finding.line }
|
|
85
88
|
}
|
|
86
89
|
}],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applesnort/crosscheck",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Run independent review lenses in parallel and merge their findings into one deduped, consensus-ranked report \u2014 with SARIF output.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Joel Mangin",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"LICENSE"
|
|
39
39
|
],
|
|
40
40
|
"scripts": {
|
|
41
|
-
"test": "node --test
|
|
41
|
+
"test": "node --test"
|
|
42
42
|
},
|
|
43
43
|
"keywords": [
|
|
44
44
|
"code-review",
|