@lcv-ideas-software/cross-review 4.0.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 (122) hide show
  1. package/CHANGELOG.md +2568 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +26 -0
  4. package/README.md +208 -0
  5. package/SECURITY.md +52 -0
  6. package/dist/scripts/api-streaming-smoke.d.ts +1 -0
  7. package/dist/scripts/api-streaming-smoke.js +78 -0
  8. package/dist/scripts/api-streaming-smoke.js.map +1 -0
  9. package/dist/scripts/runtime-default-smoke.d.ts +1 -0
  10. package/dist/scripts/runtime-default-smoke.js +88 -0
  11. package/dist/scripts/runtime-default-smoke.js.map +1 -0
  12. package/dist/scripts/runtime-smoke.d.ts +1 -0
  13. package/dist/scripts/runtime-smoke.js +148 -0
  14. package/dist/scripts/runtime-smoke.js.map +1 -0
  15. package/dist/scripts/smoke.d.ts +1 -0
  16. package/dist/scripts/smoke.js +6156 -0
  17. package/dist/scripts/smoke.js.map +1 -0
  18. package/dist/src/core/cache-manifest.d.ts +22 -0
  19. package/dist/src/core/cache-manifest.js +133 -0
  20. package/dist/src/core/cache-manifest.js.map +1 -0
  21. package/dist/src/core/caller-tokens.d.ts +32 -0
  22. package/dist/src/core/caller-tokens.js +240 -0
  23. package/dist/src/core/caller-tokens.js.map +1 -0
  24. package/dist/src/core/config.d.ts +9 -0
  25. package/dist/src/core/config.js +643 -0
  26. package/dist/src/core/config.js.map +1 -0
  27. package/dist/src/core/convergence.d.ts +5 -0
  28. package/dist/src/core/convergence.js +186 -0
  29. package/dist/src/core/convergence.js.map +1 -0
  30. package/dist/src/core/cost.d.ts +59 -0
  31. package/dist/src/core/cost.js +359 -0
  32. package/dist/src/core/cost.js.map +1 -0
  33. package/dist/src/core/file-config.d.ts +316 -0
  34. package/dist/src/core/file-config.js +490 -0
  35. package/dist/src/core/file-config.js.map +1 -0
  36. package/dist/src/core/orchestrator.d.ts +199 -0
  37. package/dist/src/core/orchestrator.js +3430 -0
  38. package/dist/src/core/orchestrator.js.map +1 -0
  39. package/dist/src/core/prompt-parts.d.ts +58 -0
  40. package/dist/src/core/prompt-parts.js +122 -0
  41. package/dist/src/core/prompt-parts.js.map +1 -0
  42. package/dist/src/core/relator-lottery.d.ts +23 -0
  43. package/dist/src/core/relator-lottery.js +112 -0
  44. package/dist/src/core/relator-lottery.js.map +1 -0
  45. package/dist/src/core/reports.d.ts +2 -0
  46. package/dist/src/core/reports.js +82 -0
  47. package/dist/src/core/reports.js.map +1 -0
  48. package/dist/src/core/session-store.d.ts +149 -0
  49. package/dist/src/core/session-store.js +1923 -0
  50. package/dist/src/core/session-store.js.map +1 -0
  51. package/dist/src/core/status.d.ts +61 -0
  52. package/dist/src/core/status.js +249 -0
  53. package/dist/src/core/status.js.map +1 -0
  54. package/dist/src/core/timeouts.d.ts +2 -0
  55. package/dist/src/core/timeouts.js +3 -0
  56. package/dist/src/core/timeouts.js.map +1 -0
  57. package/dist/src/core/types.d.ts +604 -0
  58. package/dist/src/core/types.js +36 -0
  59. package/dist/src/core/types.js.map +1 -0
  60. package/dist/src/dashboard/server.d.ts +2 -0
  61. package/dist/src/dashboard/server.js +339 -0
  62. package/dist/src/dashboard/server.js.map +1 -0
  63. package/dist/src/mcp/server.d.ts +54 -0
  64. package/dist/src/mcp/server.js +1584 -0
  65. package/dist/src/mcp/server.js.map +1 -0
  66. package/dist/src/observability/logger.d.ts +9 -0
  67. package/dist/src/observability/logger.js +24 -0
  68. package/dist/src/observability/logger.js.map +1 -0
  69. package/dist/src/peers/anthropic.d.ts +14 -0
  70. package/dist/src/peers/anthropic.js +290 -0
  71. package/dist/src/peers/anthropic.js.map +1 -0
  72. package/dist/src/peers/base.d.ts +72 -0
  73. package/dist/src/peers/base.js +416 -0
  74. package/dist/src/peers/base.js.map +1 -0
  75. package/dist/src/peers/deepseek.d.ts +12 -0
  76. package/dist/src/peers/deepseek.js +246 -0
  77. package/dist/src/peers/deepseek.js.map +1 -0
  78. package/dist/src/peers/errors.d.ts +2 -0
  79. package/dist/src/peers/errors.js +185 -0
  80. package/dist/src/peers/errors.js.map +1 -0
  81. package/dist/src/peers/gemini.d.ts +13 -0
  82. package/dist/src/peers/gemini.js +215 -0
  83. package/dist/src/peers/gemini.js.map +1 -0
  84. package/dist/src/peers/grok.d.ts +17 -0
  85. package/dist/src/peers/grok.js +346 -0
  86. package/dist/src/peers/grok.js.map +1 -0
  87. package/dist/src/peers/model-selection.d.ts +4 -0
  88. package/dist/src/peers/model-selection.js +260 -0
  89. package/dist/src/peers/model-selection.js.map +1 -0
  90. package/dist/src/peers/openai.d.ts +14 -0
  91. package/dist/src/peers/openai.js +299 -0
  92. package/dist/src/peers/openai.js.map +1 -0
  93. package/dist/src/peers/perplexity.d.ts +18 -0
  94. package/dist/src/peers/perplexity.js +375 -0
  95. package/dist/src/peers/perplexity.js.map +1 -0
  96. package/dist/src/peers/registry.d.ts +3 -0
  97. package/dist/src/peers/registry.js +77 -0
  98. package/dist/src/peers/registry.js.map +1 -0
  99. package/dist/src/peers/retry.d.ts +2 -0
  100. package/dist/src/peers/retry.js +36 -0
  101. package/dist/src/peers/retry.js.map +1 -0
  102. package/dist/src/peers/stub.d.ts +13 -0
  103. package/dist/src/peers/stub.js +344 -0
  104. package/dist/src/peers/stub.js.map +1 -0
  105. package/dist/src/peers/text.d.ts +18 -0
  106. package/dist/src/peers/text.js +39 -0
  107. package/dist/src/peers/text.js.map +1 -0
  108. package/dist/src/security/redact.d.ts +2 -0
  109. package/dist/src/security/redact.js +128 -0
  110. package/dist/src/security/redact.js.map +1 -0
  111. package/docs/api-keys.md +34 -0
  112. package/docs/architecture.md +118 -0
  113. package/docs/caching.md +135 -0
  114. package/docs/costs.md +40 -0
  115. package/docs/evidence-preflight.md +88 -0
  116. package/docs/github-security-baseline.md +32 -0
  117. package/docs/model-selection.md +105 -0
  118. package/docs/reports/cross-review-v2-api-capability-smoke-2026-04-30.md +354 -0
  119. package/docs/reports/cross-review-v2-format-recovery-findings-2026-04-28.md +223 -0
  120. package/docs/reports/cross-review-v2-official-provider-docs-refresh-2026-05-05.md +60 -0
  121. package/docs/reports/cross-review-v2-token-streaming-smoke-2026-04-30.md +119 -0
  122. package/package.json +88 -0
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env node
2
+ import http from "node:http";
3
+ import { loadConfig, VERSION } from "../core/config.js";
4
+ import { CrossReviewOrchestrator } from "../core/orchestrator.js";
5
+ import { sessionReportMarkdown } from "../core/reports.js";
6
+ import { EventLog } from "../observability/logger.js";
7
+ const config = loadConfig();
8
+ const eventLog = new EventLog(config);
9
+ const holder = {};
10
+ const orchestrator = new CrossReviewOrchestrator(config, (event) => {
11
+ eventLog.emit(event);
12
+ holder.orchestrator?.store.appendEvent(event);
13
+ });
14
+ holder.orchestrator = orchestrator;
15
+ function sendJson(response, value) {
16
+ response.writeHead(200, {
17
+ "content-type": "application/json; charset=utf-8",
18
+ "cache-control": "no-store",
19
+ });
20
+ response.end(JSON.stringify(value, null, 2));
21
+ }
22
+ function sendHtml(response, html) {
23
+ // v2.4.0 / audit closure: Content-Security-Policy + X-Frame-Options.
24
+ // The dashboard binds only to 127.0.0.1 (defense via network), but
25
+ // local processes can still load it. The CSP confines the page to
26
+ // its own origin (no third-party scripts/styles, no remote fetch),
27
+ // which blocks click-bait DOM injection if a future change accepts
28
+ // unsanitized input. `unsafe-inline` is required because the page
29
+ // ships an inline <style> + <script> by design; the style-src/script-src
30
+ // directives still block REMOTE inline injection. Anti-clickjacking
31
+ // headers complement the CSP for in-frame embedding attempts.
32
+ response.writeHead(200, {
33
+ "content-type": "text/html; charset=utf-8",
34
+ "cache-control": "no-store",
35
+ "content-security-policy": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'",
36
+ "x-frame-options": "DENY",
37
+ "x-content-type-options": "nosniff",
38
+ "referrer-policy": "no-referrer",
39
+ });
40
+ response.end(html);
41
+ }
42
+ function notFound(response) {
43
+ response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
44
+ response.end("Not found");
45
+ }
46
+ function html() {
47
+ return `<!doctype html>
48
+ <html lang="pt-BR">
49
+ <head>
50
+ <meta charset="utf-8" />
51
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
52
+ <title>Cross Review</title>
53
+ <style>
54
+ :root { color-scheme: light; font-family: Inter, Segoe UI, Arial, sans-serif; color: #102033; background: #f6f8fb; }
55
+ * { box-sizing: border-box; }
56
+ body { margin: 0; }
57
+ main { max-width: 1180px; margin: 0 auto; padding: 32px; }
58
+ header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 24px; }
59
+ h1 { margin: 0; font-size: 28px; }
60
+ h2 { margin: 0 0 10px; font-size: 18px; }
61
+ .muted { color: #52647b; }
62
+ .badge { border: 1px solid #cbd7e8; border-radius: 999px; padding: 6px 12px; background: white; font-weight: 700; }
63
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
64
+ .card, .session { background: white; border: 1px solid #d8e1ee; border-radius: 8px; padding: 16px; box-shadow: 0 8px 20px rgb(16 32 51 / 0.05); }
65
+ .metric strong { display: block; font-size: 24px; margin-top: 6px; }
66
+ .toolbar { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin: 20px 0 14px; }
67
+ input, select, button { min-height: 38px; border-radius: 8px; border: 1px solid #cbd7e8; padding: 0 12px; font: inherit; }
68
+ input { flex: 1 1 260px; }
69
+ button { font-weight: 800; color: white; background: #1f6feb; cursor: pointer; border: 0; }
70
+ button.secondary { color: #102033; background: #eef3f9; border: 1px solid #cbd7e8; }
71
+ button:hover { transform: translateY(-1px); }
72
+ .sessions { display: grid; gap: 12px; margin-top: 12px; }
73
+ .session { cursor: pointer; }
74
+ .session:hover { border-color: #1f6feb; }
75
+ .session strong { display: block; margin-bottom: 6px; }
76
+ .row { display: flex; gap: 12px; flex-wrap: wrap; color: #52647b; font-size: 14px; }
77
+ .detail-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(320px, 0.8fr); gap: 14px; margin-top: 18px; }
78
+ pre, .timeline { max-height: 520px; overflow: auto; white-space: pre-wrap; background: #0f172a; color: #e5e7eb; border-radius: 8px; padding: 16px; }
79
+ .timeline { background: white; color: #102033; border: 1px solid #d8e1ee; }
80
+ .event { border-left: 3px solid #1f6feb; padding: 8px 0 8px 10px; margin: 0 0 8px; }
81
+ .event small { color: #52647b; display: block; }
82
+ table.peer-health { width: 100%; border-collapse: collapse; font-size: 14px; }
83
+ table.peer-health th, table.peer-health td { border-bottom: 1px solid #e5ecf5; padding: 8px 10px; text-align: left; }
84
+ table.peer-health th { font-weight: 700; color: #52647b; background: #f6f8fb; }
85
+ table.peer-health td.num { text-align: right; font-variant-numeric: tabular-nums; }
86
+ table.peer-health td.peer { font-weight: 700; }
87
+ @media (max-width: 820px) { main { padding: 20px; } header, .detail-grid { display: block; } .badge { display: inline-block; margin-top: 10px; } table.peer-health { font-size: 12px; } }
88
+ </style>
89
+ </head>
90
+ <body>
91
+ <main>
92
+ <header>
93
+ <div>
94
+ <h1>Cross Review</h1>
95
+ <div class="muted">APIs oficiais, sessões duráveis, unanimidade obrigatória</div>
96
+ </div>
97
+ <div class="badge">v${VERSION}</div>
98
+ </header>
99
+ <section class="grid" id="metrics">
100
+ <article class="card metric"><span>Sessões</span><strong>...</strong></article>
101
+ <article class="card metric"><span>Convergidas</span><strong>...</strong></article>
102
+ <article class="card metric"><span>Rodadas</span><strong>...</strong></article>
103
+ <article class="card metric"><span>Custo</span><strong>...</strong></article>
104
+ </section>
105
+ <section class="card" id="peer-health" style="margin-top:14px">
106
+ <h2>Saúde por provider</h2>
107
+ <div class="muted" style="margin-bottom:8px">READY rate, NEEDS_EVIDENCE rate, custo médio e parser warnings por peer (todas as sessões salvas).</div>
108
+ <div id="peer-health-body" class="muted">Carregando...</div>
109
+ </section>
110
+ <section class="card" id="shadow-judgment" style="margin-top:14px">
111
+ <h2>Judge shadow (decisões observadas)</h2>
112
+ <div class="muted" style="margin-bottom:8px">Decisões emitidas pelo judge em modo shadow agregadas por peer julgador. Não muta estado — observabilidade pré v2.13.</div>
113
+ <div id="shadow-judgment-body" class="muted">Carregando...</div>
114
+ </section>
115
+ <section class="grid" style="margin-top:14px">
116
+ <article class="card"><strong>Dados</strong><p class="muted">${config.data_dir}</p></article>
117
+ <article class="card"><strong>Logs</strong><p class="muted">${eventLog.path()}</p></article>
118
+ </section>
119
+ <div class="toolbar">
120
+ <input id="filter" placeholder="Filtrar por sessão, estado ou texto..." />
121
+ <select id="state">
122
+ <option value="">Todos os estados</option>
123
+ <option value="running">Em execução</option>
124
+ <option value="converged">Convergidas</option>
125
+ <option value="blocked">Bloqueadas</option>
126
+ <option value="stale">Interrompidas</option>
127
+ </select>
128
+ <button id="refresh">Atualizar</button>
129
+ <button id="report" class="secondary" disabled>Relatório</button>
130
+ </div>
131
+ <section id="sessions" class="sessions">Carregando...</section>
132
+ <section class="detail-grid">
133
+ <pre id="details">Selecione uma sessão para ver detalhes.</pre>
134
+ <div class="timeline" id="timeline">A timeline aparecerá aqui.</div>
135
+ </section>
136
+ </main>
137
+ <script>
138
+ var selectedSession = null;
139
+ function escapeHtml(value) {
140
+ return String(value ?? '').replace(/[&<>"']/g, ch => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]));
141
+ }
142
+ function money(value) {
143
+ return value == null ? 'desconhecido' : '$' + Number(value).toFixed(6);
144
+ }
145
+ function stateOf(session) {
146
+ return session.outcome || session.convergence_health?.state || 'em andamento';
147
+ }
148
+ function pct(value) {
149
+ if (value == null || !Number.isFinite(value)) return '—';
150
+ return (value * 100).toFixed(1) + '%';
151
+ }
152
+ function renderShadowJudgment(rollup) {
153
+ if (!rollup || !rollup.decisions_total) {
154
+ return '<div class="muted">Nenhuma decisão shadow observada. Ative o judge shadow setando CROSS_REVIEW_EVIDENCE_JUDGE_AUTOWIRE_MODE=shadow + _PEER=codex.</div>';
155
+ }
156
+ const peers = Object.values(rollup.by_judge_peer || {}).filter(Boolean).sort((a, b) => b.decisions_total - a.decisions_total);
157
+ const head = '<thead><tr>' +
158
+ ['Judge peer','Decisões','Would promote','Skip (sat. unverified)','Skip (not satisfied)','Verified','Inferred','Unknown','Primeira','Última']
159
+ .map(h => '<th>' + h + '</th>').join('') + '</tr></thead>';
160
+ const body = '<tbody>' + peers.map(p => {
161
+ const conf = p.by_confidence || {};
162
+ const promoteRate = p.decisions_total ? (p.would_promote / p.decisions_total) : 0;
163
+ return '<tr>' +
164
+ '<td class="peer">' + escapeHtml(p.judge_peer) + '</td>' +
165
+ '<td class="num">' + p.decisions_total + '</td>' +
166
+ '<td class="num">' + p.would_promote + ' (' + pct(promoteRate) + ')</td>' +
167
+ '<td class="num">' + p.would_skip_satisfied_unverified + '</td>' +
168
+ '<td class="num">' + p.would_skip_not_satisfied + '</td>' +
169
+ '<td class="num">' + (conf.verified || 0) + '</td>' +
170
+ '<td class="num">' + (conf.inferred || 0) + '</td>' +
171
+ '<td class="num">' + (conf.unknown || 0) + '</td>' +
172
+ '<td class="num">' + escapeHtml(p.first_seen_at || '—') + '</td>' +
173
+ '<td class="num">' + escapeHtml(p.last_seen_at || '—') + '</td>' +
174
+ '</tr>';
175
+ }).join('') + '</tbody>';
176
+ const summary = '<p class="muted" style="margin:0 0 8px">Total: <strong>' + rollup.decisions_total + '</strong> decisão(ões) — ' +
177
+ rollup.would_promote_total + ' would_promote (' + pct(rollup.decisions_total ? rollup.would_promote_total / rollup.decisions_total : 0) + ')</p>';
178
+ return summary + '<table class="peer-health">' + head + body + '</table>';
179
+ }
180
+ function renderPeerHealth(perPeer) {
181
+ const peers = Object.values(perPeer || {}).filter(Boolean).sort((a, b) => b.results_total - a.results_total);
182
+ if (!peers.length) return '<div class="muted">Nenhum resultado de peer registrado ainda.</div>';
183
+ const head = '<thead><tr>' +
184
+ ['Peer','Resultados','READY','NEEDS_EVIDENCE','NOT_READY','READY rate','NE rate','Custo total','Custo médio','Parser warns','Rejections']
185
+ .map(h => '<th>' + h + '</th>').join('') + '</tr></thead>';
186
+ const body = '<tbody>' + peers.map(p =>
187
+ '<tr>' +
188
+ '<td class="peer">' + escapeHtml(p.peer) + '</td>' +
189
+ '<td class="num">' + p.results_total + '</td>' +
190
+ '<td class="num">' + p.ready_count + '</td>' +
191
+ '<td class="num">' + p.needs_evidence_count + '</td>' +
192
+ '<td class="num">' + p.not_ready_count + '</td>' +
193
+ '<td class="num">' + pct(p.ready_rate) + '</td>' +
194
+ '<td class="num">' + pct(p.needs_evidence_rate) + '</td>' +
195
+ '<td class="num">' + (p.total_cost_usd == null ? '—' : '$' + p.total_cost_usd.toFixed(4)) + '</td>' +
196
+ '<td class="num">' + (p.avg_cost_usd == null ? '—' : '$' + p.avg_cost_usd.toFixed(6)) + '</td>' +
197
+ '<td class="num">' + p.parser_warnings_total + '</td>' +
198
+ '<td class="num">' + p.rejected_total + '</td>' +
199
+ '</tr>'
200
+ ).join('') + '</tbody>';
201
+ return '<table class="peer-health">' + head + body + '</table>';
202
+ }
203
+ async function refreshMetrics() {
204
+ const metrics = await fetch('/api/metrics').then(r => r.json());
205
+ document.getElementById('metrics').innerHTML = [
206
+ ['Sessões', metrics.sessions.total],
207
+ ['Convergidas', metrics.sessions.converged],
208
+ ['Rodadas', metrics.rounds],
209
+ ['Custo', money(metrics.total_cost.total_cost)],
210
+ ].map(([label, value]) => \`<article class="card metric"><span>\${label}</span><strong>\${value}</strong></article>\`).join('');
211
+ document.getElementById('peer-health-body').innerHTML = renderPeerHealth(metrics.per_peer_health);
212
+ document.getElementById('shadow-judgment-body').innerHTML = renderShadowJudgment(metrics.shadow_judgment);
213
+ }
214
+ async function refresh() {
215
+ await refreshMetrics();
216
+ const data = await fetch('/api/sessions').then(r => r.json());
217
+ const filter = document.getElementById('filter').value.toLowerCase();
218
+ const wanted = document.getElementById('state').value;
219
+ const visible = data.filter(session => {
220
+ const state = stateOf(session);
221
+ const haystack = JSON.stringify({ id: session.session_id, state, health: session.convergence_health }).toLowerCase();
222
+ return (!wanted || state === wanted || session.convergence_health?.state === wanted) && (!filter || haystack.includes(filter));
223
+ });
224
+ const container = document.getElementById('sessions');
225
+ if (!visible.length) {
226
+ container.textContent = 'Nenhuma sessão encontrada.';
227
+ return;
228
+ }
229
+ container.innerHTML = visible.map(session => {
230
+ const health = session.convergence_health || {};
231
+ const cost = session.totals?.cost?.total_cost;
232
+ return \`<article class="session" data-session="\${session.session_id}">
233
+ <strong>\${escapeHtml(session.session_id)}</strong>
234
+ <div class="row">
235
+ <span>estado: \${escapeHtml(stateOf(session))}</span>
236
+ <span>rodadas: \${session.rounds?.length || 0}</span>
237
+ <span>custo: \${money(cost)}</span>
238
+ <span>atualizada: \${escapeHtml(session.updated_at)}</span>
239
+ </div>
240
+ <div class="muted">\${escapeHtml(health.detail || '')}</div>
241
+ </article>\`;
242
+ }).join('');
243
+ for (const node of container.querySelectorAll('.session')) {
244
+ node.addEventListener('click', async () => selectSession(node.dataset.session));
245
+ }
246
+ }
247
+ async function selectSession(id) {
248
+ selectedSession = id;
249
+ document.getElementById('report').disabled = false;
250
+ const [session, events, metrics] = await Promise.all([
251
+ fetch('/api/sessions/' + id).then(r => r.json()),
252
+ fetch('/api/sessions/' + id + '/events').then(r => r.json()),
253
+ fetch('/api/metrics?session_id=' + encodeURIComponent(id)).then(r => r.json()),
254
+ ]);
255
+ document.getElementById('details').textContent = JSON.stringify({ session, metrics }, null, 2);
256
+ document.getElementById('timeline').innerHTML = (events || []).slice(-80).map(event => \`
257
+ <div class="event">
258
+ <small>#\${event.seq} \${escapeHtml(event.ts || '')} \${escapeHtml(event.type || '')}\${event.peer ? '/' + escapeHtml(event.peer) : ''}</small>
259
+ <div>\${escapeHtml(event.message || '')}</div>
260
+ </div>\`).join('') || 'Sem eventos.';
261
+ }
262
+ async function openReport() {
263
+ if (!selectedSession) return;
264
+ const report = await fetch('/api/sessions/' + selectedSession + '/report').then(r => r.text());
265
+ document.getElementById('details').textContent = report;
266
+ }
267
+ document.getElementById('refresh').addEventListener('click', refresh);
268
+ document.getElementById('filter').addEventListener('input', refresh);
269
+ document.getElementById('state').addEventListener('change', refresh);
270
+ document.getElementById('report').addEventListener('click', openReport);
271
+ refresh();
272
+ </script>
273
+ </body>
274
+ </html>`;
275
+ }
276
+ const server = http.createServer(async (request, response) => {
277
+ const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
278
+ try {
279
+ if (url.pathname === "/") {
280
+ sendHtml(response, html());
281
+ return;
282
+ }
283
+ if (url.pathname === "/api/health") {
284
+ sendJson(response, {
285
+ ok: true,
286
+ version: VERSION,
287
+ data_dir: config.data_dir,
288
+ log_file: eventLog.path(),
289
+ stub: config.stub,
290
+ });
291
+ return;
292
+ }
293
+ if (url.pathname === "/api/probe") {
294
+ sendJson(response, await orchestrator.probeAll());
295
+ return;
296
+ }
297
+ if (url.pathname === "/api/metrics") {
298
+ sendJson(response, orchestrator.store.metrics(url.searchParams.get("session_id") ?? undefined));
299
+ return;
300
+ }
301
+ if (url.pathname === "/api/sessions") {
302
+ sendJson(response, orchestrator.store.list());
303
+ return;
304
+ }
305
+ const sessionMatch = url.pathname.match(/^\/api\/sessions\/([a-f0-9-]{36})$/);
306
+ if (sessionMatch) {
307
+ sendJson(response, orchestrator.store.read(sessionMatch[1]));
308
+ return;
309
+ }
310
+ const eventsMatch = url.pathname.match(/^\/api\/sessions\/([a-f0-9-]{36})\/events$/);
311
+ if (eventsMatch) {
312
+ const since = Number(url.searchParams.get("since_seq") ?? 0);
313
+ sendJson(response, orchestrator.store.readEvents(eventsMatch[1], since));
314
+ return;
315
+ }
316
+ const reportMatch = url.pathname.match(/^\/api\/sessions\/([a-f0-9-]{36})\/report$/);
317
+ if (reportMatch) {
318
+ const session = orchestrator.store.read(reportMatch[1]);
319
+ const markdown = sessionReportMarkdown(session, orchestrator.store.readEvents(reportMatch[1]));
320
+ orchestrator.store.saveReport(reportMatch[1], markdown);
321
+ response.writeHead(200, {
322
+ "content-type": "text/markdown; charset=utf-8",
323
+ "cache-control": "no-store",
324
+ });
325
+ response.end(markdown);
326
+ return;
327
+ }
328
+ notFound(response);
329
+ }
330
+ catch {
331
+ console.error("dashboard_request_failed");
332
+ response.writeHead(500, { "content-type": "application/json; charset=utf-8" });
333
+ response.end(JSON.stringify({ ok: false, error: "internal_server_error" }));
334
+ }
335
+ });
336
+ server.listen(config.dashboard_port, "127.0.0.1", () => {
337
+ console.log(`Cross Review dashboard: http://127.0.0.1:${config.dashboard_port}`);
338
+ });
339
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../../src/dashboard/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ import { z } from "zod";
3
+ import type { ConvergenceScope, PeerId, RuntimeEvent } from "../core/types.js";
4
+ export declare const SessionIdSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
5
+ export type ClientInfo = {
6
+ name?: string;
7
+ version?: string;
8
+ } | undefined;
9
+ import { type HostTokensRecord, type ParentProcessSnapshot } from "../core/caller-tokens.js";
10
+ export declare function getHostTokensRecord(): HostTokensRecord | null;
11
+ export declare function setHostTokensRecord(record: HostTokensRecord | null): void;
12
+ export declare function initHostTokensRecord(dataDir: string): void;
13
+ export declare function getCallerCandidatesFromClientInfo(clientInfo: ClientInfo): PeerId[];
14
+ export type IdentityVerificationMethod = "token" | "client_info" | "none";
15
+ export interface CallerIdentityResult {
16
+ identity_verified: boolean;
17
+ verification_method: IdentityVerificationMethod;
18
+ client_info_name: string | null;
19
+ identity_metadata: ParentProcessSnapshot;
20
+ }
21
+ export declare function verifyCallerIdentity(declaredCaller: PeerId | "operator", clientInfo: ClientInfo): CallerIdentityResult;
22
+ export declare function lockCallerPeerSelection<T extends {
23
+ peers?: PeerId[];
24
+ lead_peer?: PeerId;
25
+ caller?: PeerId | "operator";
26
+ session_id?: string;
27
+ }>(input: T, ctx: {
28
+ site: "ask_peers" | "session_start_round" | "run_until_unanimous" | "session_start_unanimous";
29
+ emit: (event: RuntimeEvent) => void;
30
+ enabledPeers?: readonly PeerId[];
31
+ }): T;
32
+ export declare function buildResponseNotices<T extends {
33
+ peers?: PeerId[];
34
+ lead_peer?: PeerId;
35
+ caller?: PeerId | "operator";
36
+ }>(originalInput: T, output: {
37
+ session?: {
38
+ convergence_scope?: ConvergenceScope;
39
+ };
40
+ }): string[];
41
+ type JobKind = "ask_peers" | "run_until_unanimous";
42
+ export type JobStatus = {
43
+ job_id: string;
44
+ kind: JobKind;
45
+ session_id: string;
46
+ status: "running" | "completed" | "failed" | "cancelled";
47
+ started_at: string;
48
+ completed_at?: string;
49
+ error?: string;
50
+ result_summary?: Record<string, unknown>;
51
+ };
52
+ export declare function pruneCompletedJobs(jobs: Map<string, JobStatus>, maxCompleted?: number): void;
53
+ export declare function main(): Promise<void>;
54
+ export {};