cipher-security 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cipher.js +10 -0
- package/lib/analyze/consistency.js +566 -0
- package/lib/analyze/constitution.js +110 -0
- package/lib/analyze/sharding.js +251 -0
- package/lib/autonomous/agent-tool.js +165 -0
- package/lib/autonomous/framework.js +17 -0
- package/lib/autonomous/handoff.js +506 -0
- package/lib/autonomous/modes/blue.js +26 -0
- package/lib/autonomous/modes/red.js +28 -0
- package/lib/benchmark/agent.js +88 -26
- package/lib/benchmark/baselines.js +3 -0
- package/lib/benchmark/claude-code-solver.js +254 -0
- package/lib/benchmark/cognitive.js +283 -0
- package/lib/benchmark/index.js +12 -2
- package/lib/benchmark/knowledge.js +281 -0
- package/lib/benchmark/llm.js +156 -15
- package/lib/benchmark/models.js +5 -2
- package/lib/benchmark/nyu-ctf.js +192 -0
- package/lib/benchmark/overthewire.js +347 -0
- package/lib/benchmark/picoctf.js +281 -0
- package/lib/benchmark/prompts.js +280 -0
- package/lib/benchmark/registry.js +219 -0
- package/lib/benchmark/remote-solver.js +356 -0
- package/lib/benchmark/remote-target.js +263 -0
- package/lib/benchmark/reporter.js +35 -0
- package/lib/benchmark/runner.js +174 -10
- package/lib/benchmark/sandbox.js +35 -0
- package/lib/benchmark/scorer.js +22 -4
- package/lib/benchmark/solver.js +34 -1
- package/lib/benchmark/tools.js +262 -16
- package/lib/commands.js +9 -0
- package/lib/execution/council.js +434 -0
- package/lib/execution/parallel.js +292 -0
- package/lib/gates/circuit-breaker.js +135 -0
- package/lib/gates/confidence.js +302 -0
- package/lib/gates/corrections.js +219 -0
- package/lib/gates/self-check.js +245 -0
- package/lib/gateway/commands.js +727 -0
- package/lib/guardrails/engine.js +364 -0
- package/lib/mcp/server.js +349 -3
- package/lib/memory/compressor.js +94 -7
- package/lib/pipeline/hooks.js +288 -0
- package/lib/pipeline/index.js +11 -0
- package/lib/review/budget.js +210 -0
- package/lib/review/engine.js +526 -0
- package/lib/review/layers/acceptance-auditor.js +279 -0
- package/lib/review/layers/blind-hunter.js +500 -0
- package/lib/review/layers/defense-in-depth.js +209 -0
- package/lib/review/layers/edge-case-hunter.js +266 -0
- package/lib/review/panel.js +519 -0
- package/lib/review/two-stage.js +244 -0
- package/lib/session/cost-tracker.js +203 -0
- package/lib/session/logger.js +349 -0
- package/package.json +1 -1
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
// CIPHER is a trademark of defconxt.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CIPHER Expert Panel Simulation
|
|
7
|
+
*
|
|
8
|
+
* Simulates 3 security expert personas reviewing code independently,
|
|
9
|
+
* then synthesizes findings into a consensus report. Each persona
|
|
10
|
+
* filters and reweights findings from the review engine based on
|
|
11
|
+
* their focus area.
|
|
12
|
+
*
|
|
13
|
+
* Personas:
|
|
14
|
+
* - RedTeamExpert: attack surface, exploitation paths, offensive TTPs
|
|
15
|
+
* - BlueTeamExpert: detection gaps, hardening, logging, monitoring
|
|
16
|
+
* - ArchitectExpert: trust boundaries, data flow, auth design, OWASP
|
|
17
|
+
*
|
|
18
|
+
* @module review/panel
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createReviewEngine, resolveInput } from './engine.js';
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Expert Personas
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} ExpertPersona
|
|
29
|
+
* @property {string} id - Persona identifier
|
|
30
|
+
* @property {string} name - Display name
|
|
31
|
+
* @property {string} title - Professional title
|
|
32
|
+
* @property {string} focus - What this expert focuses on
|
|
33
|
+
* @property {string[]} priorityCwes - CWEs this expert prioritizes
|
|
34
|
+
* @property {string[]} priorityTags - Tags this expert prioritizes
|
|
35
|
+
* @property {object} severityAdjust - Severity adjustments {cweOrTag: delta}
|
|
36
|
+
* @property {function} commentary - (finding) => expert perspective string
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
|
|
40
|
+
const RANK_TO_SEV = ['info', 'low', 'medium', 'high', 'critical'];
|
|
41
|
+
|
|
42
|
+
function clampSeverity(rank) {
|
|
43
|
+
return RANK_TO_SEV[Math.max(0, Math.min(4, rank))];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @type {ExpertPersona} */
|
|
47
|
+
const RED_TEAM_EXPERT = {
|
|
48
|
+
id: 'red-team',
|
|
49
|
+
name: 'Alex Chen',
|
|
50
|
+
title: 'Principal Offensive Security Engineer',
|
|
51
|
+
focus: 'Attack surface analysis, exploitation chains, OPSEC failures, privilege escalation paths',
|
|
52
|
+
priorityCwes: [
|
|
53
|
+
'CWE-89', 'CWE-78', 'CWE-95', 'CWE-502', 'CWE-918', 'CWE-22',
|
|
54
|
+
'CWE-798', 'CWE-269', 'CWE-79', 'CWE-1321',
|
|
55
|
+
],
|
|
56
|
+
priorityTags: [
|
|
57
|
+
'T1190', 'T1059', 'T1078', 'T1552.001', 'T1105', 'T1557',
|
|
58
|
+
'owasp-a03', 'owasp-a01',
|
|
59
|
+
],
|
|
60
|
+
severityBump: new Map([
|
|
61
|
+
['CWE-89', 1], // SQLi is always critical from attacker perspective
|
|
62
|
+
['CWE-78', 1], // Command injection → immediate RCE
|
|
63
|
+
['CWE-502', 1], // Deserialization → RCE
|
|
64
|
+
['CWE-798', 1], // Hardcoded creds → instant compromise
|
|
65
|
+
['CWE-269', 1], // Privilege escalation
|
|
66
|
+
]),
|
|
67
|
+
commentary(finding) {
|
|
68
|
+
const cwes = finding.cweIds || [];
|
|
69
|
+
if (cwes.includes('CWE-89') || cwes.includes('CWE-78')) {
|
|
70
|
+
return 'Direct code execution path. Attacker can chain this for initial access or lateral movement.';
|
|
71
|
+
}
|
|
72
|
+
if (cwes.includes('CWE-798')) {
|
|
73
|
+
return 'Credential harvesting target. Leaked secrets enable persistence without exploitation.';
|
|
74
|
+
}
|
|
75
|
+
if (cwes.includes('CWE-269')) {
|
|
76
|
+
return 'Privilege escalation primitive. Attacker moves from low-priv to admin in one step.';
|
|
77
|
+
}
|
|
78
|
+
if (cwes.includes('CWE-918')) {
|
|
79
|
+
return 'SSRF enables internal network reconnaissance and cloud metadata access.';
|
|
80
|
+
}
|
|
81
|
+
if (cwes.includes('CWE-502')) {
|
|
82
|
+
return 'Deserialization to RCE. No auth required if the endpoint is reachable.';
|
|
83
|
+
}
|
|
84
|
+
if (cwes.includes('CWE-79')) {
|
|
85
|
+
return 'XSS enables session hijacking, credential theft, and phishing from trusted domain.';
|
|
86
|
+
}
|
|
87
|
+
return 'Exploitable attack surface — assess reachability and impact in context.';
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** @type {ExpertPersona} */
|
|
92
|
+
const BLUE_TEAM_EXPERT = {
|
|
93
|
+
id: 'blue-team',
|
|
94
|
+
name: 'Sarah Martinez',
|
|
95
|
+
title: 'Senior Detection Engineer & SOC Lead',
|
|
96
|
+
focus: 'Detection coverage, logging gaps, monitoring blind spots, incident response readiness',
|
|
97
|
+
priorityCwes: [
|
|
98
|
+
'CWE-778', 'CWE-532', 'CWE-209', 'CWE-390', 'CWE-755',
|
|
99
|
+
'CWE-404', 'CWE-401', 'CWE-307', 'CWE-613', 'CWE-614',
|
|
100
|
+
],
|
|
101
|
+
priorityTags: [
|
|
102
|
+
'reliability', 'observability', 'owasp-a09', 'owasp-a07',
|
|
103
|
+
'owasp-a05', 'performance',
|
|
104
|
+
],
|
|
105
|
+
severityBump: new Map([
|
|
106
|
+
['CWE-778', 1], // Missing audit logging → blind spot
|
|
107
|
+
['CWE-390', 1], // Error swallowing → invisible failures
|
|
108
|
+
['CWE-532', 1], // Sensitive data in logs → compliance risk
|
|
109
|
+
['CWE-307', 1], // No rate limiting → brute force undetected
|
|
110
|
+
]),
|
|
111
|
+
commentary(finding) {
|
|
112
|
+
const cwes = finding.cweIds || [];
|
|
113
|
+
if (cwes.includes('CWE-778')) {
|
|
114
|
+
return 'Detection blind spot. This operation has no audit trail — incident responders cannot reconstruct the attack timeline.';
|
|
115
|
+
}
|
|
116
|
+
if (cwes.includes('CWE-390') || cwes.includes('CWE-755')) {
|
|
117
|
+
return 'Silent failure. Errors are swallowed — the SOC has no signal to trigger investigation.';
|
|
118
|
+
}
|
|
119
|
+
if (cwes.includes('CWE-532')) {
|
|
120
|
+
return 'Log contamination. Sensitive data in logs creates compliance exposure and complicates log sharing.';
|
|
121
|
+
}
|
|
122
|
+
if (cwes.includes('CWE-307')) {
|
|
123
|
+
return 'No rate limiting means brute-force attacks are invisible until account compromise.';
|
|
124
|
+
}
|
|
125
|
+
if (cwes.includes('CWE-209')) {
|
|
126
|
+
return 'Error details leak internal architecture to attackers, aiding reconnaissance.';
|
|
127
|
+
}
|
|
128
|
+
if (cwes.includes('CWE-404') || cwes.includes('CWE-401')) {
|
|
129
|
+
return 'Resource leak degrades availability over time — difficult to attribute to an attack vs. organic growth.';
|
|
130
|
+
}
|
|
131
|
+
return 'Review detection coverage — ensure this behavior generates actionable alerts.';
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** @type {ExpertPersona} */
|
|
136
|
+
const ARCHITECT_EXPERT = {
|
|
137
|
+
id: 'architect',
|
|
138
|
+
name: 'Dr. Priya Kapoor',
|
|
139
|
+
title: 'Security Architect & Privacy Officer',
|
|
140
|
+
focus: 'Trust boundaries, data flow integrity, auth/authz design, OWASP alignment, privacy by design',
|
|
141
|
+
priorityCwes: [
|
|
142
|
+
'CWE-306', 'CWE-862', 'CWE-863', 'CWE-200', 'CWE-942',
|
|
143
|
+
'CWE-295', 'CWE-601', 'CWE-16', 'CWE-20', 'CWE-328',
|
|
144
|
+
],
|
|
145
|
+
priorityTags: [
|
|
146
|
+
'owasp-a01', 'owasp-a02', 'owasp-a05', 'owasp-a07',
|
|
147
|
+
'privacy', 'T1078',
|
|
148
|
+
],
|
|
149
|
+
severityBump: new Map([
|
|
150
|
+
['CWE-306', 1], // Missing auth → architectural failure
|
|
151
|
+
['CWE-862', 1], // Missing authz → broken access control
|
|
152
|
+
['CWE-200', 1], // Data exposure → privacy violation
|
|
153
|
+
['CWE-942', 1], // CORS wildcard → trust boundary collapse
|
|
154
|
+
]),
|
|
155
|
+
commentary(finding) {
|
|
156
|
+
const cwes = finding.cweIds || [];
|
|
157
|
+
if (cwes.includes('CWE-306') || cwes.includes('CWE-862')) {
|
|
158
|
+
return 'Broken access control — the most common critical vulnerability class (OWASP #1). This is an architectural gap, not a bug.';
|
|
159
|
+
}
|
|
160
|
+
if (cwes.includes('CWE-200')) {
|
|
161
|
+
return 'Data over-exposure violates least-privilege and creates GDPR/CCPA notification triggers.';
|
|
162
|
+
}
|
|
163
|
+
if (cwes.includes('CWE-942') || cwes.includes('CWE-295')) {
|
|
164
|
+
return 'Trust boundary violation. The security model assumes a perimeter that this bypasses.';
|
|
165
|
+
}
|
|
166
|
+
if (cwes.includes('CWE-20')) {
|
|
167
|
+
return 'Missing input validation at the trust boundary. Defense-in-depth requires validation at every layer crossing.';
|
|
168
|
+
}
|
|
169
|
+
if (cwes.includes('CWE-328') || cwes.includes('CWE-338')) {
|
|
170
|
+
return 'Cryptographic weakness undermines the security guarantees the architecture depends on.';
|
|
171
|
+
}
|
|
172
|
+
if (cwes.includes('CWE-16')) {
|
|
173
|
+
return 'Security misconfiguration — the defaults are insecure. Harden before deployment.';
|
|
174
|
+
}
|
|
175
|
+
return 'Evaluate against the threat model — does this weaken a trust boundary or violate a design invariant?';
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const ALL_PERSONAS = [RED_TEAM_EXPERT, BLUE_TEAM_EXPERT, ARCHITECT_EXPERT];
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Expert Review
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* An expert's perspective on a single finding.
|
|
187
|
+
*/
|
|
188
|
+
class ExpertFinding {
|
|
189
|
+
constructor({ finding, persona, adjustedSeverity, comment }) {
|
|
190
|
+
this.finding = finding;
|
|
191
|
+
this.expertId = persona.id;
|
|
192
|
+
this.expertName = persona.name;
|
|
193
|
+
this.expertTitle = persona.title;
|
|
194
|
+
this.originalSeverity = finding.severity;
|
|
195
|
+
this.adjustedSeverity = adjustedSeverity;
|
|
196
|
+
this.comment = comment;
|
|
197
|
+
this.isPriority = false; // Set by reviewAsExpert
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Review findings through a specific expert persona's lens.
|
|
203
|
+
*
|
|
204
|
+
* @param {import('./engine.js').ReviewFinding[]} findings
|
|
205
|
+
* @param {ExpertPersona} persona
|
|
206
|
+
* @returns {ExpertFinding[]}
|
|
207
|
+
*/
|
|
208
|
+
function reviewAsExpert(findings, persona) {
|
|
209
|
+
const expertFindings = [];
|
|
210
|
+
|
|
211
|
+
for (const f of findings) {
|
|
212
|
+
const cwes = f.cweIds || [];
|
|
213
|
+
const tags = f.tags || [];
|
|
214
|
+
|
|
215
|
+
// Check if this finding is in the expert's priority area
|
|
216
|
+
const isPriorityCwe = cwes.some((c) => persona.priorityCwes.includes(c));
|
|
217
|
+
const isPriorityTag = tags.some((t) => persona.priorityTags.includes(t));
|
|
218
|
+
const isPriority = isPriorityCwe || isPriorityTag;
|
|
219
|
+
|
|
220
|
+
// Adjust severity based on expert perspective
|
|
221
|
+
let rank = SEVERITY_RANK[f.severity] ?? 0;
|
|
222
|
+
for (const cwe of cwes) {
|
|
223
|
+
if (persona.severityBump.has(cwe)) {
|
|
224
|
+
rank += persona.severityBump.get(cwe);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const adjustedSeverity = clampSeverity(rank);
|
|
228
|
+
|
|
229
|
+
const ef = new ExpertFinding({
|
|
230
|
+
finding: f,
|
|
231
|
+
persona,
|
|
232
|
+
adjustedSeverity,
|
|
233
|
+
comment: persona.commentary(f),
|
|
234
|
+
});
|
|
235
|
+
ef.isPriority = isPriority;
|
|
236
|
+
expertFindings.push(ef);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Sort: priority findings first, then by adjusted severity
|
|
240
|
+
expertFindings.sort((a, b) => {
|
|
241
|
+
if (a.isPriority !== b.isPriority) return a.isPriority ? -1 : 1;
|
|
242
|
+
const aRank = SEVERITY_RANK[a.adjustedSeverity] ?? 0;
|
|
243
|
+
const bRank = SEVERITY_RANK[b.adjustedSeverity] ?? 0;
|
|
244
|
+
return bRank - aRank;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return expertFindings;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// Consensus Synthesis
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* A consensus finding — identified by 2+ experts.
|
|
256
|
+
*/
|
|
257
|
+
class ConsensusFinding {
|
|
258
|
+
constructor({ finding, experts, maxSeverity, comments }) {
|
|
259
|
+
this.finding = finding;
|
|
260
|
+
this.experts = experts; // expert IDs that flagged this
|
|
261
|
+
this.expertCount = experts.length;
|
|
262
|
+
this.maxSeverity = maxSeverity; // highest adjusted severity
|
|
263
|
+
this.comments = comments; // {expertId: comment}
|
|
264
|
+
this.isUnanimous = experts.length === ALL_PERSONAS.length;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* A conflict — experts disagree on severity by 2+ levels.
|
|
270
|
+
*/
|
|
271
|
+
class SeverityConflict {
|
|
272
|
+
constructor({ finding, assessments }) {
|
|
273
|
+
this.finding = finding;
|
|
274
|
+
this.assessments = assessments; // [{expertId, severity}]
|
|
275
|
+
this.spread = 0;
|
|
276
|
+
if (assessments.length >= 2) {
|
|
277
|
+
const ranks = assessments.map((a) => SEVERITY_RANK[a.severity] ?? 0);
|
|
278
|
+
this.spread = Math.max(...ranks) - Math.min(...ranks);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Synthesize expert findings into consensus.
|
|
285
|
+
*
|
|
286
|
+
* @param {Map<string, ExpertFinding[]>} expertResults - expertId → findings
|
|
287
|
+
* @returns {{ consensus: ConsensusFinding[], conflicts: SeverityConflict[] }}
|
|
288
|
+
*/
|
|
289
|
+
function synthesize(expertResults) {
|
|
290
|
+
// Group findings by identity (file:line:titleStem)
|
|
291
|
+
const findingMap = new Map(); // key → {finding, experts: Map<expertId, ExpertFinding>}
|
|
292
|
+
|
|
293
|
+
for (const [expertId, findings] of expertResults) {
|
|
294
|
+
for (const ef of findings) {
|
|
295
|
+
const f = ef.finding;
|
|
296
|
+
const stem = f.title.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 30);
|
|
297
|
+
const key = `${f.file}:${f.line}:${stem}`;
|
|
298
|
+
|
|
299
|
+
if (!findingMap.has(key)) {
|
|
300
|
+
findingMap.set(key, { finding: f, experts: new Map() });
|
|
301
|
+
}
|
|
302
|
+
findingMap.get(key).experts.set(expertId, ef);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const consensus = [];
|
|
307
|
+
const conflicts = [];
|
|
308
|
+
|
|
309
|
+
for (const [, entry] of findingMap) {
|
|
310
|
+
const { finding, experts } = entry;
|
|
311
|
+
|
|
312
|
+
if (experts.size >= 2) {
|
|
313
|
+
// Consensus: 2+ experts flagged this
|
|
314
|
+
const expertIds = [...experts.keys()];
|
|
315
|
+
const maxRank = Math.max(
|
|
316
|
+
...[...experts.values()].map((ef) => SEVERITY_RANK[ef.adjustedSeverity] ?? 0),
|
|
317
|
+
);
|
|
318
|
+
const comments = {};
|
|
319
|
+
for (const [eid, ef] of experts) {
|
|
320
|
+
comments[eid] = ef.comment;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
consensus.push(new ConsensusFinding({
|
|
324
|
+
finding,
|
|
325
|
+
experts: expertIds,
|
|
326
|
+
maxSeverity: clampSeverity(maxRank),
|
|
327
|
+
comments,
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Check for conflicts: severity spread >= 2 levels
|
|
332
|
+
if (experts.size >= 2) {
|
|
333
|
+
const assessments = [...experts.entries()].map(([eid, ef]) => ({
|
|
334
|
+
expertId: eid,
|
|
335
|
+
severity: ef.adjustedSeverity,
|
|
336
|
+
}));
|
|
337
|
+
const conflict = new SeverityConflict({ finding, assessments });
|
|
338
|
+
if (conflict.spread >= 2) {
|
|
339
|
+
conflicts.push(conflict);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Sort consensus by expert count (unanimous first), then severity
|
|
345
|
+
consensus.sort((a, b) => {
|
|
346
|
+
if (b.expertCount !== a.expertCount) return b.expertCount - a.expertCount;
|
|
347
|
+
return (SEVERITY_RANK[b.maxSeverity] ?? 0) - (SEVERITY_RANK[a.maxSeverity] ?? 0);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return { consensus, conflicts };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// PanelResult
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
export class PanelResult {
|
|
358
|
+
constructor({ expertResults, consensus, conflicts, reviewResult, totalTime = 0 } = {}) {
|
|
359
|
+
/** @type {Map<string, ExpertFinding[]>} */
|
|
360
|
+
this.expertResults = expertResults;
|
|
361
|
+
/** @type {ConsensusFinding[]} */
|
|
362
|
+
this.consensus = consensus;
|
|
363
|
+
/** @type {SeverityConflict[]} */
|
|
364
|
+
this.conflicts = conflicts;
|
|
365
|
+
/** @type {import('./engine.js').ReviewResult} */
|
|
366
|
+
this.reviewResult = reviewResult;
|
|
367
|
+
this.totalTime = totalTime;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
toReport() {
|
|
371
|
+
const lines = [
|
|
372
|
+
'═══════════════════════════════════════════════════════',
|
|
373
|
+
' CIPHER Expert Panel Assessment',
|
|
374
|
+
'═══════════════════════════════════════════════════════',
|
|
375
|
+
'',
|
|
376
|
+
`Base review: ${this.reviewResult.summary}`,
|
|
377
|
+
`Panel consensus: ${this.consensus.length} findings agreed by 2+ experts`,
|
|
378
|
+
`Conflicts: ${this.conflicts.length} severity disagreements`,
|
|
379
|
+
`Total time: ${this.totalTime}ms`,
|
|
380
|
+
'',
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
// Per-expert summaries
|
|
384
|
+
for (const persona of ALL_PERSONAS) {
|
|
385
|
+
const findings = this.expertResults.get(persona.id) || [];
|
|
386
|
+
const priority = findings.filter((f) => f.isPriority);
|
|
387
|
+
lines.push(`── ${persona.name} (${persona.title}) ──`);
|
|
388
|
+
lines.push(`Focus: ${persona.focus}`);
|
|
389
|
+
lines.push(`Findings: ${findings.length} total, ${priority.length} in priority area`);
|
|
390
|
+
if (priority.length > 0) {
|
|
391
|
+
lines.push('Priority findings:');
|
|
392
|
+
for (const pf of priority.slice(0, 5)) {
|
|
393
|
+
const bump = pf.adjustedSeverity !== pf.originalSeverity
|
|
394
|
+
? ` (${pf.originalSeverity}→${pf.adjustedSeverity})`
|
|
395
|
+
: '';
|
|
396
|
+
lines.push(` • [${pf.adjustedSeverity.toUpperCase()}${bump}] ${pf.finding.title}`);
|
|
397
|
+
lines.push(` ${pf.comment}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
lines.push('');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Consensus
|
|
404
|
+
if (this.consensus.length > 0) {
|
|
405
|
+
lines.push('── CONSENSUS (2+ experts agree) ──');
|
|
406
|
+
for (const cf of this.consensus) {
|
|
407
|
+
const tag = cf.isUnanimous ? '★ UNANIMOUS' : `${cf.expertCount} experts`;
|
|
408
|
+
lines.push(` [${cf.maxSeverity.toUpperCase()}] ${cf.finding.title} — ${tag}`);
|
|
409
|
+
const loc = cf.finding.line ? `${cf.finding.file}:${cf.finding.line}` : cf.finding.file;
|
|
410
|
+
if (loc) lines.push(` Location: ${loc}`);
|
|
411
|
+
for (const [eid, comment] of Object.entries(cf.comments)) {
|
|
412
|
+
const expert = ALL_PERSONAS.find((p) => p.id === eid);
|
|
413
|
+
lines.push(` ${expert?.name ?? eid}: ${comment}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
lines.push('');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Conflicts
|
|
420
|
+
if (this.conflicts.length > 0) {
|
|
421
|
+
lines.push('── SEVERITY CONFLICTS ──');
|
|
422
|
+
for (const sc of this.conflicts) {
|
|
423
|
+
lines.push(` ${sc.finding.title} (spread: ${sc.spread} levels)`);
|
|
424
|
+
for (const a of sc.assessments) {
|
|
425
|
+
const expert = ALL_PERSONAS.find((p) => p.id === a.expertId);
|
|
426
|
+
lines.push(` ${expert?.name ?? a.expertId}: ${a.severity}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
lines.push('');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
lines.push('───────────────────────────────────────────────────────');
|
|
433
|
+
return lines.join('\n');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
toJSON() {
|
|
437
|
+
const expertSummaries = {};
|
|
438
|
+
for (const persona of ALL_PERSONAS) {
|
|
439
|
+
const findings = this.expertResults.get(persona.id) || [];
|
|
440
|
+
expertSummaries[persona.id] = {
|
|
441
|
+
name: persona.name,
|
|
442
|
+
title: persona.title,
|
|
443
|
+
focus: persona.focus,
|
|
444
|
+
totalFindings: findings.length,
|
|
445
|
+
priorityFindings: findings.filter((f) => f.isPriority).length,
|
|
446
|
+
topFindings: findings.slice(0, 5).map((ef) => ({
|
|
447
|
+
title: ef.finding.title,
|
|
448
|
+
originalSeverity: ef.originalSeverity,
|
|
449
|
+
adjustedSeverity: ef.adjustedSeverity,
|
|
450
|
+
isPriority: ef.isPriority,
|
|
451
|
+
comment: ef.comment,
|
|
452
|
+
file: ef.finding.file,
|
|
453
|
+
line: ef.finding.line,
|
|
454
|
+
})),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
totalTime: this.totalTime,
|
|
460
|
+
baseReview: this.reviewResult.summary,
|
|
461
|
+
experts: expertSummaries,
|
|
462
|
+
consensus: this.consensus.map((cf) => ({
|
|
463
|
+
title: cf.finding.title,
|
|
464
|
+
severity: cf.maxSeverity,
|
|
465
|
+
expertCount: cf.expertCount,
|
|
466
|
+
isUnanimous: cf.isUnanimous,
|
|
467
|
+
experts: cf.experts,
|
|
468
|
+
comments: cf.comments,
|
|
469
|
+
file: cf.finding.file,
|
|
470
|
+
line: cf.finding.line,
|
|
471
|
+
})),
|
|
472
|
+
conflicts: this.conflicts.map((sc) => ({
|
|
473
|
+
title: sc.finding.title,
|
|
474
|
+
spread: sc.spread,
|
|
475
|
+
assessments: sc.assessments,
|
|
476
|
+
})),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
// Panel Review — main entry point
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Run an expert panel review on code input.
|
|
487
|
+
*
|
|
488
|
+
* @param {string} input - File path, directory, or raw code string
|
|
489
|
+
* @param {object} [options]
|
|
490
|
+
* @param {string} [options.language] - Override language detection
|
|
491
|
+
* @param {string} [options.format] - 'text' or 'json'
|
|
492
|
+
* @returns {Promise<PanelResult>}
|
|
493
|
+
*/
|
|
494
|
+
export async function panelReview(input, options = {}) {
|
|
495
|
+
const t0 = Date.now();
|
|
496
|
+
|
|
497
|
+
// 1. Run base review engine
|
|
498
|
+
const engine = await createReviewEngine();
|
|
499
|
+
const reviewResult = await engine.review(input, {
|
|
500
|
+
language: options.language,
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// 2. Each expert reviews the findings from their perspective
|
|
504
|
+
const expertResults = new Map();
|
|
505
|
+
for (const persona of ALL_PERSONAS) {
|
|
506
|
+
expertResults.set(persona.id, reviewAsExpert(reviewResult.findings, persona));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// 3. Synthesize consensus and conflicts
|
|
510
|
+
const { consensus, conflicts } = synthesize(expertResults);
|
|
511
|
+
|
|
512
|
+
return new PanelResult({
|
|
513
|
+
expertResults,
|
|
514
|
+
consensus,
|
|
515
|
+
conflicts,
|
|
516
|
+
reviewResult,
|
|
517
|
+
totalTime: Date.now() - t0,
|
|
518
|
+
});
|
|
519
|
+
}
|