@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 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, 0.6.1 and
12
- 0.6.2 exist as versions in this repository but were never published, so a
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.0] — unreleased
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
@@ -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
- const BUILTIN_LENS_DIR = new URL('../lenses/', import.meta.url).pathname;
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 = dir.endsWith('/') ? `${dir}${name}` : `${dir}/${name}`;
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.replace(/\/[^/]*\/?$/, '');
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
- return (globs ?? []).some(g => globToRegExp(g).test(file));
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
- artifactLocation: { uri: finding.file },
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.0",
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 test/*.test.mjs"
41
+ "test": "node --test"
42
42
  },
43
43
  "keywords": [
44
44
  "code-review",