peaks-loop 4.0.15 → 4.0.17

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 (90) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README-en.md +12 -9
  3. package/README.md +15 -4
  4. package/config/eslint/.peaks-rules.cjs +47 -27
  5. package/dist/cli/commands/_register.js +4 -2
  6. package/dist/cli/commands/audit-commands.d.ts +1 -1
  7. package/dist/cli/commands/code-gate-command.d.ts +25 -0
  8. package/dist/cli/commands/code-gate-command.js +86 -0
  9. package/dist/cli/commands/code-mode-gate-commands.js +3 -4
  10. package/dist/cli/commands/code-orchestrator-can-do.js +2 -1
  11. package/dist/cli/commands/code-review-commands.d.ts +2 -30
  12. package/dist/cli/commands/code-review-commands.js +43 -65
  13. package/dist/cli/commands/cron-commands.js +16 -4
  14. package/dist/cli/commands/dashboard-long-run.js +8 -4
  15. package/dist/cli/commands/dispatch-from-dag.d.ts +1 -1
  16. package/dist/cli/commands/hooks-commands.js +56 -3
  17. package/dist/cli/commands/lint-commands.d.ts +21 -0
  18. package/dist/cli/commands/lint-commands.js +146 -0
  19. package/dist/cli/commands/qa-commands.js +1 -2
  20. package/dist/cli/commands/security-audit-commands.d.ts +1 -1
  21. package/dist/cli/commands/security-audit-commands.js +1 -1
  22. package/dist/cli/commands/sediment-commands.d.ts +1 -1
  23. package/dist/cli/commands/slice-commands.js +20 -8
  24. package/dist/cli/commands/statusline-commands.d.ts +1 -0
  25. package/dist/cli/commands/statusline-commands.js +17 -3
  26. package/dist/cli/commands/tech-commands.d.ts +1 -1
  27. package/dist/services/artifacts/request-artifact-service.d.ts +2 -2
  28. package/dist/services/artifacts/request-artifact-service.js +10 -1
  29. package/dist/services/audit/enforcers/lint-reference-shape.js +18 -4
  30. package/dist/services/code/auto-compact-orchestrator.js +9 -1
  31. package/dist/services/code/batch-heartbeat-poller.js +26 -0
  32. package/dist/services/code/orchestrator-can-do.d.ts +24 -0
  33. package/dist/services/code/orchestrator-can-do.js +48 -2
  34. package/dist/services/code/post-compact-detector.d.ts +1 -1
  35. package/dist/services/code-review/ecc-bridge.d.ts +2 -7
  36. package/dist/services/compact-statusline/compact-statusline-service.js +19 -11
  37. package/dist/services/config/config-service.d.ts +2 -19
  38. package/dist/services/config/config-service.js +2 -56
  39. package/dist/services/config/config-types.d.ts +4 -24
  40. package/dist/services/config/config-types.js +1 -10
  41. package/dist/services/container/container-lease.js +21 -8
  42. package/dist/services/context/spillover-store.d.ts +1 -1
  43. package/dist/services/crystallization/crystallization-types.js +20 -8
  44. package/dist/services/crystallization/evidence-brief-builder.js +22 -11
  45. package/dist/services/dispatch/dispatch-record-writer.js +231 -141
  46. package/dist/services/evolution/evolution-types.js +38 -21
  47. package/dist/services/feedback/feedback-promotion-service.js +9 -1
  48. package/dist/services/hooks/pre-tool-code-gate.d.ts +46 -0
  49. package/dist/services/hooks/pre-tool-code-gate.js +91 -0
  50. package/dist/services/ide/current-model-detector.js +1 -2
  51. package/dist/services/lint/detect-eslint.d.ts +10 -0
  52. package/dist/services/lint/detect-eslint.js +62 -0
  53. package/dist/services/lint/detect-ocr-18.d.ts +10 -0
  54. package/dist/services/lint/detect-ocr-18.js +40 -0
  55. package/dist/services/lint/eslint-runner.d.ts +57 -0
  56. package/dist/services/lint/eslint-runner.js +370 -0
  57. package/dist/services/lint/npx-resolver.d.ts +6 -0
  58. package/dist/services/lint/npx-resolver.js +47 -0
  59. package/dist/services/lint/ocr-multilang-adapter.d.ts +34 -0
  60. package/dist/services/lint/ocr-multilang-adapter.js +143 -0
  61. package/dist/services/loop/loop-release-types.js +32 -13
  62. package/dist/services/loop/spec-service.d.ts +7 -1
  63. package/dist/services/loop/spec-service.js +218 -182
  64. package/dist/services/rd/rd-service.js +22 -9
  65. package/dist/services/scan/archetype-service.js +21 -9
  66. package/dist/services/session/binding-store.js +1 -2
  67. package/dist/services/share/bundle-reader.d.ts +6 -9
  68. package/dist/services/share/bundle-reader.js +297 -223
  69. package/dist/services/skills/hooks-settings-service.d.ts +33 -0
  70. package/dist/services/skills/hooks-settings-service.js +27 -1
  71. package/dist/services/slice/calibration-store.js +21 -8
  72. package/dist/services/slice/slice-check-service.js +35 -14
  73. package/dist/services/slice/slice-decompose-service.js +459 -223
  74. package/dist/services/standards/project-context.js +135 -73
  75. package/dist/services/verdict/envelopes.d.ts +1 -2
  76. package/dist/services/verdict/envelopes.js +1 -2
  77. package/dist/services/verdict/verdict-aggregator.d.ts +1 -2
  78. package/dist/services/vm/vm-lease.js +19 -8
  79. package/dist/services/worktree/worktree-lease.d.ts +0 -10
  80. package/dist/services/worktree/worktree-lease.js +19 -8
  81. package/dist/shared/fs-utils.d.ts +1 -1
  82. package/package.json +8 -12
  83. package/skills/bee/peaks-rd/SKILL.md +1 -1
  84. package/skills/bee/peaks-rd/references/jsts-eslint-gate.md +197 -0
  85. package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +132 -0
  86. package/skills/bee/peaks-rd/references/parallel-review-fanout.md +1 -1
  87. package/skills/peaks-code/SKILL.md +47 -1
  88. package/dist/services/code-review/ocr-service.d.ts +0 -129
  89. package/dist/services/code-review/ocr-service.js +0 -372
  90. package/skills/bee/peaks-rd/references/ocr-integration.md +0 -229
@@ -0,0 +1,370 @@
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 { existsSync, readFileSync } from 'node:fs';
22
+ import { dirname, join } from 'node:path';
23
+ import { resolveNpxInvocation } from './npx-resolver.js';
24
+ /**
25
+ * PRD-002b slice 2 — extract runner-pipeline magic numbers (ESLint
26
+ * severity codes, buffer / timeout budgets, max project-root walk
27
+ * depth, red-line top-N aggregation cap, base severity defaults).
28
+ * Values are bytewise-identical to the original literals.
29
+ */
30
+ const ESLINT_SEVERITY_ERROR = 2;
31
+ const ESLINT_SEVERITY_WARN = 1;
32
+ const ESLINT_DEFAULT_TIMEOUT_MS = 60_000;
33
+ const KB_PER_MB = 1024;
34
+ const BYTES_PER_KB = 1024;
35
+ const MB_TO_BYTES = KB_PER_MB * BYTES_PER_KB;
36
+ const DIFF_BUFFER_BYTES = 16 * MB_TO_BYTES;
37
+ const OUTPUT_BUFFER_BYTES = 32 * MB_TO_BYTES;
38
+ const PROJECT_ROOT_WALK_MAX_DEPTH = 8;
39
+ const RED_LINE_TOP_FILES = 5;
40
+ export const ESLINT_PACKAGE_PINS = {
41
+ eslint: '8.57.1',
42
+ typescriptEslintParser: '8.66.0',
43
+ typescriptEslintPlugin: '8.66.0'
44
+ };
45
+ function severityFor(value) {
46
+ if (value === ESLINT_SEVERITY_ERROR)
47
+ return 'error';
48
+ if (value === ESLINT_SEVERITY_WARN)
49
+ return 'warn';
50
+ return 'info';
51
+ }
52
+ function summarize(findings) {
53
+ let error = 0;
54
+ let warn = 0;
55
+ let info = 0;
56
+ for (const f of findings) {
57
+ if (f.severity === 'error')
58
+ error++;
59
+ else if (f.severity === 'warn')
60
+ warn++;
61
+ else
62
+ info++;
63
+ }
64
+ return { error, warn, info };
65
+ }
66
+ /**
67
+ * Read `git diff HEAD --unified=0` and parse every `+` line as a
68
+ * touched line number. Falls back to [] on any parse error so the
69
+ * caller treats all findings as out-of-diff (no silent zero-result).
70
+ */
71
+ function resolveProjectRoot(cwd) {
72
+ // ESLint 8 auto-discovers `.eslintrc.*` from cwd upward. When the
73
+ // CLI is launched via `node bin/peaks.js`, cwd is the bin/ dir and
74
+ // ESLint fails to find the config. Walk up until we see
75
+ // `config/eslint/.peaks-rules.cjs` and use that as the project root.
76
+ const marker = join('config', 'eslint', '.peaks-rules.cjs');
77
+ let current = cwd;
78
+ for (let depth = 0; depth < PROJECT_ROOT_WALK_MAX_DEPTH; depth += 1) {
79
+ if (existsSync(join(current, marker)))
80
+ return current;
81
+ const parent = dirname(current);
82
+ if (parent === current)
83
+ break;
84
+ current = parent;
85
+ }
86
+ return cwd;
87
+ }
88
+ function loadDiffRanges(cwd) {
89
+ const ranges = [];
90
+ try {
91
+ const result = spawnSync('git', ['diff', 'HEAD', '--unified=0', '--no-color'], {
92
+ cwd,
93
+ encoding: 'utf8',
94
+ maxBuffer: DIFF_BUFFER_BYTES
95
+ });
96
+ if (result.status !== 0 || typeof result.stdout !== 'string')
97
+ return [];
98
+ const stdout = result.stdout;
99
+ let currentFile = null;
100
+ let currentLine = 0;
101
+ for (const raw of stdout.split('\n')) {
102
+ const line = raw;
103
+ if (line.startsWith('+++ ')) {
104
+ const path = line.slice(4).split('\t')[0] ?? '';
105
+ currentFile = path.startsWith('b/') ? path.slice(2) : path;
106
+ continue;
107
+ }
108
+ if (line.startsWith('--- ')) {
109
+ continue;
110
+ }
111
+ const hunk = /^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/.exec(line);
112
+ if (hunk !== null) {
113
+ currentLine = Number.parseInt(hunk[1] ?? '0', 10);
114
+ continue;
115
+ }
116
+ if (currentFile !== null && line.startsWith('+') && !line.startsWith('+++')) {
117
+ if (Number.isFinite(currentLine) && currentLine > 0) {
118
+ ranges.push({ file: currentFile, lines: [currentLine] });
119
+ }
120
+ currentLine += 1;
121
+ }
122
+ }
123
+ }
124
+ catch {
125
+ return [];
126
+ }
127
+ return ranges;
128
+ }
129
+ function inDiff(filePath, line, ranges) {
130
+ if (ranges.length === 0)
131
+ return false;
132
+ for (const r of ranges) {
133
+ if (r.file !== filePath)
134
+ continue;
135
+ for (const ln of r.lines) {
136
+ if (Math.abs(ln - line) <= 0)
137
+ return true;
138
+ }
139
+ }
140
+ return false;
141
+ }
142
+ function loadBaseline(cwd, baselineFile) {
143
+ const fullPath = join(cwd, baselineFile);
144
+ let raw;
145
+ try {
146
+ raw = readFileSync(fullPath, 'utf8');
147
+ }
148
+ catch {
149
+ return [];
150
+ }
151
+ let parsed;
152
+ try {
153
+ parsed = JSON.parse(raw);
154
+ }
155
+ catch {
156
+ return [];
157
+ }
158
+ const violations = Array.isArray(parsed.violations) ? parsed.violations : [];
159
+ const out = [];
160
+ for (const v of violations) {
161
+ if (typeof v.ruleId !== 'string' || typeof v.file !== 'string' || typeof v.line !== 'number')
162
+ continue;
163
+ out.push({
164
+ ruleId: v.ruleId,
165
+ file: v.file,
166
+ line: v.line,
167
+ severity: severityFor(v.severity),
168
+ message: typeof v.message === 'string' ? v.message : ''
169
+ });
170
+ }
171
+ return out;
172
+ }
173
+ function matchBaseline(finding, baseline) {
174
+ for (const v of baseline) {
175
+ if (v.ruleId !== finding.ruleId)
176
+ continue;
177
+ if (v.file !== finding.filePath)
178
+ continue;
179
+ if (v.line !== finding.line)
180
+ continue;
181
+ return true;
182
+ }
183
+ return false;
184
+ }
185
+ function aggregateRedLine(baseline) {
186
+ const byRule = new Map();
187
+ for (const v of baseline) {
188
+ const existing = byRule.get(v.ruleId);
189
+ if (existing === undefined) {
190
+ const fileCounts = new Map();
191
+ fileCounts.set(v.file, 1);
192
+ byRule.set(v.ruleId, { count: 1, fileCounts });
193
+ }
194
+ else {
195
+ existing.count += 1;
196
+ existing.fileCounts.set(v.file, (existing.fileCounts.get(v.file) ?? 0) + 1);
197
+ }
198
+ }
199
+ const out = [];
200
+ for (const [ruleId, agg] of byRule.entries()) {
201
+ const topFiles = [...agg.fileCounts.entries()]
202
+ .map(([file, count]) => ({ file, count }))
203
+ .sort((a, b) => b.count - a.count)
204
+ .slice(0, RED_LINE_TOP_FILES);
205
+ out.push({ ruleId, count: agg.count, topFiles });
206
+ }
207
+ out.sort((a, b) => b.count - a.count);
208
+ return out;
209
+ }
210
+ const EMPTY_DIFF_RANGE = [];
211
+ const EMPTY_REDLINE = [];
212
+ function emptyResult(state, start, rawOutput) {
213
+ return {
214
+ state,
215
+ findings: [],
216
+ summary: { error: 0, warn: 0, info: 0 },
217
+ durationMs: Date.now() - start,
218
+ rawOutput,
219
+ baselineWaived: [],
220
+ redLine: EMPTY_REDLINE
221
+ };
222
+ }
223
+ export function buildEslintArgs(options) {
224
+ if (options.fix === true || options.write === true) {
225
+ throw Object.assign(new Error('peaks code lint is read-only; --fix and --write are forbidden'), {
226
+ code: 'LINT_FIX_FORBIDDEN'
227
+ });
228
+ }
229
+ // The runner now uses the locally-installed eslint binary
230
+ // (`./node_modules/eslint/bin/eslint.js`) instead of the npx
231
+ // --package wrapper, which is broken on Windows (npm 10.9.4 chdirs
232
+ // the child to its own cache bin, breaking config auto-discovery).
233
+ // The pin constants are kept for detect-eslint's npm-registry
234
+ // probe + for the npx-resolver fallback path.
235
+ const args = ['--format', 'json'];
236
+ // Always pass the legacy .peaks-rules.cjs path; ESLint 8
237
+ // auto-discovers only `.eslintrc.*` files and our config lives at
238
+ // `config/eslint/.peaks-rules.cjs`. Callers may override via
239
+ // `options.configPath`.
240
+ const effectiveConfigPath = options.configPath ?? join('config', 'eslint', '.peaks-rules.cjs');
241
+ args.push('--config', effectiveConfigPath);
242
+ args.push(...(options.scope !== undefined && options.scope.length > 0 ? [options.scope] : ['.']));
243
+ return args;
244
+ }
245
+ export function runEslint(options) {
246
+ const start = Date.now();
247
+ let args;
248
+ try {
249
+ args = buildEslintArgs(options);
250
+ }
251
+ catch (error) {
252
+ return emptyResult('execution-failed', start, error instanceof Error ? error.message : String(error));
253
+ }
254
+ const projectRoot = resolveProjectRoot(options.cwd);
255
+ // Invoke eslint via `node <node_modules/eslint/bin/eslint.js>` to bypass
256
+ // the Windows .cmd shim entirely (Node 22 spawnSync cannot run .cmd
257
+ // shims without shell:true, and shell:true mangles quoted args).
258
+ const localEslintJs = join(projectRoot, 'node_modules', 'eslint', 'bin', 'eslint.js');
259
+ const useLocal = existsSync(localEslintJs);
260
+ const spawnOptions = {
261
+ cwd: projectRoot,
262
+ encoding: 'utf8',
263
+ timeout: options.timeoutMs ?? ESLINT_DEFAULT_TIMEOUT_MS,
264
+ maxBuffer: OUTPUT_BUFFER_BYTES
265
+ };
266
+ let command;
267
+ let invocationArgs;
268
+ let baseEnv;
269
+ if (useLocal) {
270
+ command = process.execPath;
271
+ invocationArgs = [localEslintJs, ...args];
272
+ baseEnv = process.env;
273
+ }
274
+ else {
275
+ // Fallback: resolve `npx` through the user's bundled npm install to
276
+ // bypass the Windows .cmd shim + shell-quoting issues.
277
+ const resolved = resolveNpxInvocation([
278
+ '--package', `eslint@${ESLINT_PACKAGE_PINS.eslint}`,
279
+ '--package', `@typescript-eslint/parser@${ESLINT_PACKAGE_PINS.typescriptEslintParser}`,
280
+ '--package', `@typescript-eslint/eslint-plugin@${ESLINT_PACKAGE_PINS.typescriptEslintPlugin}`,
281
+ '--', 'eslint', ...args
282
+ ]);
283
+ command = resolved.command;
284
+ invocationArgs = resolved.args;
285
+ baseEnv = resolved.baseEnv;
286
+ }
287
+ const result = spawnSync(command, invocationArgs, { ...spawnOptions, env: baseEnv });
288
+ if (result.error !== undefined && result.error !== null) {
289
+ const message = result.error.message;
290
+ const state = /ENOENT/.test(message) ? 'npx-failed' : 'execution-failed';
291
+ return emptyResult(state, start, typeof result.stdout === 'string' ? result.stdout : '');
292
+ }
293
+ if (result.signal !== null && result.signal !== undefined) {
294
+ return emptyResult('execution-failed', start, typeof result.stdout === 'string' ? result.stdout : '');
295
+ }
296
+ const stdout = typeof result.stdout === 'string' ? result.stdout : '';
297
+ let findings = [];
298
+ if (stdout.trim().length > 0) {
299
+ try {
300
+ const parsed = JSON.parse(stdout);
301
+ for (const entry of parsed) {
302
+ if (typeof entry !== 'object' || entry === null)
303
+ continue;
304
+ const parentFile = typeof entry.filePath === 'string'
305
+ ? entry.filePath
306
+ : '';
307
+ const messages = Array.isArray(entry.messages)
308
+ ? entry.messages
309
+ : [];
310
+ for (const m of messages) {
311
+ if (m === null || typeof m !== 'object')
312
+ continue;
313
+ findings.push({
314
+ filePath: typeof m.filePath === 'string' && m.filePath.length > 0 ? m.filePath : parentFile,
315
+ line: typeof m.line === 'number' ? m.line : 0,
316
+ column: typeof m.column === 'number' ? m.column : 0,
317
+ ruleId: typeof m.ruleId === 'string' ? m.ruleId : null,
318
+ severity: severityFor(m.severity),
319
+ message: typeof m.message === 'string' ? m.message : ''
320
+ });
321
+ }
322
+ }
323
+ }
324
+ catch {
325
+ return emptyResult('execution-failed', start, stdout);
326
+ }
327
+ }
328
+ if (result.status !== 0 && findings.length === 0) {
329
+ return emptyResult('eslint-missing', start, typeof result.stderr === 'string' ? result.stderr : stdout);
330
+ }
331
+ // PRD-002b slice: incremental-first / no-touch-stockcode filters.
332
+ // PRD-002b D6: baselineFile is project-level; if missing we surface
333
+ // state='baseline-missing' so the caller can re-run `peaks lint baseline`.
334
+ const diffOnly = options.diffOnly !== false;
335
+ const redLineMode = options.redLineMode ?? 'baseline-aware';
336
+ const baselinePath = options.baselineFile ?? '.peaks/lint/baseline.json';
337
+ const baseline = loadBaseline(options.cwd, baselinePath);
338
+ const diffRanges = diffOnly ? loadDiffRanges(options.cwd) : EMPTY_DIFF_RANGE;
339
+ let activeFindings = findings;
340
+ let waived = [];
341
+ if (diffOnly) {
342
+ activeFindings = findings.filter((f) => inDiff(f.filePath, f.line, diffRanges));
343
+ }
344
+ if (baseline.length > 0) {
345
+ const next = [];
346
+ waived = [];
347
+ for (const f of activeFindings) {
348
+ if (matchBaseline(f, baseline)) {
349
+ waived.push(f);
350
+ }
351
+ else {
352
+ next.push(f);
353
+ }
354
+ }
355
+ activeFindings = next;
356
+ }
357
+ const redLine = redLineMode === 'baseline-aware' ? aggregateRedLine(baseline) : EMPTY_REDLINE;
358
+ const finalState = baseline.length === 0 && diffOnly && options.baselineFile !== undefined
359
+ ? 'baseline-missing'
360
+ : 'ok';
361
+ return {
362
+ state: finalState,
363
+ findings: activeFindings,
364
+ summary: summarize(activeFindings),
365
+ durationMs: Date.now() - start,
366
+ rawOutput: stdout,
367
+ baselineWaived: waived,
368
+ redLine
369
+ };
370
+ }
@@ -0,0 +1,6 @@
1
+ export type NpxInvocation = {
2
+ readonly command: string;
3
+ readonly args: readonly string[];
4
+ readonly baseEnv: NodeJS.ProcessEnv;
5
+ };
6
+ export declare function resolveNpxInvocation(npxArgs: readonly string[]): NpxInvocation;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Cross-platform `npx` resolver.
3
+ *
4
+ * On Windows, `npm` installs `npx` as a `.cmd` shim; Node 22's
5
+ * `child_process.spawnSync` refuses to invoke it unless `shell: true`
6
+ * is set, and `shell: true` corrupts quoted `--package` arguments.
7
+ * Rather than depend on shell quoting, this helper resolves the
8
+ * npx script bundled with the user's `npm` install and invokes it
9
+ * via `node <npx-cli.js>` with the same argv. macOS / Linux continue
10
+ * to use the regular `npx` binary.
11
+ */
12
+ import { existsSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import process from 'node:process';
15
+ function locateNpxCliScript() {
16
+ const candidates = process.platform === 'win32'
17
+ ? [
18
+ join(process.execPath, '..', '..', 'node_modules', 'npm', 'bin', 'npx-cli.js'),
19
+ 'C:/nvm4w/nodejs/node_modules/npm/bin/npx-cli.js',
20
+ 'C:/Program Files/nodejs/node_modules/npm/bin/npx-cli.js'
21
+ ]
22
+ : [
23
+ join(process.execPath, '..', '..', 'lib', 'node_modules', 'npm', 'bin', 'npx-cli.js')
24
+ ];
25
+ for (const candidate of candidates) {
26
+ if (existsSync(candidate))
27
+ return candidate;
28
+ }
29
+ return null;
30
+ }
31
+ export function resolveNpxInvocation(npxArgs) {
32
+ if (process.platform === 'win32') {
33
+ const cliScript = locateNpxCliScript();
34
+ if (cliScript !== null) {
35
+ return {
36
+ command: process.execPath,
37
+ args: [cliScript, ...npxArgs],
38
+ baseEnv: process.env
39
+ };
40
+ }
41
+ }
42
+ return {
43
+ command: 'npx',
44
+ args: npxArgs,
45
+ baseEnv: process.env
46
+ };
47
+ }
@@ -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
+ }
@@ -1,4 +1,23 @@
1
1
  import { z } from "zod";
2
+ /* ---------------------------------------------------------------------- */
3
+ /* PRD-002b slice 2 — schema-limit constants extracted from inline */
4
+ /* `.max(N)` calls so the no-magic-numbers rule stops flagging the */
5
+ /* constraint values. Names describe the field, not just the number. */
6
+ /* Bytewise-identical to the original literals (signaled in commit). */
7
+ /* ---------------------------------------------------------------------- */
8
+ const LOOP_ID_MAX = 64;
9
+ const LOOP_SHARE_EXCLUDED_PATH_MAX = 4096;
10
+ const LOOP_NAME_MAX = 200;
11
+ const LOOP_SCENARIO_MAX = 4000;
12
+ const LOOP_TRIGGER_POLICY_MAX = 2000;
13
+ const LOOP_SUCCESS_CRITERIA_ITEM_MAX = 1000;
14
+ const LOOP_INTERACTION_POLICY_MAX = 2000;
15
+ const LOOP_FEEDBACK_POLICY_MAX = 2000;
16
+ const LOOP_EVOLUTION_POLICY_MAX = 2000;
17
+ const LOOP_EVALUATOR_POLICY_ITEM_MAX = 1000;
18
+ const LOOP_LINKED_BEE_MAX = 64;
19
+ const LOOP_HISTORY_ITEM_MAX = 128;
20
+ const LOOP_CRYSTALLIZATION_EVIDENCE_MAX = 128;
2
21
  /**
3
22
  * LoopRelease — spec §4.1 (Loop Engineering Asset).
4
23
  *
@@ -74,7 +93,7 @@ export const LOOP_RELEASE_M3_FIELDS = [
74
93
  export const LoopReleaseM3ExtensionSchema = z.object({
75
94
  shareable: z.boolean().default(true),
76
95
  share_excluded_paths: z
77
- .array(z.string().min(1).max(4096))
96
+ .array(z.string().min(1).max(LOOP_SHARE_EXCLUDED_PATH_MAX))
78
97
  .default([]),
79
98
  desktop_visible: z.boolean().default(true),
80
99
  export_bundle_format: z
@@ -92,27 +111,27 @@ export const LoopReleaseInputSchema = z
92
111
  id: z
93
112
  .string()
94
113
  .min(1)
95
- .max(64)
114
+ .max(LOOP_ID_MAX)
96
115
  .regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, {
97
116
  message: "id must be kebab-case starting with a lowercase letter (e.g. loop-onboarding-research)",
98
117
  }),
99
- name: z.string().min(1).max(200),
100
- scenario: z.string().trim().min(1).max(4000),
101
- trigger_policy: z.string().trim().min(1).max(2000),
118
+ name: z.string().min(1).max(LOOP_NAME_MAX),
119
+ scenario: z.string().trim().min(1).max(LOOP_SCENARIO_MAX),
120
+ trigger_policy: z.string().trim().min(1).max(LOOP_TRIGGER_POLICY_MAX),
102
121
  success_criteria: z
103
- .array(z.string().trim().min(1).max(1000))
122
+ .array(z.string().trim().min(1).max(LOOP_SUCCESS_CRITERIA_ITEM_MAX))
104
123
  .min(1, "success_criteria must list at least one declarative criterion"),
105
124
  interaction_policy: z
106
125
  .string()
107
126
  .trim()
108
127
  .min(1, "interaction_policy must declare human-NL-choice-only semantics")
109
- .max(2000),
110
- feedback_policy: z.string().trim().min(1).max(2000),
111
- evolution_policy: z.string().trim().min(1).max(2000),
112
- evaluator_policy: z.array(z.string().trim().min(1).max(1000)).min(1),
113
- linked_bees: z.array(z.string().min(1).max(64)).default([]),
114
- run_history: z.array(z.string().min(1).max(128)).default([]),
115
- crystallization_evidence: z.array(z.string().min(1).max(128)).default([]),
128
+ .max(LOOP_INTERACTION_POLICY_MAX),
129
+ feedback_policy: z.string().trim().min(1).max(LOOP_FEEDBACK_POLICY_MAX),
130
+ evolution_policy: z.string().trim().min(1).max(LOOP_EVOLUTION_POLICY_MAX),
131
+ evaluator_policy: z.array(z.string().trim().min(1).max(LOOP_EVALUATOR_POLICY_ITEM_MAX)).min(1),
132
+ linked_bees: z.array(z.string().min(1).max(LOOP_LINKED_BEE_MAX)).default([]),
133
+ run_history: z.array(z.string().min(1).max(LOOP_HISTORY_ITEM_MAX)).default([]),
134
+ crystallization_evidence: z.array(z.string().min(1).max(LOOP_CRYSTALLIZATION_EVIDENCE_MAX)).default([]),
116
135
  lifecycle_status: LoopReleaseLifecycleStatusSchema,
117
136
  version: z
118
137
  .string()
@@ -57,7 +57,13 @@ export declare function resolveLoopSpec(projectRoot: string, sid: string, rid: s
57
57
  * `buildSpec` preserves raw values so out-of-range entries surface in
58
58
  * `lintLoopSpec` rather than being silently clamped. */
59
59
  export declare function buildSpec(input: Partial<LoopSpec>, expectedRid: string): LoopSpec;
60
- /** Lint a LoopSpec — returns a report with semantic errors / warnings. */
60
+ /** Lint a LoopSpec — returns a report with semantic errors / warnings.
61
+ *
62
+ * Refactored 2026-08-07 (PRD-002b slice 3 commit A — extract-method):
63
+ * the original function had complexity 14. Each validation phase now lives
64
+ * in its own helper, dropping the orchestrator to complexity ~5.
65
+ * Behavior is byte-for-byte preserved: same error/warning strings, same
66
+ * ordering, same return shape. */
61
67
  export declare function lintLoopSpec(spec: LoopSpec): SpecLintReport;
62
68
  /** Serialize a LoopSpec to a stable YAML representation. Pure. */
63
69
  export declare function serializeSpec(spec: LoopSpec): string;