@applesnort/crosscheck 0.2.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/lib/run.mjs ADDED
@@ -0,0 +1,129 @@
1
+ /*!
2
+ * Copyright (c) 2026 Joel Mangin. MIT License.
3
+ */
4
+ // Plan and execute a panel.
5
+ //
6
+ // crosscheck does not talk to a model. It builds the prompts, decides the roster,
7
+ // fans out, and merges what comes back — the caller supplies a command that turns
8
+ // a prompt into text. That keeps the tool agnostic about which agent framework or
9
+ // model you use, and keeps this module testable with a fake executor.
10
+ //
11
+ // The executor contract: exec({prompt, lens}) resolves to
12
+ // {stdout, stderr?, code?}. A non-zero code, a thrown error, or empty stdout all
13
+ // mean that lens did not produce a report — recorded as incomplete, never
14
+ // silently treated as "found nothing".
15
+
16
+ import { buildLensPrompt } from './prompt.mjs';
17
+ import { applyOverrides, routeRoster } from './lenses.mjs';
18
+ import { parseLensOutput } from './parse.mjs';
19
+
20
+ export const DEFAULT_CONCURRENCY = 4;
21
+
22
+ // lenses: [{name, when, owns, 'not-owns', definition?, definitionPath?}]
23
+ // Returns {roster, skipped} — skipped always carries a reason, because a lens
24
+ // dropped without one reads as coverage that never happened.
25
+ export function planRun(lenses, files, overrides = {}) {
26
+ if (!Array.isArray(files) || files.length === 0) {
27
+ throw new Error('no files in scope — nothing to audit');
28
+ }
29
+ const routed = routeRoster(lenses, files);
30
+ const { roster, skipped } = applyOverrides(routed, overrides);
31
+ if (overrides.only?.length) {
32
+ const known = new Set((lenses ?? []).map(l => l.name));
33
+ for (const name of overrides.only) {
34
+ if (!known.has(name)) {
35
+ throw new Error(`--only names an unknown lens: ${name}`);
36
+ }
37
+ }
38
+ }
39
+ return { roster, skipped };
40
+ }
41
+
42
+ export function promptsFor(roster, options = {}) {
43
+ return roster.map(lens => ({
44
+ lens: lens.name,
45
+ files: lens.files,
46
+ prompt: buildLensPrompt(lens, lens.files, {
47
+ definition: lens.definition,
48
+ definitionPath: lens.definitionPath,
49
+ ...options
50
+ })
51
+ }));
52
+ }
53
+
54
+ // Bounded-concurrency map that preserves input order. Kept local rather than
55
+ // pulling in a dependency for nine lines.
56
+ async function mapLimit(items, limit, worker) {
57
+ const results = new Array(items.length);
58
+ let next = 0;
59
+ const runners = Array.from(
60
+ { length: Math.max(1, Math.min(limit, items.length)) },
61
+ async () => {
62
+ while (true) {
63
+ const index = next++;
64
+ if (index >= items.length) {
65
+ return;
66
+ }
67
+ results[index] = await worker(items[index], index);
68
+ }
69
+ });
70
+ await Promise.all(runners);
71
+ return results;
72
+ }
73
+
74
+ // Returns reports in the shape mergeFindings consumes, plus what was skipped and
75
+ // what failed. `findings: null` marks a lens that did not complete.
76
+ export async function runPanel({
77
+ roster,
78
+ skipped = [],
79
+ exec,
80
+ concurrency = DEFAULT_CONCURRENCY,
81
+ promptOptions = {},
82
+ onLensStart = null,
83
+ onLensDone = null
84
+ } = {}) {
85
+ if (typeof exec !== 'function') {
86
+ throw new Error('runPanel requires an exec function');
87
+ }
88
+ const jobs = promptsFor(roster ?? [], promptOptions);
89
+ const failures = [];
90
+
91
+ const reports = await mapLimit(jobs, concurrency, async job => {
92
+ onLensStart?.(job.lens);
93
+ let result;
94
+ try {
95
+ result = await exec({ prompt: job.prompt, lens: job.lens, files: job.files });
96
+ } catch (error) {
97
+ failures.push({ lens: job.lens, reason: error?.message ?? String(error) });
98
+ onLensDone?.(job.lens, { ok: false });
99
+ return { lens: job.lens, findings: null, unparsed: [], output: null };
100
+ }
101
+ const code = result?.code ?? 0;
102
+ const stdout = String(result?.stdout ?? '');
103
+ if (code !== 0) {
104
+ failures.push({
105
+ lens: job.lens,
106
+ reason: `exec exited ${code}` +
107
+ (result?.stderr ? `: ${String(result.stderr).trim().slice(0, 500)}` : '')
108
+ });
109
+ onLensDone?.(job.lens, { ok: false });
110
+ return { lens: job.lens, findings: null, unparsed: [], output: null };
111
+ }
112
+ if (stdout.trim() === '') {
113
+ failures.push({
114
+ lens: job.lens,
115
+ reason: 'exec produced no output; a lens with nothing to report must ' +
116
+ 'say NO FINDINGS'
117
+ });
118
+ onLensDone?.(job.lens, { ok: false });
119
+ return { lens: job.lens, findings: null, unparsed: [], output: null };
120
+ }
121
+ const { findings, unparsed } = parseLensOutput(stdout);
122
+ onLensDone?.(job.lens, { ok: true, findings: findings.length });
123
+ // `output` is the verbatim lens text, kept so `--out` can save a run and it
124
+ // can be rescored later without paying the model again.
125
+ return { lens: job.lens, findings, unparsed, output: stdout };
126
+ });
127
+
128
+ return { reports, skipped, failures };
129
+ }
package/lib/sarif.mjs ADDED
@@ -0,0 +1,176 @@
1
+ /*!
2
+ * Copyright (c) 2026 Joel Mangin. MIT License.
3
+ */
4
+ // Emit merged panel findings as SARIF 2.1.0.
5
+ //
6
+ // Why: every other multi-persona review panel emits prose for a human to read
7
+ // once. SARIF is the OASIS interchange format that static analyzers already
8
+ // speak, so emitting it puts lens findings into GitHub code scanning, editor
9
+ // problem panels, and security dashboards without any of them knowing an LLM
10
+ // produced the results.
11
+ //
12
+ // Two panel-specific properties ride along in `properties`, since SARIF has no
13
+ // native concept for either: the lenses that reported a finding, and the
14
+ // consensus score. Lenses that failed to complete are recorded as tool
15
+ // execution notifications — the format's own place for "this run was partial",
16
+ // which keeps the disclosure machine-readable instead of a line of prose.
17
+
18
+ const SARIF_VERSION = '2.1.0';
19
+ const SCHEMA =
20
+ 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json';
21
+
22
+ // BLOCK/FIX/CONSIDER onto SARIF's result levels.
23
+ const LEVEL = { BLOCK: 'error', FIX: 'warning', CONSIDER: 'note' };
24
+
25
+ export function sarifLevel(severity) {
26
+ return LEVEL[severity] ?? 'note';
27
+ }
28
+
29
+ // Stable across runs and independent of finding order, so a consumer can match
30
+ // the same defect between two runs. SARIF's own mechanism for this.
31
+ export function fingerprint(finding) {
32
+ const basis = `${finding.file}:${finding.line}|${finding.issue}`;
33
+ // A short, dependency-free digest. Not cryptographic — it only has to be
34
+ // stable and collision-resistant enough to key one repo's findings.
35
+ let h1 = 0x811c9dc5;
36
+ let h2 = 0x01000193;
37
+ for (let i = 0; i < basis.length; i++) {
38
+ const c = basis.charCodeAt(i);
39
+ h1 = Math.imul(h1 ^ c, 0x01000193) >>> 0;
40
+ h2 = Math.imul(h2 + c, 0x85ebca6b) >>> 0;
41
+ }
42
+ return (h1.toString(16).padStart(8, '0') +
43
+ h2.toString(16).padStart(8, '0'));
44
+ }
45
+
46
+ // One SARIF rule per lens: the lens is what "decided" the finding, so it is the
47
+ // unit a consumer will want to filter and configure by.
48
+ function rulesForLenses(lenses, lensMeta = {}) {
49
+ return [...lenses].sort().map(name => {
50
+ const meta = lensMeta[name] ?? {};
51
+ const rule = {
52
+ id: `lens/${name}`,
53
+ name,
54
+ shortDescription: { text: meta.summary ?? `${name} review lens` },
55
+ properties: {}
56
+ };
57
+ if (meta.cites?.length) {
58
+ rule.properties.cites = meta.cites;
59
+ }
60
+ if (meta.owns) {
61
+ rule.properties.owns = meta.owns;
62
+ }
63
+ if (Object.keys(rule.properties).length === 0) {
64
+ delete rule.properties;
65
+ }
66
+ return rule;
67
+ });
68
+ }
69
+
70
+ function resultFor(finding) {
71
+ const text = finding.fix
72
+ ? `${finding.issue} — fix: ${finding.fix}`
73
+ : finding.issue;
74
+ return {
75
+ // A finding confirmed by several lenses is attributed to the first that
76
+ // reported it, with the full set in properties; SARIF results carry one
77
+ // ruleId.
78
+ ruleId: `lens/${finding.lenses[0]}`,
79
+ level: sarifLevel(finding.severity),
80
+ message: { text },
81
+ locations: [{
82
+ physicalLocation: {
83
+ artifactLocation: { uri: finding.file },
84
+ region: { startLine: finding.line }
85
+ }
86
+ }],
87
+ partialFingerprints: { crosscheckFindingV1: fingerprint(finding) },
88
+ properties: {
89
+ lenses: finding.lenses,
90
+ consensus: finding.consensus === true,
91
+ consensusScore: finding.consensusScore ?? 1,
92
+ severity: finding.severity
93
+ }
94
+ };
95
+ }
96
+
97
+ // merged: the object returned by mergeFindings (plus optional refuted list).
98
+ // options: { toolVersion, informationUri, lensMeta, refuted }
99
+ export function toSarif(merged, options = {}) {
100
+ const {
101
+ toolVersion = '0.1.0',
102
+ informationUri = 'https://github.com/applesnort/crosscheck',
103
+ lensMeta = {},
104
+ refuted = []
105
+ } = options;
106
+
107
+ const findings = merged?.findings ?? [];
108
+ const incomplete = merged?.incomplete ?? [];
109
+ const unparsed = merged?.unparsed ?? [];
110
+ const lenses = new Set(findings.flatMap(f => f.lenses));
111
+ for (const lens of incomplete) {
112
+ lenses.add(lens);
113
+ }
114
+
115
+ const notifications = [];
116
+ for (const lens of incomplete) {
117
+ notifications.push({
118
+ level: 'error',
119
+ message: {
120
+ text: `Lens "${lens}" did not complete; its coverage is missing from ` +
121
+ 'this run.'
122
+ },
123
+ descriptor: { id: 'crosscheck/lensIncomplete' },
124
+ properties: { lens }
125
+ });
126
+ }
127
+ for (const { lens, line } of unparsed) {
128
+ notifications.push({
129
+ level: 'warning',
130
+ message: {
131
+ text: `Lens "${lens}" emitted a line that does not match the finding ` +
132
+ `contract: ${line}`
133
+ },
134
+ descriptor: { id: 'crosscheck/unparsedOutput' },
135
+ properties: { lens }
136
+ });
137
+ }
138
+ for (const finding of refuted) {
139
+ notifications.push({
140
+ level: 'note',
141
+ message: {
142
+ text: `Finding at ${finding.file}:${finding.line} was refuted during ` +
143
+ `verification and excluded: ${finding.issue}`
144
+ },
145
+ descriptor: { id: 'crosscheck/refuted' },
146
+ properties: { lenses: finding.lenses }
147
+ });
148
+ }
149
+
150
+ return {
151
+ $schema: SCHEMA,
152
+ version: SARIF_VERSION,
153
+ runs: [{
154
+ tool: {
155
+ driver: {
156
+ name: 'crosscheck',
157
+ version: toolVersion,
158
+ informationUri,
159
+ rules: rulesForLenses(lenses, lensMeta)
160
+ }
161
+ },
162
+ invocations: [{
163
+ // A panel missing a lens still produced results, so the invocation
164
+ // succeeded; the gap is reported, not hidden, and not faked as success
165
+ // of the whole roster.
166
+ executionSuccessful: incomplete.length === 0,
167
+ toolExecutionNotifications: notifications
168
+ }],
169
+ results: findings.map(resultFor)
170
+ }]
171
+ };
172
+ }
173
+
174
+ export function toSarifJson(merged, options = {}) {
175
+ return JSON.stringify(toSarif(merged, options), null, 2) + '\n';
176
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@applesnort/crosscheck",
3
+ "version": "0.2.0",
4
+ "description": "Run independent review lenses in parallel and merge their findings into one deduped, consensus-ranked report \u2014 with SARIF output.",
5
+ "license": "MIT",
6
+ "author": "Joel Mangin",
7
+ "type": "module",
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
11
+ "bin": {
12
+ "crosscheck": "bin/crosscheck.mjs"
13
+ },
14
+ "exports": {
15
+ ".": "./lib/merge.mjs",
16
+ "./merge": "./lib/merge.mjs",
17
+ "./parse": "./lib/parse.mjs",
18
+ "./sarif": "./lib/sarif.mjs",
19
+ "./baseline": "./lib/baseline.mjs",
20
+ "./lenses": "./lib/lenses.mjs",
21
+ "./calibrate": "./lib/calibrate.mjs",
22
+ "./run": "./lib/run.mjs",
23
+ "./prompt": "./lib/prompt.mjs"
24
+ },
25
+ "files": [
26
+ "bin/",
27
+ "lib/",
28
+ "lenses/",
29
+ "fixtures/calibration/",
30
+ "foreman.md",
31
+ "PROVENANCE.md",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "test": "node --test test/*.test.mjs"
37
+ },
38
+ "keywords": [
39
+ "code-review",
40
+ "sarif",
41
+ "static-analysis",
42
+ "llm",
43
+ "agents",
44
+ "consensus"
45
+ ],
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/applesnort/crosscheck.git"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ }
53
+ }