peaks-loop 4.0.15 → 4.0.16

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/config/eslint/.peaks-rules.cjs +8 -1
  3. package/dist/cli/commands/_register.js +2 -1
  4. package/dist/cli/commands/code-review-commands.d.ts +2 -30
  5. package/dist/cli/commands/code-review-commands.js +43 -65
  6. package/dist/cli/commands/lint-commands.d.ts +21 -0
  7. package/dist/cli/commands/lint-commands.js +146 -0
  8. package/dist/cli/commands/security-audit-commands.d.ts +1 -1
  9. package/dist/cli/commands/security-audit-commands.js +1 -1
  10. package/dist/services/code-review/ecc-bridge.d.ts +2 -7
  11. package/dist/services/config/config-service.d.ts +2 -19
  12. package/dist/services/config/config-service.js +0 -52
  13. package/dist/services/config/config-types.d.ts +4 -24
  14. package/dist/services/config/config-types.js +1 -10
  15. package/dist/services/lint/detect-eslint.d.ts +10 -0
  16. package/dist/services/lint/detect-eslint.js +59 -0
  17. package/dist/services/lint/detect-ocr-18.d.ts +10 -0
  18. package/dist/services/lint/detect-ocr-18.js +40 -0
  19. package/dist/services/lint/eslint-runner.d.ts +58 -0
  20. package/dist/services/lint/eslint-runner.js +307 -0
  21. package/dist/services/lint/ocr-multilang-adapter.d.ts +34 -0
  22. package/dist/services/lint/ocr-multilang-adapter.js +143 -0
  23. package/package.json +3 -10
  24. package/skills/bee/peaks-rd/SKILL.md +1 -1
  25. package/skills/bee/peaks-rd/references/jsts-eslint-gate.md +197 -0
  26. package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +132 -0
  27. package/skills/bee/peaks-rd/references/parallel-review-fanout.md +1 -1
  28. package/skills/peaks-code/SKILL.md +1 -1
  29. package/dist/services/code-review/ocr-service.d.ts +0 -129
  30. package/dist/services/code-review/ocr-service.js +0 -372
  31. package/skills/bee/peaks-rd/references/ocr-integration.md +0 -229
@@ -0,0 +1,10 @@
1
+ import { ESLINT_PACKAGE_PINS } from './eslint-runner.js';
2
+ export type EslintDetectState = 'ready' | 'eslint-missing' | 'config-error' | 'npx-failed' | 'detection-failed';
3
+ export type EslintDetectResult = {
4
+ readonly state: EslintDetectState;
5
+ readonly npxAvailable: boolean;
6
+ readonly pinnedVersions: typeof ESLINT_PACKAGE_PINS;
7
+ readonly warnings: readonly string[];
8
+ readonly nextActions: readonly string[];
9
+ };
10
+ export declare function detectEslint(): EslintDetectResult;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * 5-state ESLint detect — soft-fail when the toolchain is missing.
3
+ * Mirrors the ECC bridge detect shape so peaks-rd can compose a
4
+ * unified Gate B5 verdict.
5
+ */
6
+ import { spawnSync } from 'node:child_process';
7
+ import { ESLINT_PACKAGE_PINS } from './eslint-runner.js';
8
+ const PACKAGES_TO_PROBE = [
9
+ 'eslint',
10
+ 'typescriptEslintParser',
11
+ 'typescriptEslintPlugin',
12
+ 'importPlugin'
13
+ ];
14
+ function packageNameFor(key) {
15
+ switch (key) {
16
+ case 'eslint': return 'eslint';
17
+ case 'typescriptEslintParser': return '@typescript-eslint/parser';
18
+ case 'typescriptEslintPlugin': return '@typescript-eslint/eslint-plugin';
19
+ case 'importPlugin': return 'eslint-plugin-import';
20
+ }
21
+ }
22
+ function probeNpx() {
23
+ const probe = spawnSync('npx', ['--version'], { encoding: 'utf8' });
24
+ return probe.status === 0;
25
+ }
26
+ function probePackage(key) {
27
+ const pkg = packageNameFor(key);
28
+ const pin = ESLINT_PACKAGE_PINS[key];
29
+ const result = spawnSync('npm', ['view', `${pkg}@${pin}`, 'version'], { encoding: 'utf8' });
30
+ return result.status === 0;
31
+ }
32
+ export function detectEslint() {
33
+ const nextActions = [];
34
+ const warnings = [];
35
+ if (!probeNpx()) {
36
+ return {
37
+ state: 'npx-failed',
38
+ npxAvailable: false,
39
+ pinnedVersions: ESLINT_PACKAGE_PINS,
40
+ warnings: ['npx is not on PATH'],
41
+ nextActions: ['Install Node.js ≥ 20 with npm to enable `npx --package`.', 'Or run `peaks code lint --no-npx` (future slice).']
42
+ };
43
+ }
44
+ for (const key of PACKAGES_TO_PROBE) {
45
+ if (!probePackage(key)) {
46
+ warnings.push(`npm registry cannot resolve ${packageNameFor(key)}@${ESLINT_PACKAGE_PINS[key]}`);
47
+ }
48
+ }
49
+ if (warnings.length > 0) {
50
+ nextActions.push('Re-run `peaks code lint --json` after npm connectivity is restored.');
51
+ }
52
+ return {
53
+ state: 'ready',
54
+ npxAvailable: true,
55
+ pinnedVersions: ESLINT_PACKAGE_PINS,
56
+ warnings,
57
+ nextActions
58
+ };
59
+ }
@@ -0,0 +1,10 @@
1
+ import { OCR_18_PACKAGE } from './ocr-multilang-adapter.js';
2
+ export type Ocr18DetectState = 'ready' | 'ocr18-missing' | 'binary-missing' | 'llm-config-missing' | 'detection-failed';
3
+ export type Ocr18DetectResult = {
4
+ readonly state: Ocr18DetectState;
5
+ readonly npxAvailable: boolean;
6
+ readonly package: typeof OCR_18_PACKAGE;
7
+ readonly warnings: readonly string[];
8
+ readonly nextActions: readonly string[];
9
+ };
10
+ export declare function detectOcr18(): Ocr18DetectResult;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 5-state OCR 1.8.x detect. Mirrors the ECC detect shape.
3
+ */
4
+ import { spawnSync } from 'node:child_process';
5
+ import { OCR_18_PACKAGE } from './ocr-multilang-adapter.js';
6
+ function probeNpx() {
7
+ const probe = spawnSync('npx', ['--version'], { encoding: 'utf8' });
8
+ return probe.status === 0;
9
+ }
10
+ function probeOcr18() {
11
+ const result = spawnSync('npx', ['--package', OCR_18_PACKAGE, '--', 'ocr', 'version'], { encoding: 'utf8' });
12
+ return result.status === 0;
13
+ }
14
+ export function detectOcr18() {
15
+ if (!probeNpx()) {
16
+ return {
17
+ state: 'ocr18-missing',
18
+ npxAvailable: false,
19
+ package: OCR_18_PACKAGE,
20
+ warnings: ['npx is not on PATH'],
21
+ nextActions: ['Install Node.js ≥ 20 with npm to enable `npx --package`.']
22
+ };
23
+ }
24
+ if (!probeOcr18()) {
25
+ return {
26
+ state: 'ocr18-missing',
27
+ npxAvailable: true,
28
+ package: OCR_18_PACKAGE,
29
+ warnings: [`could not resolve ${OCR_18_PACKAGE}`],
30
+ nextActions: ['Run `npm i @alibaba-group/open-code-review@1.8.9` to install the reviewer.']
31
+ };
32
+ }
33
+ return {
34
+ state: 'ready',
35
+ npxAvailable: true,
36
+ package: OCR_18_PACKAGE,
37
+ warnings: [],
38
+ nextActions: []
39
+ };
40
+ }
@@ -0,0 +1,58 @@
1
+ export declare const ESLINT_PACKAGE_PINS: {
2
+ readonly eslint: "10.8.0";
3
+ readonly typescriptEslintParser: "8.66.0";
4
+ readonly typescriptEslintPlugin: "8.66.0";
5
+ readonly importPlugin: "2.32.0";
6
+ };
7
+ export type RedLineMode = 'none' | 'baseline-aware';
8
+ export type EslintState = 'ok' | 'eslint-missing' | 'npx-failed' | 'execution-failed' | 'baseline-missing';
9
+ export type EslintFinding = {
10
+ readonly filePath: string;
11
+ readonly line: number;
12
+ readonly column: number;
13
+ readonly ruleId: string | null;
14
+ readonly severity: 'error' | 'warn' | 'info';
15
+ readonly message: string;
16
+ };
17
+ export type EslintSummary = {
18
+ readonly error: number;
19
+ readonly warn: number;
20
+ readonly info: number;
21
+ };
22
+ export type BaselineViolation = {
23
+ readonly ruleId: string;
24
+ readonly file: string;
25
+ readonly line: number;
26
+ readonly severity: 'error' | 'warn' | 'info';
27
+ readonly message: string;
28
+ };
29
+ export type RedLineEntry = {
30
+ readonly ruleId: string;
31
+ readonly count: number;
32
+ readonly topFiles: ReadonlyArray<{
33
+ readonly file: string;
34
+ readonly count: number;
35
+ }>;
36
+ };
37
+ export type EslintRunResult = {
38
+ readonly state: EslintState;
39
+ readonly findings: readonly EslintFinding[];
40
+ readonly summary: EslintSummary;
41
+ readonly durationMs: number;
42
+ readonly rawOutput: string;
43
+ readonly baselineWaived: readonly EslintFinding[];
44
+ readonly redLine: readonly RedLineEntry[];
45
+ };
46
+ export type EslintRunOptions = {
47
+ readonly cwd: string;
48
+ readonly scope?: string;
49
+ readonly configPath?: string;
50
+ readonly fix?: boolean;
51
+ readonly write?: boolean;
52
+ readonly timeoutMs?: number;
53
+ readonly diffOnly?: boolean;
54
+ readonly baselineFile?: string;
55
+ readonly redLineMode?: RedLineMode;
56
+ };
57
+ export declare function buildEslintArgs(options: EslintRunOptions): string[];
58
+ export declare function runEslint(options: EslintRunOptions): EslintRunResult;
@@ -0,0 +1,307 @@
1
+ /**
2
+ * ESLint runner — read-only verifier for peaks code lint.
3
+ * Pins the 4 toolchain packages to the same major versions
4
+ * `config/eslint/.peaks-rules.cjs` requires; the runner loads them
5
+ * via `npx --package` so peaks-loop devDeps do not grow. Per the
6
+ * G-lint-2 red line, --fix / --write are FORBIDDEN.
7
+ *
8
+ * PRD-002b slice: three new options enforce the
9
+ * incremental-first / no-touch-stockcode / project-aware baseline
10
+ * invariants:
11
+ *
12
+ * - diffOnly (default true): filter findings to git-diff hunks;
13
+ * 存量违规 silently skipped. Enforces D4 + D5.
14
+ * - baselineFile (default '.peaks/lint/baseline.json'): waiver
15
+ * matching findings (ruleId + file + line). Enforces D5.
16
+ * - redLineMode (default 'baseline-aware'): aggregate baseline
17
+ * violations by ruleId so the envelope carries an LLM-readable
18
+ * red-line. Enforces D6 + supplementary S2.
19
+ */
20
+ import { spawnSync } from 'node:child_process';
21
+ import { readFileSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+ export const ESLINT_PACKAGE_PINS = {
24
+ eslint: '10.8.0',
25
+ typescriptEslintParser: '8.66.0',
26
+ typescriptEslintPlugin: '8.66.0',
27
+ importPlugin: '2.32.0'
28
+ };
29
+ function severityFor(value) {
30
+ if (value === 2)
31
+ return 'error';
32
+ if (value === 1)
33
+ return 'warn';
34
+ return 'info';
35
+ }
36
+ function summarize(findings) {
37
+ let error = 0;
38
+ let warn = 0;
39
+ let info = 0;
40
+ for (const f of findings) {
41
+ if (f.severity === 'error')
42
+ error++;
43
+ else if (f.severity === 'warn')
44
+ warn++;
45
+ else
46
+ info++;
47
+ }
48
+ return { error, warn, info };
49
+ }
50
+ /**
51
+ * Read `git diff HEAD --unified=0` and parse every `+` line as a
52
+ * touched line number. Falls back to [] on any parse error so the
53
+ * caller treats all findings as out-of-diff (no silent zero-result).
54
+ */
55
+ function loadDiffRanges(cwd) {
56
+ const ranges = [];
57
+ try {
58
+ const result = spawnSync('git', ['diff', 'HEAD', '--unified=0', '--no-color'], {
59
+ cwd,
60
+ encoding: 'utf8',
61
+ maxBuffer: 16 * 1024 * 1024
62
+ });
63
+ if (result.status !== 0 || typeof result.stdout !== 'string')
64
+ return [];
65
+ const stdout = result.stdout;
66
+ let currentFile = null;
67
+ let currentLine = 0;
68
+ for (const raw of stdout.split('\n')) {
69
+ const line = raw;
70
+ if (line.startsWith('+++ ')) {
71
+ const path = line.slice(4).split('\t')[0] ?? '';
72
+ currentFile = path.startsWith('b/') ? path.slice(2) : path;
73
+ continue;
74
+ }
75
+ if (line.startsWith('--- ')) {
76
+ continue;
77
+ }
78
+ const hunk = /^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/.exec(line);
79
+ if (hunk !== null) {
80
+ currentLine = Number.parseInt(hunk[1] ?? '0', 10);
81
+ continue;
82
+ }
83
+ if (currentFile !== null && line.startsWith('+') && !line.startsWith('+++')) {
84
+ if (Number.isFinite(currentLine) && currentLine > 0) {
85
+ ranges.push({ file: currentFile, lines: [currentLine] });
86
+ }
87
+ currentLine += 1;
88
+ }
89
+ }
90
+ }
91
+ catch {
92
+ return [];
93
+ }
94
+ return ranges;
95
+ }
96
+ function inDiff(filePath, line, ranges) {
97
+ if (ranges.length === 0)
98
+ return false;
99
+ for (const r of ranges) {
100
+ if (r.file !== filePath)
101
+ continue;
102
+ for (const ln of r.lines) {
103
+ if (Math.abs(ln - line) <= 0)
104
+ return true;
105
+ }
106
+ }
107
+ return false;
108
+ }
109
+ function loadBaseline(cwd, baselineFile) {
110
+ const fullPath = join(cwd, baselineFile);
111
+ let raw;
112
+ try {
113
+ raw = readFileSync(fullPath, 'utf8');
114
+ }
115
+ catch {
116
+ return [];
117
+ }
118
+ let parsed;
119
+ try {
120
+ parsed = JSON.parse(raw);
121
+ }
122
+ catch {
123
+ return [];
124
+ }
125
+ const violations = Array.isArray(parsed.violations) ? parsed.violations : [];
126
+ const out = [];
127
+ for (const v of violations) {
128
+ if (typeof v.ruleId !== 'string' || typeof v.file !== 'string' || typeof v.line !== 'number')
129
+ continue;
130
+ out.push({
131
+ ruleId: v.ruleId,
132
+ file: v.file,
133
+ line: v.line,
134
+ severity: severityFor(v.severity),
135
+ message: typeof v.message === 'string' ? v.message : ''
136
+ });
137
+ }
138
+ return out;
139
+ }
140
+ function matchBaseline(finding, baseline) {
141
+ for (const v of baseline) {
142
+ if (v.ruleId !== finding.ruleId)
143
+ continue;
144
+ if (v.file !== finding.filePath)
145
+ continue;
146
+ if (v.line !== finding.line)
147
+ continue;
148
+ return true;
149
+ }
150
+ return false;
151
+ }
152
+ function aggregateRedLine(baseline) {
153
+ const byRule = new Map();
154
+ for (const v of baseline) {
155
+ const existing = byRule.get(v.ruleId);
156
+ if (existing === undefined) {
157
+ const fileCounts = new Map();
158
+ fileCounts.set(v.file, 1);
159
+ byRule.set(v.ruleId, { count: 1, fileCounts });
160
+ }
161
+ else {
162
+ existing.count += 1;
163
+ existing.fileCounts.set(v.file, (existing.fileCounts.get(v.file) ?? 0) + 1);
164
+ }
165
+ }
166
+ const out = [];
167
+ for (const [ruleId, agg] of byRule.entries()) {
168
+ const topFiles = [...agg.fileCounts.entries()]
169
+ .map(([file, count]) => ({ file, count }))
170
+ .sort((a, b) => b.count - a.count)
171
+ .slice(0, 5);
172
+ out.push({ ruleId, count: agg.count, topFiles });
173
+ }
174
+ out.sort((a, b) => b.count - a.count);
175
+ return out;
176
+ }
177
+ const EMPTY_DIFF_RANGE = [];
178
+ const EMPTY_REDLINE = [];
179
+ function emptyResult(state, start, rawOutput) {
180
+ return {
181
+ state,
182
+ findings: [],
183
+ summary: { error: 0, warn: 0, info: 0 },
184
+ durationMs: Date.now() - start,
185
+ rawOutput,
186
+ baselineWaived: [],
187
+ redLine: EMPTY_REDLINE
188
+ };
189
+ }
190
+ export function buildEslintArgs(options) {
191
+ if (options.fix === true || options.write === true) {
192
+ throw Object.assign(new Error('peaks code lint is read-only; --fix and --write are forbidden'), {
193
+ code: 'LINT_FIX_FORBIDDEN'
194
+ });
195
+ }
196
+ const args = [
197
+ '--package', `eslint@${ESLINT_PACKAGE_PINS.eslint}`,
198
+ '--package', `@typescript-eslint/parser@${ESLINT_PACKAGE_PINS.typescriptEslintParser}`,
199
+ '--package', `@typescript-eslint/eslint-plugin@${ESLINT_PACKAGE_PINS.typescriptEslintPlugin}`,
200
+ '--package', `eslint-plugin-import@${ESLINT_PACKAGE_PINS.importPlugin}`,
201
+ '--', 'eslint', '--no-warn-ignored', '--format', 'json'
202
+ ];
203
+ if (options.configPath !== undefined) {
204
+ args.push('--config', options.configPath);
205
+ }
206
+ args.push(...(options.scope !== undefined && options.scope.length > 0 ? [options.scope] : ['.']));
207
+ return args;
208
+ }
209
+ export function runEslint(options) {
210
+ const start = Date.now();
211
+ let args;
212
+ try {
213
+ args = buildEslintArgs(options);
214
+ }
215
+ catch (error) {
216
+ return emptyResult('execution-failed', start, error instanceof Error ? error.message : String(error));
217
+ }
218
+ const spawnOptions = {
219
+ cwd: options.cwd,
220
+ encoding: 'utf8',
221
+ timeout: options.timeoutMs ?? 60_000,
222
+ maxBuffer: 32 * 1024 * 1024
223
+ };
224
+ const result = spawnSync('npx', args, spawnOptions);
225
+ if (result.error !== undefined && result.error !== null) {
226
+ const message = result.error.message;
227
+ const state = /ENOENT/.test(message) ? 'npx-failed' : 'execution-failed';
228
+ return emptyResult(state, start, typeof result.stdout === 'string' ? result.stdout : '');
229
+ }
230
+ if (result.signal !== null && result.signal !== undefined) {
231
+ return emptyResult('execution-failed', start, typeof result.stdout === 'string' ? result.stdout : '');
232
+ }
233
+ const stdout = typeof result.stdout === 'string' ? result.stdout : '';
234
+ let findings = [];
235
+ if (stdout.trim().length > 0) {
236
+ try {
237
+ const parsed = JSON.parse(stdout);
238
+ for (const entry of parsed) {
239
+ if (typeof entry !== 'object' || entry === null)
240
+ continue;
241
+ const parentFile = typeof entry.filePath === 'string'
242
+ ? entry.filePath
243
+ : '';
244
+ const messages = Array.isArray(entry.messages)
245
+ ? entry.messages
246
+ : [];
247
+ for (const m of messages) {
248
+ if (m === null || typeof m !== 'object')
249
+ continue;
250
+ findings.push({
251
+ filePath: typeof m.filePath === 'string' && m.filePath.length > 0 ? m.filePath : parentFile,
252
+ line: typeof m.line === 'number' ? m.line : 0,
253
+ column: typeof m.column === 'number' ? m.column : 0,
254
+ ruleId: typeof m.ruleId === 'string' ? m.ruleId : null,
255
+ severity: severityFor(m.severity),
256
+ message: typeof m.message === 'string' ? m.message : ''
257
+ });
258
+ }
259
+ }
260
+ }
261
+ catch {
262
+ return emptyResult('execution-failed', start, stdout);
263
+ }
264
+ }
265
+ if (result.status !== 0 && findings.length === 0) {
266
+ return emptyResult('eslint-missing', start, typeof result.stderr === 'string' ? result.stderr : stdout);
267
+ }
268
+ // PRD-002b slice: incremental-first / no-touch-stockcode filters.
269
+ // PRD-002b D6: baselineFile is project-level; if missing we surface
270
+ // state='baseline-missing' so the caller can re-run `peaks lint baseline`.
271
+ const diffOnly = options.diffOnly !== false;
272
+ const redLineMode = options.redLineMode ?? 'baseline-aware';
273
+ const baselinePath = options.baselineFile ?? '.peaks/lint/baseline.json';
274
+ const baseline = loadBaseline(options.cwd, baselinePath);
275
+ const diffRanges = diffOnly ? loadDiffRanges(options.cwd) : EMPTY_DIFF_RANGE;
276
+ let activeFindings = findings;
277
+ let waived = [];
278
+ if (diffOnly) {
279
+ activeFindings = findings.filter((f) => inDiff(f.filePath, f.line, diffRanges));
280
+ }
281
+ if (baseline.length > 0) {
282
+ const next = [];
283
+ waived = [];
284
+ for (const f of activeFindings) {
285
+ if (matchBaseline(f, baseline)) {
286
+ waived.push(f);
287
+ }
288
+ else {
289
+ next.push(f);
290
+ }
291
+ }
292
+ activeFindings = next;
293
+ }
294
+ const redLine = redLineMode === 'baseline-aware' ? aggregateRedLine(baseline) : EMPTY_REDLINE;
295
+ const finalState = baseline.length === 0 && diffOnly && options.baselineFile !== undefined
296
+ ? 'baseline-missing'
297
+ : 'ok';
298
+ return {
299
+ state: finalState,
300
+ findings: activeFindings,
301
+ summary: summarize(activeFindings),
302
+ durationMs: Date.now() - start,
303
+ rawOutput: stdout,
304
+ baselineWaived: waived,
305
+ redLine
306
+ };
307
+ }
@@ -0,0 +1,34 @@
1
+ export declare const OCR_18_PACKAGE = "@alibaba-group/open-code-review@1.8.9";
2
+ export declare const OCR_18_LANGUAGES: readonly ["python", "go", "java", "rust", "cpp", "csharp", "ruby", "php"];
3
+ export type Ocr18Language = (typeof OCR_18_LANGUAGES)[number];
4
+ export type Ocr18State = 'ok' | 'ocr18-missing' | 'binary-missing' | 'llm-config-missing' | 'execution-failed' | 'language-unsupported';
5
+ export type Ocr18Finding = {
6
+ readonly filePath: string;
7
+ readonly line: number;
8
+ readonly ruleId: string | null;
9
+ readonly severity: 'error' | 'warn' | 'info';
10
+ readonly message: string;
11
+ };
12
+ export type Ocr18Summary = {
13
+ readonly total: number;
14
+ readonly bySeverity: Readonly<Record<'error' | 'warn' | 'info', number>>;
15
+ readonly byLanguage: Readonly<Record<Ocr18Language, number>>;
16
+ };
17
+ export type Ocr18RunResult = {
18
+ readonly state: Ocr18State;
19
+ readonly findings: readonly Ocr18Finding[];
20
+ readonly summary: Ocr18Summary | null;
21
+ readonly durationMs: number;
22
+ readonly rawOutput: string;
23
+ };
24
+ export type Ocr18RunOptions = {
25
+ readonly cwd: string;
26
+ readonly language: string;
27
+ readonly from?: string;
28
+ readonly to?: string;
29
+ readonly commit?: string;
30
+ readonly delegate?: boolean;
31
+ readonly timeoutMs?: number;
32
+ };
33
+ export declare function buildOcr18Args(options: Ocr18RunOptions): string[];
34
+ export declare function runOcr18(options: Ocr18RunOptions): Ocr18RunResult;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * OCR 1.8.x multi-language reviewer adapter. Routes one of the
3
+ * 8 supported languages to the corresponding `ocr review` filter.
4
+ */
5
+ import { spawnSync } from 'node:child_process';
6
+ export const OCR_18_PACKAGE = '@alibaba-group/open-code-review@1.8.9';
7
+ export const OCR_18_LANGUAGES = [
8
+ 'python',
9
+ 'go',
10
+ 'java',
11
+ 'rust',
12
+ 'cpp',
13
+ 'csharp',
14
+ 'ruby',
15
+ 'php'
16
+ ];
17
+ function isSupportedLanguage(value) {
18
+ return OCR_18_LANGUAGES.includes(value);
19
+ }
20
+ function severityFor(value) {
21
+ if (value === 'error' || value === 'critical' || value === 2)
22
+ return 'error';
23
+ if (value === 'warn' || value === 'warning' || value === 1)
24
+ return 'warn';
25
+ return 'info';
26
+ }
27
+ function emptyByLanguage() {
28
+ const out = {};
29
+ for (const lang of OCR_18_LANGUAGES) {
30
+ out[lang] = 0;
31
+ }
32
+ return out;
33
+ }
34
+ export function buildOcr18Args(options) {
35
+ if (!isSupportedLanguage(options.language)) {
36
+ throw Object.assign(new Error(`unsupported language: ${options.language}`), { code: 'LANGUAGE_UNSUPPORTED' });
37
+ }
38
+ const args = [
39
+ '--package', OCR_18_PACKAGE,
40
+ '--', 'ocr'
41
+ ];
42
+ if (options.delegate === true) {
43
+ args.push('delegate', 'preview');
44
+ if (options.from !== undefined)
45
+ args.push('--from', options.from);
46
+ if (options.to !== undefined)
47
+ args.push('--to', options.to);
48
+ if (options.commit !== undefined)
49
+ args.push('--commit', options.commit);
50
+ }
51
+ else {
52
+ args.push('review', '--filter-language', options.language);
53
+ if (options.from !== undefined)
54
+ args.push('--from', options.from);
55
+ if (options.to !== undefined)
56
+ args.push('--to', options.to);
57
+ if (options.commit !== undefined)
58
+ args.push('--commit', options.commit);
59
+ args.push('--format', 'json');
60
+ }
61
+ return args;
62
+ }
63
+ export function runOcr18(options) {
64
+ const start = Date.now();
65
+ if (!isSupportedLanguage(options.language)) {
66
+ return {
67
+ state: 'language-unsupported',
68
+ findings: [],
69
+ summary: null,
70
+ durationMs: Date.now() - start,
71
+ rawOutput: `unsupported language: ${options.language}`
72
+ };
73
+ }
74
+ const args = buildOcr18Args(options);
75
+ const spawnOptions = {
76
+ cwd: options.cwd,
77
+ encoding: 'utf8',
78
+ timeout: options.timeoutMs ?? 60_000,
79
+ maxBuffer: 32 * 1024 * 1024
80
+ };
81
+ const result = spawnSync('npx', args, spawnOptions);
82
+ const stdout = typeof result.stdout === 'string' ? result.stdout : '';
83
+ const stderr = typeof result.stderr === 'string' ? result.stderr : '';
84
+ if (result.error !== undefined && result.error !== null) {
85
+ return {
86
+ state: 'ocr18-missing',
87
+ findings: [],
88
+ summary: null,
89
+ durationMs: Date.now() - start,
90
+ rawOutput: stderr || stdout
91
+ };
92
+ }
93
+ if (result.status !== 0) {
94
+ return {
95
+ state: 'execution-failed',
96
+ findings: [],
97
+ summary: null,
98
+ durationMs: Date.now() - start,
99
+ rawOutput: stderr || stdout
100
+ };
101
+ }
102
+ let findings = [];
103
+ if (stdout.trim().length > 0) {
104
+ try {
105
+ const parsed = JSON.parse(stdout);
106
+ if (Array.isArray(parsed.findings)) {
107
+ for (const entry of parsed.findings) {
108
+ if (entry === null || typeof entry !== 'object')
109
+ continue;
110
+ const f = entry;
111
+ findings.push({
112
+ filePath: typeof f.file === 'string' ? f.file : '',
113
+ line: typeof f.line === 'number' ? f.line : 0,
114
+ ruleId: typeof f.rule === 'string' ? f.rule : null,
115
+ severity: severityFor(f.severity),
116
+ message: typeof f.message === 'string' ? f.message : ''
117
+ });
118
+ }
119
+ }
120
+ }
121
+ catch {
122
+ return {
123
+ state: 'execution-failed',
124
+ findings: [],
125
+ summary: null,
126
+ durationMs: Date.now() - start,
127
+ rawOutput: stderr || stdout
128
+ };
129
+ }
130
+ }
131
+ const bySeverity = { error: 0, warn: 0, info: 0 };
132
+ for (const f of findings)
133
+ bySeverity[f.severity]++;
134
+ const byLanguageMut = { ...emptyByLanguage() };
135
+ byLanguageMut[options.language] = findings.length;
136
+ return {
137
+ state: 'ok',
138
+ findings,
139
+ summary: { total: findings.length, bySeverity, byLanguage: byLanguageMut },
140
+ durationMs: Date.now() - start,
141
+ rawOutput: stdout
142
+ };
143
+ }