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.
- package/CHANGELOG.md +82 -0
- package/README-en.md +12 -9
- package/README.md +15 -4
- package/config/eslint/.peaks-rules.cjs +47 -27
- package/dist/cli/commands/_register.js +4 -2
- package/dist/cli/commands/audit-commands.d.ts +1 -1
- package/dist/cli/commands/code-gate-command.d.ts +25 -0
- package/dist/cli/commands/code-gate-command.js +86 -0
- package/dist/cli/commands/code-mode-gate-commands.js +3 -4
- package/dist/cli/commands/code-orchestrator-can-do.js +2 -1
- package/dist/cli/commands/code-review-commands.d.ts +2 -30
- package/dist/cli/commands/code-review-commands.js +43 -65
- package/dist/cli/commands/cron-commands.js +16 -4
- package/dist/cli/commands/dashboard-long-run.js +8 -4
- package/dist/cli/commands/dispatch-from-dag.d.ts +1 -1
- package/dist/cli/commands/hooks-commands.js +56 -3
- package/dist/cli/commands/lint-commands.d.ts +21 -0
- package/dist/cli/commands/lint-commands.js +146 -0
- package/dist/cli/commands/qa-commands.js +1 -2
- package/dist/cli/commands/security-audit-commands.d.ts +1 -1
- package/dist/cli/commands/security-audit-commands.js +1 -1
- package/dist/cli/commands/sediment-commands.d.ts +1 -1
- package/dist/cli/commands/slice-commands.js +20 -8
- package/dist/cli/commands/statusline-commands.d.ts +1 -0
- package/dist/cli/commands/statusline-commands.js +17 -3
- package/dist/cli/commands/tech-commands.d.ts +1 -1
- package/dist/services/artifacts/request-artifact-service.d.ts +2 -2
- package/dist/services/artifacts/request-artifact-service.js +10 -1
- package/dist/services/audit/enforcers/lint-reference-shape.js +18 -4
- package/dist/services/code/auto-compact-orchestrator.js +9 -1
- package/dist/services/code/batch-heartbeat-poller.js +26 -0
- package/dist/services/code/orchestrator-can-do.d.ts +24 -0
- package/dist/services/code/orchestrator-can-do.js +48 -2
- package/dist/services/code/post-compact-detector.d.ts +1 -1
- package/dist/services/code-review/ecc-bridge.d.ts +2 -7
- package/dist/services/compact-statusline/compact-statusline-service.js +19 -11
- package/dist/services/config/config-service.d.ts +2 -19
- package/dist/services/config/config-service.js +2 -56
- package/dist/services/config/config-types.d.ts +4 -24
- package/dist/services/config/config-types.js +1 -10
- package/dist/services/container/container-lease.js +21 -8
- package/dist/services/context/spillover-store.d.ts +1 -1
- package/dist/services/crystallization/crystallization-types.js +20 -8
- package/dist/services/crystallization/evidence-brief-builder.js +22 -11
- package/dist/services/dispatch/dispatch-record-writer.js +231 -141
- package/dist/services/evolution/evolution-types.js +38 -21
- package/dist/services/feedback/feedback-promotion-service.js +9 -1
- package/dist/services/hooks/pre-tool-code-gate.d.ts +46 -0
- package/dist/services/hooks/pre-tool-code-gate.js +91 -0
- package/dist/services/ide/current-model-detector.js +1 -2
- package/dist/services/lint/detect-eslint.d.ts +10 -0
- package/dist/services/lint/detect-eslint.js +62 -0
- package/dist/services/lint/detect-ocr-18.d.ts +10 -0
- package/dist/services/lint/detect-ocr-18.js +40 -0
- package/dist/services/lint/eslint-runner.d.ts +57 -0
- package/dist/services/lint/eslint-runner.js +370 -0
- package/dist/services/lint/npx-resolver.d.ts +6 -0
- package/dist/services/lint/npx-resolver.js +47 -0
- package/dist/services/lint/ocr-multilang-adapter.d.ts +34 -0
- package/dist/services/lint/ocr-multilang-adapter.js +143 -0
- package/dist/services/loop/loop-release-types.js +32 -13
- package/dist/services/loop/spec-service.d.ts +7 -1
- package/dist/services/loop/spec-service.js +218 -182
- package/dist/services/rd/rd-service.js +22 -9
- package/dist/services/scan/archetype-service.js +21 -9
- package/dist/services/session/binding-store.js +1 -2
- package/dist/services/share/bundle-reader.d.ts +6 -9
- package/dist/services/share/bundle-reader.js +297 -223
- package/dist/services/skills/hooks-settings-service.d.ts +33 -0
- package/dist/services/skills/hooks-settings-service.js +27 -1
- package/dist/services/slice/calibration-store.js +21 -8
- package/dist/services/slice/slice-check-service.js +35 -14
- package/dist/services/slice/slice-decompose-service.js +459 -223
- package/dist/services/standards/project-context.js +135 -73
- package/dist/services/verdict/envelopes.d.ts +1 -2
- package/dist/services/verdict/envelopes.js +1 -2
- package/dist/services/verdict/verdict-aggregator.d.ts +1 -2
- package/dist/services/vm/vm-lease.js +19 -8
- package/dist/services/worktree/worktree-lease.d.ts +0 -10
- package/dist/services/worktree/worktree-lease.js +19 -8
- package/dist/shared/fs-utils.d.ts +1 -1
- package/package.json +8 -12
- package/skills/bee/peaks-rd/SKILL.md +1 -1
- package/skills/bee/peaks-rd/references/jsts-eslint-gate.md +197 -0
- package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +132 -0
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +1 -1
- package/skills/peaks-code/SKILL.md +47 -1
- package/dist/services/code-review/ocr-service.d.ts +0 -129
- package/dist/services/code-review/ocr-service.js +0 -372
- 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,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(
|
|
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(
|
|
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(
|
|
100
|
-
scenario: z.string().trim().min(1).max(
|
|
101
|
-
trigger_policy: z.string().trim().min(1).max(
|
|
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(
|
|
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(
|
|
110
|
-
feedback_policy: z.string().trim().min(1).max(
|
|
111
|
-
evolution_policy: z.string().trim().min(1).max(
|
|
112
|
-
evaluator_policy: z.array(z.string().trim().min(1).max(
|
|
113
|
-
linked_bees: z.array(z.string().min(1).max(
|
|
114
|
-
run_history: z.array(z.string().min(1).max(
|
|
115
|
-
crystallization_evidence: z.array(z.string().min(1).max(
|
|
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;
|