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.
Files changed (54) hide show
  1. package/bin/cipher.js +10 -0
  2. package/lib/analyze/consistency.js +566 -0
  3. package/lib/analyze/constitution.js +110 -0
  4. package/lib/analyze/sharding.js +251 -0
  5. package/lib/autonomous/agent-tool.js +165 -0
  6. package/lib/autonomous/framework.js +17 -0
  7. package/lib/autonomous/handoff.js +506 -0
  8. package/lib/autonomous/modes/blue.js +26 -0
  9. package/lib/autonomous/modes/red.js +28 -0
  10. package/lib/benchmark/agent.js +88 -26
  11. package/lib/benchmark/baselines.js +3 -0
  12. package/lib/benchmark/claude-code-solver.js +254 -0
  13. package/lib/benchmark/cognitive.js +283 -0
  14. package/lib/benchmark/index.js +12 -2
  15. package/lib/benchmark/knowledge.js +281 -0
  16. package/lib/benchmark/llm.js +156 -15
  17. package/lib/benchmark/models.js +5 -2
  18. package/lib/benchmark/nyu-ctf.js +192 -0
  19. package/lib/benchmark/overthewire.js +347 -0
  20. package/lib/benchmark/picoctf.js +281 -0
  21. package/lib/benchmark/prompts.js +280 -0
  22. package/lib/benchmark/registry.js +219 -0
  23. package/lib/benchmark/remote-solver.js +356 -0
  24. package/lib/benchmark/remote-target.js +263 -0
  25. package/lib/benchmark/reporter.js +35 -0
  26. package/lib/benchmark/runner.js +174 -10
  27. package/lib/benchmark/sandbox.js +35 -0
  28. package/lib/benchmark/scorer.js +22 -4
  29. package/lib/benchmark/solver.js +34 -1
  30. package/lib/benchmark/tools.js +262 -16
  31. package/lib/commands.js +9 -0
  32. package/lib/execution/council.js +434 -0
  33. package/lib/execution/parallel.js +292 -0
  34. package/lib/gates/circuit-breaker.js +135 -0
  35. package/lib/gates/confidence.js +302 -0
  36. package/lib/gates/corrections.js +219 -0
  37. package/lib/gates/self-check.js +245 -0
  38. package/lib/gateway/commands.js +727 -0
  39. package/lib/guardrails/engine.js +364 -0
  40. package/lib/mcp/server.js +349 -3
  41. package/lib/memory/compressor.js +94 -7
  42. package/lib/pipeline/hooks.js +288 -0
  43. package/lib/pipeline/index.js +11 -0
  44. package/lib/review/budget.js +210 -0
  45. package/lib/review/engine.js +526 -0
  46. package/lib/review/layers/acceptance-auditor.js +279 -0
  47. package/lib/review/layers/blind-hunter.js +500 -0
  48. package/lib/review/layers/defense-in-depth.js +209 -0
  49. package/lib/review/layers/edge-case-hunter.js +266 -0
  50. package/lib/review/panel.js +519 -0
  51. package/lib/review/two-stage.js +244 -0
  52. package/lib/session/cost-tracker.js +203 -0
  53. package/lib/session/logger.js +349 -0
  54. 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
+ }