chainlesschain 0.47.9 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/chainlesschain.js +0 -0
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
- package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
- package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/codegen.js +303 -0
- package/src/commands/collab.js +482 -0
- package/src/commands/crosschain.js +382 -0
- package/src/commands/dbevo.js +388 -0
- package/src/commands/dev.js +411 -0
- package/src/commands/federation.js +427 -0
- package/src/commands/fusion.js +332 -0
- package/src/commands/governance.js +505 -0
- package/src/commands/hardening.js +110 -0
- package/src/commands/incentive.js +373 -0
- package/src/commands/inference.js +304 -0
- package/src/commands/infra.js +361 -0
- package/src/commands/kg.js +371 -0
- package/src/commands/marketplace.js +326 -0
- package/src/commands/mcp.js +97 -18
- package/src/commands/nlprog.js +329 -0
- package/src/commands/ops.js +408 -0
- package/src/commands/perception.js +385 -0
- package/src/commands/pqc.js +34 -0
- package/src/commands/privacy.js +345 -0
- package/src/commands/quantization.js +280 -0
- package/src/commands/recommend.js +336 -0
- package/src/commands/reputation.js +349 -0
- package/src/commands/runtime.js +500 -0
- package/src/commands/sla.js +352 -0
- package/src/commands/stress.js +252 -0
- package/src/commands/tech.js +268 -0
- package/src/commands/tenant.js +576 -0
- package/src/commands/trust.js +366 -0
- package/src/harness/mcp-client.js +330 -54
- package/src/index.js +112 -0
- package/src/lib/aiops.js +523 -0
- package/src/lib/autonomous-developer.js +524 -0
- package/src/lib/code-agent.js +442 -0
- package/src/lib/collaboration-governance.js +556 -0
- package/src/lib/community-governance.js +649 -0
- package/src/lib/content-recommendation.js +600 -0
- package/src/lib/cross-chain.js +669 -0
- package/src/lib/dbevo.js +669 -0
- package/src/lib/decentral-infra.js +445 -0
- package/src/lib/federation-hardening.js +587 -0
- package/src/lib/hardening-manager.js +409 -0
- package/src/lib/inference-network.js +407 -0
- package/src/lib/knowledge-graph.js +530 -0
- package/src/lib/mcp-client.js +3 -0
- package/src/lib/multimodal.js +698 -0
- package/src/lib/nl-programming.js +595 -0
- package/src/lib/perception.js +500 -0
- package/src/lib/pqc-manager.js +141 -9
- package/src/lib/privacy-computing.js +575 -0
- package/src/lib/protocol-fusion.js +535 -0
- package/src/lib/quantization.js +362 -0
- package/src/lib/reputation-optimizer.js +509 -0
- package/src/lib/skill-marketplace.js +397 -0
- package/src/lib/sla-manager.js +484 -0
- package/src/lib/stress-tester.js +383 -0
- package/src/lib/tech-learning-engine.js +651 -0
- package/src/lib/tenant-saas.js +831 -0
- package/src/lib/token-incentive.js +513 -0
- package/src/lib/trust-security.js +473 -0
- package/src/lib/universal-runtime.js +771 -0
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Generation Agent 2.0 — CLI port of Phase 86
|
|
3
|
+
* (docs/design/modules/51_代码生成Agent2.0.md).
|
|
4
|
+
*
|
|
5
|
+
* Desktop uses LLM-powered full-stack code generation, AI code review,
|
|
6
|
+
* scaffold generation, and CI/CD configuration.
|
|
7
|
+
* CLI port ships:
|
|
8
|
+
*
|
|
9
|
+
* - Code generation session tracking (prompt/language/framework)
|
|
10
|
+
* - Heuristic code review with security rule detection
|
|
11
|
+
* - Scaffold template catalog and record tracking
|
|
12
|
+
* - CI/CD platform catalog
|
|
13
|
+
*
|
|
14
|
+
* What does NOT port: real LLM code generation, AI-powered review,
|
|
15
|
+
* full-stack generation pipeline, intelligent refactoring.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import crypto from "crypto";
|
|
19
|
+
|
|
20
|
+
/* ── Constants ──────────────────────────────────────────── */
|
|
21
|
+
|
|
22
|
+
export const SCAFFOLD_TEMPLATE = Object.freeze({
|
|
23
|
+
REACT: "react",
|
|
24
|
+
VUE: "vue",
|
|
25
|
+
EXPRESS: "express",
|
|
26
|
+
FASTAPI: "fastapi",
|
|
27
|
+
SPRING_BOOT: "spring_boot",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const REVIEW_SEVERITY = Object.freeze({
|
|
31
|
+
CRITICAL: "critical",
|
|
32
|
+
HIGH: "high",
|
|
33
|
+
MEDIUM: "medium",
|
|
34
|
+
LOW: "low",
|
|
35
|
+
INFO: "info",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const SECURITY_RULE = Object.freeze({
|
|
39
|
+
EVAL_DETECTION: "eval_detection",
|
|
40
|
+
SQL_INJECTION: "sql_injection",
|
|
41
|
+
XSS: "xss",
|
|
42
|
+
PATH_TRAVERSAL: "path_traversal",
|
|
43
|
+
COMMAND_INJECTION: "command_injection",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const CICD_PLATFORM = Object.freeze({
|
|
47
|
+
GITHUB_ACTIONS: "github_actions",
|
|
48
|
+
GITLAB_CI: "gitlab_ci",
|
|
49
|
+
JENKINS: "jenkins",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/* ── State ──────────────────────────────────────────────── */
|
|
53
|
+
|
|
54
|
+
let _generations = new Map();
|
|
55
|
+
let _reviews = new Map();
|
|
56
|
+
let _scaffolds = new Map();
|
|
57
|
+
|
|
58
|
+
function _id() {
|
|
59
|
+
return crypto.randomUUID();
|
|
60
|
+
}
|
|
61
|
+
function _now() {
|
|
62
|
+
return Date.now();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _strip(row) {
|
|
66
|
+
if (!row) return null;
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const [k, v] of Object.entries(row)) {
|
|
69
|
+
if (k !== "_rowid_" && k !== "rowid") out[k] = v;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* ── Schema ─────────────────────────────────────────────── */
|
|
75
|
+
|
|
76
|
+
export function ensureCodeAgentTables(db) {
|
|
77
|
+
db.exec(`CREATE TABLE IF NOT EXISTS code_generations (
|
|
78
|
+
id TEXT PRIMARY KEY,
|
|
79
|
+
prompt TEXT NOT NULL,
|
|
80
|
+
language TEXT,
|
|
81
|
+
framework TEXT,
|
|
82
|
+
generated_code TEXT,
|
|
83
|
+
file_count INTEGER DEFAULT 0,
|
|
84
|
+
token_count INTEGER DEFAULT 0,
|
|
85
|
+
metadata TEXT,
|
|
86
|
+
created_at INTEGER
|
|
87
|
+
)`);
|
|
88
|
+
|
|
89
|
+
db.exec(`CREATE TABLE IF NOT EXISTS code_reviews (
|
|
90
|
+
id TEXT PRIMARY KEY,
|
|
91
|
+
generation_id TEXT,
|
|
92
|
+
code_hash TEXT,
|
|
93
|
+
language TEXT,
|
|
94
|
+
issues_found INTEGER DEFAULT 0,
|
|
95
|
+
security_issues INTEGER DEFAULT 0,
|
|
96
|
+
severity_summary TEXT,
|
|
97
|
+
issues_detail TEXT,
|
|
98
|
+
reviewed_at INTEGER
|
|
99
|
+
)`);
|
|
100
|
+
|
|
101
|
+
db.exec(`CREATE TABLE IF NOT EXISTS code_scaffolds (
|
|
102
|
+
id TEXT PRIMARY KEY,
|
|
103
|
+
template TEXT NOT NULL,
|
|
104
|
+
project_name TEXT,
|
|
105
|
+
options TEXT,
|
|
106
|
+
files_generated INTEGER DEFAULT 0,
|
|
107
|
+
output_path TEXT,
|
|
108
|
+
created_at INTEGER
|
|
109
|
+
)`);
|
|
110
|
+
|
|
111
|
+
_loadAll(db);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function _loadAll(db) {
|
|
115
|
+
_generations.clear();
|
|
116
|
+
_reviews.clear();
|
|
117
|
+
_scaffolds.clear();
|
|
118
|
+
|
|
119
|
+
const tables = [
|
|
120
|
+
["code_generations", _generations],
|
|
121
|
+
["code_reviews", _reviews],
|
|
122
|
+
["code_scaffolds", _scaffolds],
|
|
123
|
+
];
|
|
124
|
+
for (const [table, map] of tables) {
|
|
125
|
+
try {
|
|
126
|
+
for (const row of db.prepare(`SELECT * FROM ${table}`).all()) {
|
|
127
|
+
const r = _strip(row);
|
|
128
|
+
map.set(r.id, r);
|
|
129
|
+
}
|
|
130
|
+
} catch (_e) {
|
|
131
|
+
/* table may not exist */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ── Phase 86: Code Generation ──────────────────────────── */
|
|
137
|
+
|
|
138
|
+
export function createGeneration(
|
|
139
|
+
db,
|
|
140
|
+
{
|
|
141
|
+
prompt,
|
|
142
|
+
language,
|
|
143
|
+
framework,
|
|
144
|
+
generatedCode,
|
|
145
|
+
fileCount,
|
|
146
|
+
tokenCount,
|
|
147
|
+
metadata,
|
|
148
|
+
} = {},
|
|
149
|
+
) {
|
|
150
|
+
if (!prompt) return { generationId: null, reason: "missing_prompt" };
|
|
151
|
+
|
|
152
|
+
const id = _id();
|
|
153
|
+
const now = _now();
|
|
154
|
+
|
|
155
|
+
const entry = {
|
|
156
|
+
id,
|
|
157
|
+
prompt,
|
|
158
|
+
language: language || null,
|
|
159
|
+
framework: framework || null,
|
|
160
|
+
generated_code: generatedCode || null,
|
|
161
|
+
file_count: fileCount || 0,
|
|
162
|
+
token_count: tokenCount || 0,
|
|
163
|
+
metadata: metadata || null,
|
|
164
|
+
created_at: now,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
db.prepare(
|
|
168
|
+
`INSERT INTO code_generations (id, prompt, language, framework, generated_code, file_count, token_count, metadata, created_at)
|
|
169
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
170
|
+
).run(
|
|
171
|
+
id,
|
|
172
|
+
prompt,
|
|
173
|
+
entry.language,
|
|
174
|
+
entry.framework,
|
|
175
|
+
entry.generated_code,
|
|
176
|
+
entry.file_count,
|
|
177
|
+
entry.token_count,
|
|
178
|
+
entry.metadata,
|
|
179
|
+
now,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
_generations.set(id, entry);
|
|
183
|
+
return { generationId: id };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function getGeneration(db, id) {
|
|
187
|
+
const g = _generations.get(id);
|
|
188
|
+
return g ? { ...g } : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function listGenerations(db, { language, framework, limit = 50 } = {}) {
|
|
192
|
+
let gens = [..._generations.values()];
|
|
193
|
+
if (language) gens = gens.filter((g) => g.language === language);
|
|
194
|
+
if (framework) gens = gens.filter((g) => g.framework === framework);
|
|
195
|
+
return gens
|
|
196
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
197
|
+
.slice(0, limit)
|
|
198
|
+
.map((g) => ({ ...g }));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* ── Heuristic Code Review ──────────────────────────────── */
|
|
202
|
+
|
|
203
|
+
const SECURITY_PATTERNS = {
|
|
204
|
+
eval_detection: [
|
|
205
|
+
/\beval\s*\(/,
|
|
206
|
+
/\bnew\s+Function\s*\(/,
|
|
207
|
+
/\bsetTimeout\s*\(\s*["'`]/,
|
|
208
|
+
/\bsetInterval\s*\(\s*["'`]/,
|
|
209
|
+
],
|
|
210
|
+
sql_injection: [
|
|
211
|
+
/['"`]\s*\+\s*\w+.*(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER)/i,
|
|
212
|
+
/\$\{[^}]+\}.*(?:SELECT|INSERT|UPDATE|DELETE|DROP)/i,
|
|
213
|
+
/(?:execute|query)\s*\(\s*["'`].*\+/i,
|
|
214
|
+
],
|
|
215
|
+
xss: [
|
|
216
|
+
/\.innerHTML\s*=/,
|
|
217
|
+
/document\.write\s*\(/,
|
|
218
|
+
/\.insertAdjacentHTML\s*\(/,
|
|
219
|
+
/dangerouslySetInnerHTML/,
|
|
220
|
+
],
|
|
221
|
+
path_traversal: [
|
|
222
|
+
/\.\.\//,
|
|
223
|
+
/\.\.\\/, // backslash variant
|
|
224
|
+
/path\.join\s*\([^)]*\.\./,
|
|
225
|
+
],
|
|
226
|
+
command_injection: [
|
|
227
|
+
/child_process.*exec\s*\(\s*[`"'].*\$\{/,
|
|
228
|
+
/exec\s*\(\s*.*\+\s*\w+/,
|
|
229
|
+
/spawn\s*\(\s*["'`](?:sh|bash|cmd)/,
|
|
230
|
+
/os\.system\s*\(/,
|
|
231
|
+
/subprocess\.(?:call|run|Popen)\s*\(\s*[^[\]]*\+/,
|
|
232
|
+
],
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const SEVERITY_FOR_RULE = {
|
|
236
|
+
eval_detection: "high",
|
|
237
|
+
sql_injection: "critical",
|
|
238
|
+
xss: "high",
|
|
239
|
+
path_traversal: "medium",
|
|
240
|
+
command_injection: "critical",
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
function _detectIssues(code) {
|
|
244
|
+
const issues = [];
|
|
245
|
+
|
|
246
|
+
for (const [rule, patterns] of Object.entries(SECURITY_PATTERNS)) {
|
|
247
|
+
for (const pattern of patterns) {
|
|
248
|
+
const matches = code.match(
|
|
249
|
+
new RegExp(pattern.source, pattern.flags + "g"),
|
|
250
|
+
);
|
|
251
|
+
if (matches) {
|
|
252
|
+
for (const match of matches) {
|
|
253
|
+
issues.push({
|
|
254
|
+
rule,
|
|
255
|
+
severity: SEVERITY_FOR_RULE[rule],
|
|
256
|
+
match: match.slice(0, 80),
|
|
257
|
+
pattern: pattern.source,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return issues;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function reviewCode(db, { generationId, code, language } = {}) {
|
|
268
|
+
if (!code) return { reviewId: null, reason: "missing_code" };
|
|
269
|
+
|
|
270
|
+
const issues = _detectIssues(code);
|
|
271
|
+
const securityIssues = issues.length;
|
|
272
|
+
const severitySummary = {};
|
|
273
|
+
for (const sev of Object.values(REVIEW_SEVERITY)) {
|
|
274
|
+
severitySummary[sev] = 0;
|
|
275
|
+
}
|
|
276
|
+
for (const issue of issues) {
|
|
277
|
+
severitySummary[issue.severity] =
|
|
278
|
+
(severitySummary[issue.severity] || 0) + 1;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const id = _id();
|
|
282
|
+
const now = _now();
|
|
283
|
+
const codeHash = crypto
|
|
284
|
+
.createHash("sha256")
|
|
285
|
+
.update(code)
|
|
286
|
+
.digest("hex")
|
|
287
|
+
.slice(0, 16);
|
|
288
|
+
|
|
289
|
+
const entry = {
|
|
290
|
+
id,
|
|
291
|
+
generation_id: generationId || null,
|
|
292
|
+
code_hash: codeHash,
|
|
293
|
+
language: language || null,
|
|
294
|
+
issues_found: issues.length,
|
|
295
|
+
security_issues: securityIssues,
|
|
296
|
+
severity_summary: JSON.stringify(severitySummary),
|
|
297
|
+
issues_detail: JSON.stringify(issues),
|
|
298
|
+
reviewed_at: now,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
db.prepare(
|
|
302
|
+
`INSERT INTO code_reviews (id, generation_id, code_hash, language, issues_found, security_issues, severity_summary, issues_detail, reviewed_at)
|
|
303
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
304
|
+
).run(
|
|
305
|
+
id,
|
|
306
|
+
entry.generation_id,
|
|
307
|
+
codeHash,
|
|
308
|
+
entry.language,
|
|
309
|
+
entry.issues_found,
|
|
310
|
+
entry.security_issues,
|
|
311
|
+
entry.severity_summary,
|
|
312
|
+
entry.issues_detail,
|
|
313
|
+
now,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
_reviews.set(id, entry);
|
|
317
|
+
return {
|
|
318
|
+
reviewId: id,
|
|
319
|
+
issuesFound: issues.length,
|
|
320
|
+
securityIssues,
|
|
321
|
+
severitySummary,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function getReview(db, id) {
|
|
326
|
+
const r = _reviews.get(id);
|
|
327
|
+
return r ? { ...r } : null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function listReviews(db, { language, limit = 50 } = {}) {
|
|
331
|
+
let revs = [..._reviews.values()];
|
|
332
|
+
if (language) revs = revs.filter((r) => r.language === language);
|
|
333
|
+
return revs
|
|
334
|
+
.sort((a, b) => b.reviewed_at - a.reviewed_at)
|
|
335
|
+
.slice(0, limit)
|
|
336
|
+
.map((r) => ({ ...r }));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* ── Scaffold ───────────────────────────────────────────── */
|
|
340
|
+
|
|
341
|
+
const VALID_TEMPLATES = new Set(Object.values(SCAFFOLD_TEMPLATE));
|
|
342
|
+
|
|
343
|
+
export function createScaffold(
|
|
344
|
+
db,
|
|
345
|
+
{ template, projectName, options, filesGenerated, outputPath } = {},
|
|
346
|
+
) {
|
|
347
|
+
if (!template || !VALID_TEMPLATES.has(template))
|
|
348
|
+
return { scaffoldId: null, reason: "invalid_template" };
|
|
349
|
+
if (!projectName) return { scaffoldId: null, reason: "missing_project_name" };
|
|
350
|
+
|
|
351
|
+
const id = _id();
|
|
352
|
+
const now = _now();
|
|
353
|
+
|
|
354
|
+
const entry = {
|
|
355
|
+
id,
|
|
356
|
+
template,
|
|
357
|
+
project_name: projectName,
|
|
358
|
+
options: options || null,
|
|
359
|
+
files_generated: filesGenerated || 0,
|
|
360
|
+
output_path: outputPath || null,
|
|
361
|
+
created_at: now,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
db.prepare(
|
|
365
|
+
`INSERT INTO code_scaffolds (id, template, project_name, options, files_generated, output_path, created_at)
|
|
366
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
367
|
+
).run(
|
|
368
|
+
id,
|
|
369
|
+
template,
|
|
370
|
+
projectName,
|
|
371
|
+
entry.options,
|
|
372
|
+
entry.files_generated,
|
|
373
|
+
entry.output_path,
|
|
374
|
+
now,
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
_scaffolds.set(id, entry);
|
|
378
|
+
return { scaffoldId: id };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function getScaffold(db, id) {
|
|
382
|
+
const s = _scaffolds.get(id);
|
|
383
|
+
return s ? { ...s } : null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function listScaffolds(db, { template, limit = 50 } = {}) {
|
|
387
|
+
let scf = [..._scaffolds.values()];
|
|
388
|
+
if (template) scf = scf.filter((s) => s.template === template);
|
|
389
|
+
return scf
|
|
390
|
+
.sort((a, b) => b.created_at - a.created_at)
|
|
391
|
+
.slice(0, limit)
|
|
392
|
+
.map((s) => ({ ...s }));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* ── Stats ──────────────────────────────────────────────── */
|
|
396
|
+
|
|
397
|
+
export function getCodeAgentStats(db) {
|
|
398
|
+
const gens = [..._generations.values()];
|
|
399
|
+
const revs = [..._reviews.values()];
|
|
400
|
+
const scfs = [..._scaffolds.values()];
|
|
401
|
+
|
|
402
|
+
const totalIssues = revs.reduce((s, r) => s + (r.issues_found || 0), 0);
|
|
403
|
+
const languages = new Set(
|
|
404
|
+
gens.filter((g) => g.language).map((g) => g.language),
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
generations: {
|
|
409
|
+
total: gens.length,
|
|
410
|
+
totalTokens: gens.reduce((s, g) => s + (g.token_count || 0), 0),
|
|
411
|
+
totalFiles: gens.reduce((s, g) => s + (g.file_count || 0), 0),
|
|
412
|
+
uniqueLanguages: languages.size,
|
|
413
|
+
},
|
|
414
|
+
reviews: {
|
|
415
|
+
total: revs.length,
|
|
416
|
+
totalIssues,
|
|
417
|
+
totalSecurityIssues: revs.reduce(
|
|
418
|
+
(s, r) => s + (r.security_issues || 0),
|
|
419
|
+
0,
|
|
420
|
+
),
|
|
421
|
+
avgIssuesPerReview:
|
|
422
|
+
revs.length > 0
|
|
423
|
+
? Math.round((totalIssues / revs.length) * 100) / 100
|
|
424
|
+
: 0,
|
|
425
|
+
},
|
|
426
|
+
scaffolds: {
|
|
427
|
+
total: scfs.length,
|
|
428
|
+
byTemplate: scfs.reduce((acc, s) => {
|
|
429
|
+
acc[s.template] = (acc[s.template] || 0) + 1;
|
|
430
|
+
return acc;
|
|
431
|
+
}, {}),
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* ── Reset (tests) ──────────────────────────────────────── */
|
|
437
|
+
|
|
438
|
+
export function _resetState() {
|
|
439
|
+
_generations.clear();
|
|
440
|
+
_reviews.clear();
|
|
441
|
+
_scaffolds.clear();
|
|
442
|
+
}
|