@praneeth_54/agentdoctor 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -1
- package/README.md +10 -8
- package/dist/agent/chat/deterministic.js +22 -4
- package/dist/agent/context/retrieve.d.ts +2 -1
- package/dist/agent/context/retrieve.js +14 -1
- package/dist/agent/tools/execute.js +39 -43
- package/dist/agent/tools/write.d.ts +1 -1
- package/dist/agent/tools/write.js +18 -4
- package/dist/ai/providers/adversarial-local.d.ts +7 -0
- package/dist/ai/providers/adversarial-local.js +146 -0
- package/dist/cli/commands/agent.js +13 -3
- package/dist/cli/commands/architecture.js +7 -2
- package/dist/cli/commands/brain.js +7 -2
- package/dist/cli/commands/chat.js +13 -3
- package/dist/cli/commands/complete.js +13 -3
- package/dist/cli/commands/fix.js +6 -0
- package/dist/cli/commands/learn.js +7 -2
- package/dist/cli/commands/mcp.js +7 -3
- package/dist/cli/commands/platform.js +7 -2
- package/dist/cli/commands/policy-graph-run.js +7 -0
- package/dist/cli/commands/product.js +13 -3
- package/dist/cli/commands/scan.js +6 -0
- package/dist/cli/commands/start.js +22 -7
- package/dist/cli/commands/v2.js +61 -11
- package/dist/cli/commands/verify.js +6 -0
- package/dist/cli/safe-root.d.ts +17 -0
- package/dist/cli/safe-root.js +32 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/brain-cli/service.js +5 -2
- package/dist/core/monorepo/detect.js +10 -0
- package/dist/core/secrets/scan.js +9 -0
- package/dist/core/understanding/brain/storage/store.d.ts +4 -0
- package/dist/core/understanding/brain/storage/store.js +22 -2
- package/dist/dashboard/page.d.ts +2 -0
- package/dist/dashboard/page.js +53 -0
- package/dist/dashboard/server.js +20 -244
- package/dist/dashboard/ui/client.d.ts +2 -0
- package/dist/dashboard/ui/client.js +2 -0
- package/dist/dashboard/ui/styles.d.ts +2 -0
- package/dist/dashboard/ui/styles.js +138 -0
- package/dist/discovery/files.js +13 -0
- package/dist/intelligence/graph/build.js +10 -30
- package/dist/intelligence/graph/incremental.d.ts +2 -0
- package/dist/intelligence/graph/incremental.js +9 -0
- package/dist/mcp/intelligence/path-safety.d.ts +6 -1
- package/dist/mcp/intelligence/path-safety.js +55 -2
- package/dist/platform/graph/build.js +9 -0
- package/dist/product/decisions/ledger.d.ts +13 -1
- package/dist/product/decisions/ledger.js +41 -10
- package/dist/product/discovery/roots.d.ts +10 -0
- package/dist/product/discovery/roots.js +137 -15
- package/dist/product/graph/enrich-languages.js +7 -32
- package/dist/product/index.d.ts +4 -1
- package/dist/product/index.js +2 -1
- package/dist/product/map/software-map.js +2 -1
- package/dist/product/memory/institutional.js +5 -0
- package/dist/product/search/software-search.js +5 -0
- package/dist/product/twin/store.js +9 -1
- package/dist/product/whatif/engine.js +22 -1
- package/dist/project/ownership.d.ts +65 -0
- package/dist/project/ownership.js +169 -0
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { atomicWriteTextFile } from "../../../../utils/fs.js";
|
|
5
|
+
import { OWNERSHIP_BOUNDARY_VERSION } from "../../../../project/ownership.js";
|
|
5
6
|
import { parseBrainDelta, serializeBrainDelta } from "../delta.js";
|
|
6
7
|
import { checkBrainCompatibility, migrateBrain } from "../migrate.js";
|
|
7
8
|
import { assertBrainContract } from "../contract.js";
|
|
@@ -198,6 +199,7 @@ export class LocalBrainStore {
|
|
|
198
199
|
projectName: brain.metadata.projectName,
|
|
199
200
|
brainId: brain.metadata.brainId,
|
|
200
201
|
checksum: nextChecksum,
|
|
202
|
+
ownershipBoundaryVersion: OWNERSHIP_BOUNDARY_VERSION,
|
|
201
203
|
};
|
|
202
204
|
const meta = await this.readMeta();
|
|
203
205
|
const snapshots = meta.snapshots.filter((s) => s.id !== snapshotId);
|
|
@@ -209,10 +211,15 @@ export class LocalBrainStore {
|
|
|
209
211
|
projectName: brain.metadata.projectName,
|
|
210
212
|
latestSnapshotId: snapshotId,
|
|
211
213
|
snapshots,
|
|
214
|
+
ownershipBoundaryVersion: OWNERSHIP_BOUNDARY_VERSION,
|
|
212
215
|
});
|
|
213
216
|
return snapMeta;
|
|
214
217
|
}
|
|
215
218
|
async loadSnapshot(snapshotId) {
|
|
219
|
+
const meta = await this.readMeta();
|
|
220
|
+
if (meta.ownershipBoundaryVersion !== OWNERSHIP_BOUNDARY_VERSION) {
|
|
221
|
+
throw new BrainStorageError(`brain store ownership boundary is stale (have ${String(meta.ownershipBoundaryVersion ?? "missing")}, need ${OWNERSHIP_BOUNDARY_VERSION}); rebuild required`);
|
|
222
|
+
}
|
|
216
223
|
const target = await assertInsideRootResolved(this.root, this.brainPath(snapshotId));
|
|
217
224
|
let raw;
|
|
218
225
|
try {
|
|
@@ -224,9 +231,17 @@ export class LocalBrainStore {
|
|
|
224
231
|
}
|
|
225
232
|
throw error;
|
|
226
233
|
}
|
|
227
|
-
const meta = await this.readMeta();
|
|
228
234
|
const snap = meta.snapshots.find((s) => s.id === snapshotId);
|
|
229
235
|
if (snap) {
|
|
236
|
+
if (snap.ownershipBoundaryVersion !== undefined &&
|
|
237
|
+
snap.ownershipBoundaryVersion !== OWNERSHIP_BOUNDARY_VERSION) {
|
|
238
|
+
throw new BrainStorageError(`snapshot ${snapshotId} ownership boundary is stale (have ${snap.ownershipBoundaryVersion}, need ${OWNERSHIP_BOUNDARY_VERSION}); rebuild required`);
|
|
239
|
+
}
|
|
240
|
+
// Pre-stamp snapshots created before ownershipBoundaryVersion was recorded are
|
|
241
|
+
// unsafe to serve once the store claims a current boundary — require rebuild.
|
|
242
|
+
if (snap.ownershipBoundaryVersion === undefined) {
|
|
243
|
+
throw new BrainStorageError(`snapshot ${snapshotId} missing ownershipBoundaryVersion; rebuild required`);
|
|
244
|
+
}
|
|
230
245
|
const actual = checksumPayload(raw);
|
|
231
246
|
if (actual !== snap.checksum) {
|
|
232
247
|
throw new BrainStorageError(`checksum mismatch for snapshot ${snapshotId}`);
|
|
@@ -261,7 +276,12 @@ export class LocalBrainStore {
|
|
|
261
276
|
return meta.latestSnapshotId;
|
|
262
277
|
}
|
|
263
278
|
async loadLatest() {
|
|
264
|
-
const
|
|
279
|
+
const meta = await this.readMeta();
|
|
280
|
+
if (meta.ownershipBoundaryVersion !== OWNERSHIP_BOUNDARY_VERSION) {
|
|
281
|
+
// Pre-boundary or outdated snapshots must not be served as current project truth.
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
const id = meta.latestSnapshotId;
|
|
265
285
|
if (!id) {
|
|
266
286
|
return null;
|
|
267
287
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { PACKAGE_VERSION } from "../constants.js";
|
|
2
|
+
import { DASHBOARD_CSS } from "./ui/styles.js";
|
|
3
|
+
import { DASHBOARD_CLIENT_JS } from "./ui/client.js";
|
|
4
|
+
/** Embedded dashboard HTML. Version injected only from PACKAGE_VERSION. */
|
|
5
|
+
export function htmlPage() {
|
|
6
|
+
const version = PACKAGE_VERSION;
|
|
7
|
+
const client = DASHBOARD_CLIENT_JS.replaceAll("%%PACKAGE_VERSION%%", version).replaceAll("</script>", "<\\/script>");
|
|
8
|
+
return `<!doctype html>
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="utf-8" />
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
13
|
+
<title>AgentDoctor ${version}</title>
|
|
14
|
+
<style>${DASHBOARD_CSS}</style>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<div class="app" id="app">
|
|
18
|
+
<header class="topbar" role="banner">
|
|
19
|
+
<button type="button" class="btn btn-ghost mobile-nav" id="btnMenu" aria-label="Open navigation">☰</button>
|
|
20
|
+
<div class="brand">
|
|
21
|
+
<span class="brand-name">AgentDoctor</span>
|
|
22
|
+
<span class="brand-ver" id="headerVersion">v${version}</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="project-chip" title="Active project">
|
|
25
|
+
<strong id="projectName">Loading…</strong>
|
|
26
|
+
<span id="projectMeta">Detecting stack…</span>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="topbar-spacer"></div>
|
|
29
|
+
<div class="topbar-actions">
|
|
30
|
+
<span class="topbar-status" id="topbarStatus" title="Dashboard mode"><span class="status-dot" aria-hidden="true"></span>Local · Read-only</span>
|
|
31
|
+
<button type="button" class="btn" id="btnCommand" aria-label="Open command palette">Search <span class="kbd">⌘K</span></button>
|
|
32
|
+
<a class="btn" href="#system">System</a>
|
|
33
|
+
</div>
|
|
34
|
+
</header>
|
|
35
|
+
<aside class="sidebar" id="sidebar" aria-label="Primary">
|
|
36
|
+
<button type="button" class="btn btn-ghost side-toggle" id="btnCollapse" aria-label="Collapse sidebar">⇄</button>
|
|
37
|
+
<nav id="sidebarNav"></nav>
|
|
38
|
+
</aside>
|
|
39
|
+
<main class="main" role="main">
|
|
40
|
+
<div class="content" id="pageRoot"></div>
|
|
41
|
+
</main>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="drawer-backdrop" id="drawerBackdrop"></div>
|
|
44
|
+
<div class="palette" id="palette" role="dialog" aria-modal="true" aria-label="Command palette">
|
|
45
|
+
<div class="palette-box">
|
|
46
|
+
<input id="paletteInput" placeholder="Go to page or search project…" aria-label="Command search" />
|
|
47
|
+
<div class="palette-list" id="paletteList"></div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<script>${client}</script>
|
|
51
|
+
</body>
|
|
52
|
+
</html>`;
|
|
53
|
+
}
|
package/dist/dashboard/server.js
CHANGED
|
@@ -20,7 +20,6 @@ import { listKnowledge } from "../knowledge/store.js";
|
|
|
20
20
|
import { CONTRACTS_VERSION } from "../contracts/index.js";
|
|
21
21
|
import { collectOpsHealth } from "../ops/health.js";
|
|
22
22
|
import { listPolicyPacks } from "../policy/packs.js";
|
|
23
|
-
import { sanitizeForOutput } from "../utils/path.js";
|
|
24
23
|
import { createModelProvider, loadAiConfig } from "../ai/index.js";
|
|
25
24
|
import { ChatService } from "../agent/chat/service.js";
|
|
26
25
|
import { formatChatResponseForCli } from "../agent/chat/response.js";
|
|
@@ -43,10 +42,15 @@ import { analyzeFeatureIntelligence } from "../product/features/intelligence.js"
|
|
|
43
42
|
import { buildSoftwareEvolutionTimeline } from "../product/evolution/timeline.js";
|
|
44
43
|
import { queryMemory } from "../product/memory/institutional.js";
|
|
45
44
|
import { loadDecisionLedger } from "../product/decisions/ledger.js";
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
import { ProjectOwnershipError } from "../project/ownership.js";
|
|
46
|
+
import { PathEscapeError } from "../security/paths.js";
|
|
47
|
+
import { htmlPage } from "./page.js";
|
|
48
|
+
/** HTTP-safe client error — never returns Error.message/stack (CodeQL js/stack-trace-exposure). */
|
|
49
|
+
function clientErrorMessage(error) {
|
|
50
|
+
if (error instanceof ProjectOwnershipError || error instanceof PathEscapeError) {
|
|
51
|
+
return "path outside project ownership";
|
|
52
|
+
}
|
|
53
|
+
return "internal error";
|
|
50
54
|
}
|
|
51
55
|
function pathnameLooksHostile(pathname) {
|
|
52
56
|
const lower = pathname.toLowerCase();
|
|
@@ -87,242 +91,6 @@ export function isLoopbackHost(host) {
|
|
|
87
91
|
normalized === "localhost" ||
|
|
88
92
|
normalized === "0:0:0:0:0:0:0:1");
|
|
89
93
|
}
|
|
90
|
-
function htmlPage() {
|
|
91
|
-
return `<!doctype html>
|
|
92
|
-
<html lang="en">
|
|
93
|
-
<head>
|
|
94
|
-
<meta charset="utf-8" />
|
|
95
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
96
|
-
<title>AgentDoctor Dashboard</title>
|
|
97
|
-
<style>
|
|
98
|
-
:root { --bg:#0f1419; --fg:#e7ecf1; --muted:#9aa7b5; --accent:#3d9cfd; --card:#1a222c; }
|
|
99
|
-
* { box-sizing: border-box; }
|
|
100
|
-
body { margin:0; font-family: ui-sans-serif, system-ui, sans-serif; background:var(--bg); color:var(--fg); }
|
|
101
|
-
header { padding:1.25rem 1.5rem; border-bottom:1px solid #243040; }
|
|
102
|
-
h1 { margin:0; font-size:1.25rem; letter-spacing:0.02em; }
|
|
103
|
-
p { color:var(--muted); margin:0.35rem 0 0; }
|
|
104
|
-
main { padding:1.5rem; display:grid; gap:1rem; max-width:1100px; margin:0 auto; }
|
|
105
|
-
section { background:var(--card); border-radius:10px; padding:1rem 1.1rem; }
|
|
106
|
-
h2 { margin:0 0 0.75rem; font-size:1rem; }
|
|
107
|
-
pre { white-space:pre-wrap; word-break:break-word; font-size:0.85rem; color:#d5dde6; }
|
|
108
|
-
.muted { color:var(--muted); }
|
|
109
|
-
a { color:var(--accent); }
|
|
110
|
-
.notice { border-left:3px solid var(--accent); padding-left:0.75rem; margin-top:0.75rem; }
|
|
111
|
-
</style>
|
|
112
|
-
</head>
|
|
113
|
-
<body>
|
|
114
|
-
<header>
|
|
115
|
-
<h1>AgentDoctor 2.0</h1>
|
|
116
|
-
<p>Local read-only dashboard — Safety + Brain + intelligence. No cloud writes.</p>
|
|
117
|
-
<p class="notice muted">Action Policy Evaluator is evaluate-only. Query ?user= is a localDevIdentityHint only (not authentication). Team local-dev auth is separate and not SSO. OIDC JWT validation is a library path — full browser OAuth redirect is experimental.</p>
|
|
118
|
-
</header>
|
|
119
|
-
<main>
|
|
120
|
-
<nav aria-label="Views" style="display:flex;flex-wrap:wrap;gap:0.75rem;font-size:0.9rem">
|
|
121
|
-
<a href="#home">Home</a>
|
|
122
|
-
<a href="#dna">DNA</a>
|
|
123
|
-
<a href="#graph">Graph</a>
|
|
124
|
-
<a href="#chat">Chat</a>
|
|
125
|
-
<a href="#security">Security</a>
|
|
126
|
-
<a href="#twin">Twin</a>
|
|
127
|
-
<a href="#requirements">Requirements</a>
|
|
128
|
-
<a href="#map">Map</a>
|
|
129
|
-
<a href="#features">Features</a>
|
|
130
|
-
<a href="#deps">Deps</a>
|
|
131
|
-
<a href="#search">Search</a>
|
|
132
|
-
<a href="#whatif">What-if</a>
|
|
133
|
-
<a href="#forensic">Forensic</a>
|
|
134
|
-
<a href="#incident">Incident</a>
|
|
135
|
-
<a href="#infra">Infra</a>
|
|
136
|
-
<a href="#evolution">Evolution</a>
|
|
137
|
-
<a href="#memory">Memory</a>
|
|
138
|
-
<a href="#decisions">Decisions</a>
|
|
139
|
-
<a href="#health">Health</a>
|
|
140
|
-
<a href="#agent">Doctors</a>
|
|
141
|
-
</nav>
|
|
142
|
-
<section id="home" data-route="home">
|
|
143
|
-
<h2>Home</h2>
|
|
144
|
-
<pre id="status" class="muted">Loading…</pre>
|
|
145
|
-
</section>
|
|
146
|
-
<section id="dna" data-route="dna" hidden>
|
|
147
|
-
<h2>Project DNA</h2>
|
|
148
|
-
<pre id="dnaBody" class="muted">Loading…</pre>
|
|
149
|
-
</section>
|
|
150
|
-
<section id="graph" data-route="graph" hidden>
|
|
151
|
-
<h2>Graph / Git / C4</h2>
|
|
152
|
-
<pre id="graphBody" class="muted">Loading…</pre>
|
|
153
|
-
</section>
|
|
154
|
-
<section id="security" data-route="security" hidden>
|
|
155
|
-
<h2>Security doctor</h2>
|
|
156
|
-
<pre id="securityBody" class="muted">Loading…</pre>
|
|
157
|
-
</section>
|
|
158
|
-
<section id="twin" data-route="twin" hidden>
|
|
159
|
-
<h2>Digital twin</h2>
|
|
160
|
-
<pre id="twinBody" class="muted">Loading…</pre>
|
|
161
|
-
</section>
|
|
162
|
-
<section id="requirements" data-route="requirements" hidden>
|
|
163
|
-
<h2>Requirements trace</h2>
|
|
164
|
-
<pre id="requirementsBody" class="muted">Loading…</pre>
|
|
165
|
-
</section>
|
|
166
|
-
<section id="map" data-route="map" hidden>
|
|
167
|
-
<h2>Software map</h2>
|
|
168
|
-
<pre id="mapBody" class="muted">Loading…</pre>
|
|
169
|
-
</section>
|
|
170
|
-
<section id="features" data-route="features" hidden>
|
|
171
|
-
<h2>Feature intelligence</h2>
|
|
172
|
-
<pre id="featuresBody" class="muted">Loading…</pre>
|
|
173
|
-
</section>
|
|
174
|
-
<section id="deps" data-route="deps" hidden>
|
|
175
|
-
<h2>Dependencies</h2>
|
|
176
|
-
<pre id="depsBody" class="muted">Loading…</pre>
|
|
177
|
-
</section>
|
|
178
|
-
<section id="search" data-route="search" hidden>
|
|
179
|
-
<h2>Software search</h2>
|
|
180
|
-
<pre id="searchBody" class="muted">Loading…</pre>
|
|
181
|
-
</section>
|
|
182
|
-
<section id="whatif" data-route="whatif" hidden>
|
|
183
|
-
<h2>What-if</h2>
|
|
184
|
-
<pre id="whatifBody" class="muted">Loading…</pre>
|
|
185
|
-
</section>
|
|
186
|
-
<section id="forensic" data-route="forensic" hidden>
|
|
187
|
-
<h2>Forensic</h2>
|
|
188
|
-
<pre id="forensicBody" class="muted">Loading…</pre>
|
|
189
|
-
</section>
|
|
190
|
-
<section id="incident" data-route="incident" hidden>
|
|
191
|
-
<h2>Incident</h2>
|
|
192
|
-
<pre id="incidentBody" class="muted">Loading…</pre>
|
|
193
|
-
</section>
|
|
194
|
-
<section id="infra" data-route="infra" hidden>
|
|
195
|
-
<h2>Infra</h2>
|
|
196
|
-
<pre id="infraBody" class="muted">Loading…</pre>
|
|
197
|
-
</section>
|
|
198
|
-
<section id="evolution" data-route="evolution" hidden>
|
|
199
|
-
<h2>Evolution</h2>
|
|
200
|
-
<pre id="evolutionBody" class="muted">Loading…</pre>
|
|
201
|
-
</section>
|
|
202
|
-
<section id="memory" data-route="memory" hidden>
|
|
203
|
-
<h2>Institutional memory</h2>
|
|
204
|
-
<pre id="memoryBody" class="muted">Loading…</pre>
|
|
205
|
-
</section>
|
|
206
|
-
<section id="decisions" data-route="decisions" hidden>
|
|
207
|
-
<h2>Decisions</h2>
|
|
208
|
-
<pre id="decisionsBody" class="muted">Loading…</pre>
|
|
209
|
-
</section>
|
|
210
|
-
<section id="health" data-route="health" hidden>
|
|
211
|
-
<h2>Code health</h2>
|
|
212
|
-
<pre id="healthBody" class="muted">Loading…</pre>
|
|
213
|
-
</section>
|
|
214
|
-
<section id="agent" data-route="agent" hidden>
|
|
215
|
-
<h2>Product doctors (API, DB, events)</h2>
|
|
216
|
-
<pre id="agentBody" class="muted">Loading…</pre>
|
|
217
|
-
</section>
|
|
218
|
-
<section id="chat" data-route="chat" hidden>
|
|
219
|
-
<h2>Project Chat</h2>
|
|
220
|
-
<p class="muted">Ask about this repository. Answers use evidence + truth labels. No file writes from this panel.</p>
|
|
221
|
-
<div style="display:grid;grid-template-columns:1.4fr 1fr;gap:1rem">
|
|
222
|
-
<div>
|
|
223
|
-
<textarea id="chatInput" rows="3" style="width:100%;background:#0d1520;color:#e8eef5;border:1px solid #243040;border-radius:8px;padding:0.6rem" placeholder="How does authentication work?"></textarea>
|
|
224
|
-
<button id="chatAsk" style="margin-top:0.5rem;background:var(--accent);color:#041018;border:0;border-radius:6px;padding:0.45rem 0.9rem;font-weight:600;cursor:pointer">Ask</button>
|
|
225
|
-
<pre id="chatBody" class="muted" style="margin-top:0.75rem">Ask a question…</pre>
|
|
226
|
-
</div>
|
|
227
|
-
<div>
|
|
228
|
-
<h3 style="margin:0 0 0.5rem;font-size:0.9rem">Evidence</h3>
|
|
229
|
-
<pre id="chatEvidence" class="muted">—</pre>
|
|
230
|
-
</div>
|
|
231
|
-
</div>
|
|
232
|
-
</section>
|
|
233
|
-
</main>
|
|
234
|
-
<script>
|
|
235
|
-
const routes = ['home','dna','graph','chat','security','twin','requirements','map','features','deps','search','whatif','forensic','incident','infra','evolution','memory','decisions','health','agent'];
|
|
236
|
-
function showRoute(name) {
|
|
237
|
-
const r = routes.includes(name) ? name : 'home';
|
|
238
|
-
for (const el of document.querySelectorAll('[data-route]')) {
|
|
239
|
-
el.hidden = el.getAttribute('data-route') !== r;
|
|
240
|
-
}
|
|
241
|
-
location.hash = r;
|
|
242
|
-
}
|
|
243
|
-
window.addEventListener('hashchange', () => showRoute((location.hash || '#home').slice(1)));
|
|
244
|
-
showRoute((location.hash || '#home').slice(1));
|
|
245
|
-
|
|
246
|
-
async function loadSection(route) {
|
|
247
|
-
const map = {
|
|
248
|
-
dna: [['dnaBody','/api/dna']],
|
|
249
|
-
graph: [['graphBody','/api/v2/graph']],
|
|
250
|
-
security: [['securityBody','/api/security']],
|
|
251
|
-
twin: [['twinBody','/api/twin']],
|
|
252
|
-
requirements: [['requirementsBody','/api/requirements']],
|
|
253
|
-
map: [['mapBody','/api/map']],
|
|
254
|
-
features: [['featuresBody','/api/features']],
|
|
255
|
-
deps: [['depsBody','/api/deps']],
|
|
256
|
-
search: [['searchBody','/api/search?q=main']],
|
|
257
|
-
whatif: [['whatifBody','/api/what-if?target=src']],
|
|
258
|
-
forensic: [['forensicBody','/api/forensic']],
|
|
259
|
-
incident: [['incidentBody','/api/incident']],
|
|
260
|
-
infra: [['infraBody','/api/infra']],
|
|
261
|
-
evolution: [['evolutionBody','/api/evolution']],
|
|
262
|
-
memory: [['memoryBody','/api/memory?q=change']],
|
|
263
|
-
decisions: [['decisionsBody','/api/decisions']],
|
|
264
|
-
health: [['healthBody','/api/health']],
|
|
265
|
-
agent: [['agentBody','/api/api-doctor']],
|
|
266
|
-
};
|
|
267
|
-
if (route === 'home') {
|
|
268
|
-
const [status, scan, brain, platform] = await Promise.all([
|
|
269
|
-
fetch('/api/status').then(r => r.json()),
|
|
270
|
-
fetch('/api/scan').then(r => r.json()),
|
|
271
|
-
fetch('/api/brain').then(r => r.json()),
|
|
272
|
-
fetch('/api/platform').then(r => r.json()),
|
|
273
|
-
]);
|
|
274
|
-
document.getElementById('status').textContent = JSON.stringify({ status, scan, brain, platform }, null, 2);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
if (route === 'agent') {
|
|
278
|
-
const [api, db, events] = await Promise.all([
|
|
279
|
-
fetch('/api/api-doctor').then(r => r.json()),
|
|
280
|
-
fetch('/api/database').then(r => r.json()),
|
|
281
|
-
fetch('/api/events').then(r => r.json()),
|
|
282
|
-
]);
|
|
283
|
-
document.getElementById('agentBody').textContent = JSON.stringify({ api, db, events }, null, 2);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
const loaders = map[route] || [];
|
|
287
|
-
for (const [elId, url] of loaders) {
|
|
288
|
-
const data = await fetch(url).then(r => r.json());
|
|
289
|
-
document.getElementById(elId).textContent = JSON.stringify(data, null, 2);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
async function load() {
|
|
293
|
-
const route = (location.hash || '#home').slice(1) || 'home';
|
|
294
|
-
await loadSection(route);
|
|
295
|
-
}
|
|
296
|
-
window.addEventListener('hashchange', () => load().catch(err => console.error(err)));
|
|
297
|
-
load().catch(err => {
|
|
298
|
-
document.getElementById('status').textContent = String(err);
|
|
299
|
-
});
|
|
300
|
-
document.getElementById('chatAsk').addEventListener('click', async () => {
|
|
301
|
-
const question = document.getElementById('chatInput').value.trim();
|
|
302
|
-
if (!question) return;
|
|
303
|
-
document.getElementById('chatBody').textContent = 'Thinking…';
|
|
304
|
-
try {
|
|
305
|
-
const res = await fetch('/api/chat', {
|
|
306
|
-
method: 'POST',
|
|
307
|
-
headers: { 'Content-Type': 'application/json' },
|
|
308
|
-
body: JSON.stringify({ question }),
|
|
309
|
-
});
|
|
310
|
-
const data = await res.json();
|
|
311
|
-
document.getElementById('chatBody').textContent = data.message || JSON.stringify(data, null, 2);
|
|
312
|
-
document.getElementById('chatEvidence').textContent = JSON.stringify({
|
|
313
|
-
truthClaims: data.truthClaims || [],
|
|
314
|
-
citations: data.citations || [],
|
|
315
|
-
limitations: data.limitations || [],
|
|
316
|
-
status: data.status,
|
|
317
|
-
}, null, 2);
|
|
318
|
-
} catch (err) {
|
|
319
|
-
document.getElementById('chatBody').textContent = String(err);
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
</script>
|
|
323
|
-
</body>
|
|
324
|
-
</html>`;
|
|
325
|
-
}
|
|
326
94
|
function sendJson(res, status, body) {
|
|
327
95
|
const payload = JSON.stringify(body, null, 2);
|
|
328
96
|
res.writeHead(status, {
|
|
@@ -611,13 +379,21 @@ export async function startDashboardServer(options) {
|
|
|
611
379
|
sendJson(res, 200, await searchSymbolsAndConcepts(root, q));
|
|
612
380
|
return;
|
|
613
381
|
}
|
|
614
|
-
if (url.pathname === "/api/what-if") {
|
|
382
|
+
if (url.pathname === "/api/what-if" || url.pathname === "/api/whatif") {
|
|
615
383
|
const target = url.searchParams.get("target") ?? "";
|
|
616
384
|
if (!target.trim()) {
|
|
617
385
|
sendJson(res, 400, { error: "target query param required" });
|
|
618
386
|
return;
|
|
619
387
|
}
|
|
620
|
-
|
|
388
|
+
try {
|
|
389
|
+
sendJson(res, 200, await analyzeWhatIf(root, target));
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
const ownershipDenied = error instanceof ProjectOwnershipError || error instanceof PathEscapeError;
|
|
393
|
+
sendJson(res, ownershipDenied ? 400 : 500, {
|
|
394
|
+
error: ownershipDenied ? "path outside project ownership" : "what-if analysis failed",
|
|
395
|
+
});
|
|
396
|
+
}
|
|
621
397
|
return;
|
|
622
398
|
}
|
|
623
399
|
if (url.pathname === "/api/forensic") {
|
|
@@ -664,7 +440,7 @@ export async function startDashboardServer(options) {
|
|
|
664
440
|
sendJson(res, 404, { error: "not found" });
|
|
665
441
|
}
|
|
666
442
|
catch (error) {
|
|
667
|
-
sendJson(res, 500, { error:
|
|
443
|
+
sendJson(res, 500, { error: clientErrorMessage(error) });
|
|
668
444
|
}
|
|
669
445
|
});
|
|
670
446
|
await new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Dashboard client application (browser). */
|
|
2
|
+
export const DASHBOARD_CLIENT_JS = 'const EMBEDDED_VERSION = "%%PACKAGE_VERSION%%";\nconst state = { route: "home", collapsed: false, project: null, status: null, dna: null, cache: {}, loadGen: 0 };\n\nconst NAV = [\n { group: "OVERVIEW", items: [\n { id: "home", label: "Home", ico: "\u2302" },\n { id: "chat", label: "Project Chat", ico: "\u2726" },\n ]},\n { group: "UNDERSTAND", items: [\n { id: "dna", label: "DNA", ico: "\u25ce" },\n { id: "map", label: "Map", ico: "\u25a6" },\n { id: "graph", label: "Graph", ico: "\u26ad" },\n { id: "deps", label: "Dependencies", ico: "\u29c9" },\n { id: "search", label: "Search", ico: "\u2315" },\n { id: "features", label: "Features", ico: "\u2b21" },\n ]},\n { group: "ENGINEERING", items: [\n { id: "requirements", label: "Requirements", ico: "\u25a3" },\n { id: "health", label: "Health", ico: "\u2713" },\n { id: "evidence", label: "Evidence", ico: "\u2261" },\n { id: "decisions", label: "Decisions", ico: "\u2691" },\n { id: "evolution", label: "Evolution", ico: "\u21ba" },\n ]},\n { group: "SECURITY", items: [\n { id: "security", label: "Security", ico: "\u26e8" },\n { id: "forensic", label: "Forensics", ico: "\u25c8" },\n { id: "incident", label: "Incidents", ico: "\u26a0" },\n { id: "doctors", label: "Doctors", ico: "+" },\n ]},\n { group: "INTELLIGENCE", items: [\n { id: "whatif", label: "What-if", ico: "?" },\n { id: "memory", label: "Memory", ico: "\u25cc" },\n { id: "twin", label: "Digital Twin", ico: "\u25c9" },\n ]},\n { group: "OPERATIONS", items: [\n { id: "infra", label: "Infrastructure", ico: "\u2637" },\n ]},\n { group: "LEARNING", items: [\n { id: "learn", label: "Learn", ico: "\u00b6" },\n ]},\n { group: "SYSTEM", items: [\n { id: "system", label: "System", ico: "\u2699" },\n ]},\n];\n\nconst PAGES = Object.fromEntries(NAV.flatMap(g => g.items.map(i => [i.id, { ...i, group: g.group }])));\n\nfunction esc(s){return String(s??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");}\nfunction shortPath(p){if(!p||typeof p!=="string")return"Unknown";const parts=p.replace(/\\\\/g,"/").split("/").filter(Boolean);return parts.length<=2?p:"~/"+parts.slice(-2).join("/");}\nfunction joinList(arr,limit){const a=Array.isArray(arr)?arr.filter(Boolean):[];if(!a.length)return"Not detected";const shown=a.slice(0,limit||6);const extra=a.length-shown.length;return shown.join(", ")+(extra>0?" +"+extra:"");}\nfunction stateClass(st){const s=String(st||"").toLowerCase();if(s==="healthy"||s==="ok")return"ok";if(s==="attention")return"attention";if(s==="warning"||s==="critical"||s==="blocked")return"warning";return"unknown";}\nfunction badge(text, cls){return `<span class="badge ${cls||""}">${esc(text)}</span>`;}\nfunction truthBadge(t){return t?badge(t,"truth"):"";}\nfunction pageHeader(title, desc, metaHtml){return `<header class="page-header"><h1>${esc(title)}</h1>${desc?`<p class="page-desc">${esc(desc)}</p>`:""}${metaHtml?`<div class="meta-row">${metaHtml}</div>`:""}</header>`;}\nfunction emptyState(title, why, next){return `<div class="empty" role="status"><strong>${esc(title)}</strong><p>${esc(why)}</p>${next?`<p>${esc(next)}</p>`:""}</div>`;}\nfunction errorState(msg, next){return `<div class="error" role="alert"><strong>Analysis could not finish</strong><p>${esc(msg)}</p><p>${esc(next||"Try again, open a lighter page (DNA or System), or expand Technical Details if available.")}</p></div>`;}\nfunction loadingState(label){return `<div class="loading" aria-busy="true"><div class="skeleton" style="width:40%;margin-bottom:.5rem"></div><div class="skeleton" style="width:70%;margin-bottom:.5rem"></div><div class="skeleton" style="width:55%"></div><p style="margin-top:.75rem">${esc(label||"Loading\u2026")}</p></div>`;}\nfunction techBlock(obj, id){const json=JSON.stringify(obj,null,2);return `<div class="tech"><details><summary>Technical Details</summary><div style="margin:.5rem 0"><button type="button" class="btn" data-copy="${esc(id||"tech")}">Copy JSON</button></div><pre class="pre" id="${esc(id||"techJson")}">${esc(json)}</pre></details></div>`;}\nfunction card(title, state, truth, detail, href, action){return `<article class="card"><h3>${esc(title)}</h3><div>${badge(state,stateClass(state))}${truthBadge(truth)}</div><p>${esc(detail)}</p>${href?`<div class="actions"><a href="${esc(href)}">${esc(action||"Open")}</a></div>`:""}</article>`;}\n\nasync function fetchJson(url, opts){const ms=(opts&&opts.timeoutMs)||25000;const ctrl=typeof AbortController!=="undefined"?new AbortController():null;const timer=ctrl?setTimeout(function(){ctrl.abort();},ms):null;try{const res=await fetch(url,{signal:ctrl?ctrl.signal:undefined});if(!res.ok)throw new Error(url+" \u2192 HTTP "+res.status);return await res.json();} finally { if(timer) clearTimeout(timer); }}\nasync function cached(key, url, opts){if(state.cache[key])return state.cache[key];const data=await fetchJson(url,opts||{timeoutMs:20000});state.cache[key]=data;return data;}\n\nfunction renderSidebar(){\n const el=document.getElementById("sidebarNav");\n el.innerHTML=NAV.map(g=>`<div class="nav-group"><div class="nav-group-label">${esc(g.group)}</div>${g.items.map(i=>`<a class="nav-link${state.route===i.id?" active":""}" href="#${esc(i.id)}" data-route-link="${esc(i.id)}" title="${esc(i.label)}"><span class="nav-ico" aria-hidden="true">${i.ico}</span><span class="nav-text">${esc(i.label)}</span></a>`).join("")}</div>`).join("");\n}\nfunction updateProjectChip(){\n const dna=state.dna, st=state.status;\n const name=(dna&&dna.name)||(st&&st.root&&shortPath(st.root))||"Project";\n const stack=dna?joinList([...(dna.languages||[]).slice(0,2),...(dna.frameworks||[]).slice(0,2)],4):"Not analyzed";\n const ver=((st&&st.ops&&st.ops.version)||EMBEDDED_VERSION);\n document.getElementById("headerVersion").textContent="v"+ver;\n document.getElementById("projectName").textContent=name;\n document.getElementById("projectMeta").textContent=stack+(dna&&dna.git&&dna.git.present?" \u00b7 Git":"");\n document.title="AgentDoctor "+ver+" \u00b7 "+name;\n}\n\nfunction setRoute(route, opts){\n const r=PAGES[route]?route:"home";\n const fromHash=opts&&opts.fromHash;\n if(!fromHash){\n if(((location.hash||"#home").slice(1)||"home")!==r){ location.hash=r; return; }\n }\n state.route=r;\n renderSidebar();\n document.querySelectorAll("[data-route-link]").forEach(a=>a.classList.toggle("active",a.getAttribute("data-route-link")===r));\n loadRoute(r);\n}\n\nasync function ensureContext(){\n if(!state.status){try{state.status=await fetchJson("/api/status",{timeoutMs:8000});}catch{}}\n if(!state.dna){try{state.dna=await fetchJson("/api/dna",{timeoutMs:20000});}catch{}}\n updateProjectChip();\n}\n\n/* ---------- Page renderers ---------- */\nfunction renderHomeView(model){\n return `<div id="homeOverview">`+pageHeader("Project Command Center","See project health, findings, and what to investigate next.", \n badge("AgentDoctor v"+model.version)+badge(model.projectType)+badge(joinList(model.langs,4))+badge(model.gitPresent?"Git repository":"Git not detected","truth"))+\n `<p class="path-sec">Project root: ${esc(shortPath(model.root))}</p>`+\n `<section class="section"><h2>Project status</h2><p class="page-desc">${esc(model.projectDetail)}</p><div class="meta-row">${badge(model.projectState,stateClass(model.projectState))}${truthBadge(model.dnaTruth)}</div></section>`+\n `<section class="section"><h2>Key areas</h2><div class="grid">${model.cards.map(c=>card(c.title,c.state,c.truth,c.detail,c.href,c.action)).join("")}</div></section>`+\n (model.findingsHtml||"")+\n `<section class="section"><h2>Recommended next action</h2><p><strong>${esc(model.nextTitle)}</strong> \u2014 ${esc(model.nextDetail)}</p><p style="margin-top:.45rem"><a href="${esc(model.nextHref)}">Continue \u2192</a></p></section>`+\n techBlock(model.raw,"homeTech")+`</div>`;\n}\n\nfunction deriveHome(status,dna,scan,brain,security,deps,health){\n const version=(status&&status.ops&&status.ops.version)||EMBEDDED_VERSION;\n const name=(dna&&dna.name)||"Unknown project";\n const projectType=(dna&&dna.projectType)||"Unknown";\n const langs=(dna&&dna.languages)||[];\n const findings=(scan&&scan.findings)||[];\n const findingCount=typeof(scan&&scan.findingCount)==="number"?scan.findingCount:findings.length;\n const critical=findings.filter(f=>f.severity==="critical").length;\n const warnings=findings.filter(f=>f.severity==="warning"||f.severity==="warn").length;\n const secretFindings=(security&&security.secretScan&&security.secretScan.findings)||[];\n const danger=(security&&security.dangerousPatterns)||[];\n const testFw=(dna&&dna.testFrameworks)||[];\n const testFiles=dna&&dna.counts?dna.counts.testFiles:null;\n const lockParsed=(deps&&deps.lockfilesParsed)||[];\n const directDeps=(deps&&deps.directDependencies)||[];\n const aiTools=(dna&&dna.aiCodingTools)||[];\n const hasBrain=!!(brain&&brain.hasSnapshot);\n const indicators=(health&&health.indicators)||[];\n let projectState="Unknown", projectDetail="Project state could not be fully determined from available evidence.";\n if(critical>0){projectState="Warning";projectDetail=critical+" critical finding(s) need review.";}\n else if(secretFindings.length>0){projectState="Attention";projectDetail=secretFindings.length+" potential secret finding(s) detected.";}\n else if(findingCount>0||danger.length>0){projectState="Attention";projectDetail=findingCount+" scan finding(s); "+danger.length+" security-pattern finding(s).";}\n else if(dna){projectState="Healthy";projectDetail="No critical findings in the current scan sample. DNA is available.";}\n const cards=[];\n cards.push({title:"Project state",state:projectState,truth:(dna&&dna.truth)||"UNKNOWN",detail:projectDetail,href:"#dna",action:"Open DNA"});\n cards.push(!testFw.length&&!(testFiles>0)\n ?{title:"Tests",state:"Not analyzed",truth:"UNKNOWN",detail:"No test framework or test files detected in DNA.",href:"#health",action:"Open health"}\n :{title:"Tests",state:"Healthy",truth:(dna&&dna.truth)||"INFERRED",detail:"Frameworks: "+joinList(testFw,4)+". Test files: "+(testFiles??"Unknown")+".",href:"#health",action:"Open health"});\n if(secretFindings.length) cards.push({title:"Security",state:"Warning",truth:"INFERRED",detail:secretFindings.length+" secret finding(s); "+danger.length+" security-pattern finding(s).",href:"#security",action:"Review"});\n else if(danger.length) cards.push({title:"Security",state:"Attention",truth:"INFERRED",detail:danger.length+" security-pattern finding(s). Static analysis only.",href:"#security",action:"Review"});\n else if(security) cards.push({title:"Security",state:"Healthy",truth:"INFERRED",detail:"No secrets detected in this scan. Not a compliance certification.",href:"#security",action:"Open"});\n else cards.push({title:"Security",state:"Unknown",truth:"UNKNOWN",detail:"Security data unavailable.",href:"#security",action:"Retry"});\n cards.push({title:"Architecture",state:dna?"Healthy":"Not analyzed",truth:(dna&&dna.truth)||"UNKNOWN",detail:dna?("Type "+projectType+". FE: "+joinList(dna.frontend||[],3)+". BE: "+joinList(dna.backend||[],3)+"."):"Not loaded.",href:"#map",action:"Explore map"});\n cards.push(lockParsed.length\n ?{title:"Dependencies",state:"Healthy",truth:"VERIFIED",detail:directDeps.length+" direct entries; "+lockParsed.length+" lockfile(s) parsed.",href:"#deps",action:"Open deps"}\n :{title:"Dependencies",state:"Unknown",truth:"UNKNOWN",detail:"No lockfiles parsed.",href:"#deps",action:"Inspect"});\n cards.push({title:"Changes / findings",state:critical>0?"Warning":findingCount>0?"Attention":scan?"Healthy":"Not analyzed",truth:"INFERRED",detail:scan?(findingCount+" finding(s); critical "+critical+", warning "+warnings+"."):"Scan unavailable.",href:"#evidence",action:"Open evidence"});\n cards.push(aiTools.length\n ?{title:"AI tooling",state:"Attention",truth:"VERIFIED",detail:"Detected: "+joinList(aiTools,6)+". Writes still require human approval.",href:"#chat",action:"Chat"}\n :{title:"AI tooling",state:"Healthy",truth:"INFERRED",detail:"No AI coding tools detected. AgentDoctor works without them.",href:"#chat",action:"Chat"});\n cards.push({title:"Evidence / Brain",state:hasBrain?"Healthy":"Not analyzed",truth:hasBrain?"VERIFIED":"UNKNOWN",detail:hasBrain?("Snapshot "+(brain.latestSnapshotId||"present")+" \u00b7 count "+(brain.snapshotCount??"?")):"No brain snapshot yet.",href:"#dna",action:"DNA"});\n if(indicators.length){const tip=indicators.slice(0,2).map(i=>i.label+": "+i.value).join("; ");cards.push({title:"Code health",state:"Attention",truth:"INFERRED",detail:tip+". Descriptive indicators only.",href:"#health",action:"Open"});}\n let nextTitle="Explore Project DNA", nextDetail="Confirm languages and structure from DNA evidence.", nextHref="#dna";\n if(critical>0||secretFindings.length){nextTitle="Review security findings";nextDetail="Static evidence indicates issues to review.";nextHref="#security";}\n else if(!testFw.length&&!(testFiles>0)){nextTitle="Inspect testing setup";nextDetail="No test framework detected in DNA.";nextHref="#health";}\n else if(!hasBrain){nextTitle="Build Project Brain";nextDetail="Compile/rebuild brain for richer chat.";nextHref="#chat";}\n else if(findingCount>0){nextTitle="Review findings";nextDetail="Scan returned findings.";nextHref="#security";}\n else if(dna){nextTitle="Explore architecture map";nextDetail="Use Map/Graph, then Chat.";nextHref="#map";}\n const topFindings=findings.slice(0,5);\n const findingsHtml=topFindings.length?`<section class="section"><h2>Important findings</h2><div class="table-wrap"><table class="data"><thead><tr><th>Severity</th><th>Title</th><th>Rule</th><th>Path</th></tr></thead><tbody>${topFindings.map(f=>`<tr><td>${badge(f.severity||"info",stateClass(f.severity==="critical"?"critical":f.severity))}</td><td>${esc(f.title||f.id)}</td><td class="mono">${esc(f.ruleId||"\u2014")}</td><td class="mono">${esc(f.path||"\u2014")}</td></tr>`).join("")}</tbody></table></div><p class="page-desc">Showing up to 50 findings from the latest scan.</p></section>`:"";\n return {version,name,projectType,langs,gitPresent:!!(dna&&dna.git&&dna.git.present),root:(dna&&dna.root)||(status&&status.root)||"",projectState,projectDetail,dnaTruth:(dna&&dna.truth)||"UNKNOWN",cards,nextTitle,nextDetail,nextHref,findingsHtml,raw:{status,dna,scan,brain,security,deps,health}};\n}\n\nfunction renderDna(data){\n if(!data)return emptyState("DNA not available","Project DNA could not be loaded.","Retry or run discovery from the CLI.");\n const chips=(arr)=>`<div class="chips">${(arr||[]).slice(0,24).map(x=>badge(x)).join("")||badge("Not detected","unknown")}</div>`;\n return pageHeader("Project DNA","Understand the technologies and structure detected in this project.",truthBadge(data.truth)+badge(data.projectType||"Unknown"))+\n `<section class="section"><h2>Identity</h2><p><strong>${esc(data.name||"Unknown")}</strong></p><p class="path-sec">${esc(shortPath(data.root))}</p><div class="meta-row">${badge(data.git&&data.git.present?"Git present":"Git not detected")}${truthBadge(data.git&&data.git.truth)}${badge(data.monorepo&&data.monorepo.isMonorepo?("Monorepo \u00b7 "+(data.monorepo.tool||"yes")):"Single package")}</div></section>`+\n `<section class="section"><h2>Technology</h2><h3>Languages</h3>${chips(data.languages)}<h3>Frameworks</h3>${chips(data.frameworks)}<h3>Frontend</h3>${chips(data.frontend)}<h3>Backend</h3>${chips(data.backend)}<h3>Databases</h3>${chips(data.database)}<h3>Package managers</h3>${chips(data.packageManagers)}<h3>Test frameworks</h3>${chips(data.testFrameworks)}<h3>AI coding tools</h3>${chips(data.aiCodingTools)}</section>`+\n `<section class="section"><h2>Counts</h2><div class="grid">${[["Files",data.counts&&data.counts.files],["Source",data.counts&&data.counts.sourceFiles],["Tests",data.counts&&data.counts.testFiles],["Config",data.counts&&data.counts.configFiles]].map(([k,v])=>`<article class="card"><h3>${esc(k)}</h3><p>${esc(v??"Unknown")}</p></article>`).join("")}</div></section>`+\n ((data.limitations&&data.limitations.length)?`<section class="section"><h2>Limitations</h2><ul>${data.limitations.map(l=>`<li>${esc(l)}</li>`).join("")}</ul></section>`:"")+\n techBlock(data,"dnaTech");\n}\n\nfunction renderGraph(data){\n const g=data&&data.graph?data.graph:data;\n const nodes=(g&&(g.sampleNodes||g.nodes))||[];\n const edges=(g&&(g.sampleEdges||g.edges))||[];\n const totalNodes=g&&typeof g.nodeCount==="number"?g.nodeCount:nodes.length;\n const totalEdges=g&&typeof g.edgeCount==="number"?g.edgeCount:edges.length;\n if(!nodes.length&&!edges.length)return pageHeader("Graph","Browse a sample of code relationships detected in this project.", "")+emptyState("No graph sample available","No graph nodes were returned for this project.","Try Map or rebuild graph intelligence from the CLI.")+techBlock(data,"graphTech");\n const byType={};nodes.forEach(n=>{const t=n.kind||n.type||"node";byType[t]=(byType[t]||0)+1;});\n const legend=badge("nodes: "+totalNodes)+badge("edges: "+totalEdges)+badge("builder: "+((g&&g.builder)||"unknown"))+Object.entries(byType).map(([k,v])=>badge("sample "+k+": "+v)).join("");\n // Simple SVG layout by type columns\n const types=Object.keys(byType).slice(0,8);\n const W=900,H=300;let svg=`<svg class="graph-canvas" viewBox="0 0 ${W} ${H}" role="img" aria-label="Graph overview">`;\n types.forEach((t,ti)=>{\n const col=nodes.filter(n=>(n.kind||n.type||"node")===t).slice(0,12);\n col.forEach((n,ni)=>{\n const x=60+ti*100; const y=40+ni*22;\n svg+=`<circle cx="${x}" cy="${y}" r="5" fill="#4aa3ff" opacity=".85"/><text x="${x+10}" y="${y+4}" fill="#93a0b0" font-size="9">${esc((n.label||n.id||"").toString().slice(0,18))}</text>`;\n });\n });\n svg+="</svg>";\n const rows=nodes.slice(0,40).map(n=>`<tr><td>${esc(n.kind||n.type||"\u2014")}</td><td class="mono">${esc(n.label||n.id)}</td><td class="mono">${esc(n.path||"\u2014")}</td></tr>`).join("");\n return pageHeader("Graph","Browse a sample of code relationships. This is not a full interactive graph engine.", badge(nodes.length+" nodes")+badge(edges.length+" edges"))+\n `<section class="section"><h2>Overview</h2><div class="graph-legend">${legend}</div>${svg}<p class="scope-note">Sample canvas for readability \u2014 not a full zoomable graph engine. Node list below.</p></section>`+\n `<section class="section"><h2>Nodes (sample)</h2><div class="table-wrap"><table class="data"><thead><tr><th>Type</th><th>Label</th><th>Path</th></tr></thead><tbody>${rows||`<tr><td colspan="3">None</td></tr>`}</tbody></table></div></section>`+\n techBlock(data,"graphTech");\n}\n\nfunction renderMap(data){\n if(!data)return emptyState("Map unavailable","The architecture map could not be loaded for this project.","Retry or open DNA for technology context.");\n const tree=data.tree||data;\n function walk(node, depth){\n if(!node)return"";\n if(typeof node==="string")return `<li class="mono">${esc(node)}</li>`;\n const name=node.name||node.id||node.path||"node";\n const kids=node.children||node.nodes||[];\n return `<li><strong>${esc(name)}</strong>${node.path?` <span class="path-sec">${esc(node.path)}</span>`:""}${kids.length?`<ul>${kids.slice(0,40).map(c=>walk(c,depth+1)).join("")}</ul>`:""}</li>`;\n }\n let listHtml="";\n if(Array.isArray(tree)) listHtml=`<ul>${tree.slice(0,50).map(t=>walk(t,0)).join("")}</ul>`;\n else if(tree&&typeof tree==="object") listHtml=`<ul>${walk(tree,0)}</ul>`;\n else listHtml=emptyState("No map structure","The map response did not include a navigable project tree.","Open DNA or Technical Details for available signals.");\n return pageHeader("Map","Explore how the project is structured from detected directories and packages.","")+\n `<section class="section"><h2>Structure</h2>${listHtml}</section>`+techBlock(data,"mapTech");\n}\n\nfunction renderSecurity(data){\n if(!data)return emptyState("Security analysis unavailable","No security analysis result is available yet.","Retry, or inspect System for connectivity and diagnostics.");\n const secrets=(data.secretScan&&data.secretScan.findings)||[];\n const danger=data.dangerousPatterns||[];\n const auth=data.authMarkers||[];\n return pageHeader("Security","Review security findings, evidence, and recommended actions.", badge("Static analysis")+badge(data.staticAnalysisLabel||"local"))+\n `<section class="section"><h2>Overview</h2><div class="grid">\n ${card("Secrets",secrets.length?"Warning":"Healthy","INFERRED",secrets.length?secrets.length+" finding(s). Values are redacted.":"No secrets detected.","#security","")}\n ${card("Security patterns",danger.length?"Attention":"Healthy","INFERRED",danger.length?danger.length+" finding(s).":"None detected in this scan.","#security","")}\n ${card("Auth markers",auth.length?"Healthy":"Unknown","INFERRED",auth.length?auth.length+" auth signal(s).":"None detected.","#security","")}\n </div></section>`+\n `<section class="section"><h2>Secret findings</h2>${secrets.length?`<div class="table-wrap"><table class="data"><thead><tr><th>Rule</th><th>Path</th><th>Note</th></tr></thead><tbody>${secrets.slice(0,30).map(f=>`<tr><td class="mono">${esc(f.ruleId||f.id||"\u2014")}</td><td class="mono">${esc(f.path||f.file||"\u2014")}</td><td>${esc(f.message||f.title||"Potential secret (values redacted)")}</td></tr>`).join("")}</tbody></table></div>`:emptyState("No secrets detected","No secret findings were reported in this scan.","Continue monitoring as the project changes.")}</section>`+\n `<section class="section"><h2>Dangerous patterns</h2>${danger.length?`<div class="table-wrap"><table class="data"><thead><tr><th>Title</th><th>Truth</th><th>Evidence</th></tr></thead><tbody>${danger.slice(0,40).map(f=>`<tr><td>${esc(f.title||f.id)}</td><td>${truthBadge(f.truth)}</td><td class="mono">${esc(((f.evidence||[])[0]&&((f.evidence[0].path||"")+(f.evidence[0].line?":"+f.evidence[0].line:"")))||"\u2014")}</td></tr>`).join("")}</tbody></table></div>`:emptyState("No security-pattern findings","No dangerous code patterns matched in the sampled files.","Continue reviewing as the project changes.")}</section>`+\n ((data.limitations&&data.limitations.length)?`<section class="section"><h2>Limitations</h2><ul>${data.limitations.map(l=>`<li>${esc(l)}</li>`).join("")}</ul></section>`:"")+\n techBlock({...data,secretScan:{...(data.secretScan||{}),findings:(secrets||[]).map(f=>({...f,value:undefined,match:undefined}))}},"secTech");\n}\n\nfunction renderDeps(data){\n if(!data)return emptyState("Dependencies unavailable","Dependency analysis did not return data for this project.","Retry or confirm package manifests exist.");\n const direct=(data.directDependencies||[]).slice(0,100);\n const locks=data.lockfilesParsed||data.lockfilesPresent||[];\n return pageHeader("Dependencies","Explore packages, lockfiles, and dependency signals.", badge((data.directDependencies||[]).length+" direct")+badge((locks.length||0)+" lockfiles"))+\n `<section class="section"><h2>Overview</h2><div class="grid">\n ${card("Lockfiles",locks.length?"Healthy":"Unknown",locks.length?"VERIFIED":"UNKNOWN",locks.length?joinList(locks.map(l=>String(l).split("/").pop()),5):"None detected","#deps","")}\n ${card("Duplicates",(data.duplicateNames||[]).length?"Attention":"Healthy","INFERRED",(data.duplicateNames||[]).length+" duplicate name group(s).","#deps","")}\n ${card("Monorepo",data.monorepo?"Healthy":"Unknown","INFERRED",data.monorepo?String(data.monorepo):"Not indicated","#deps","")}\n </div></section>`+\n `<section class="section"><h2>Direct dependencies (first 100)</h2>\n <input id="depsFilter" class="btn filter-input" placeholder="Filter by name\u2026" aria-label="Filter dependencies" />\n <div class="table-wrap"><table class="data" id="depsTable"><thead><tr><th>Name</th><th>Version</th><th>Manager</th><th>Path</th></tr></thead><tbody>\n ${direct.map(d=>`<tr data-name="${esc((d.name||"").toLowerCase())}"><td class="mono">${esc(d.name)}</td><td class="mono">${esc(d.version||d.range||"\u2014")}</td><td>${esc(d.manager||d.ecosystem||"\u2014")}</td><td class="mono">${esc(d.path||d.manifest||"\u2014")}</td></tr>`).join("")||`<tr><td colspan="4">None</td></tr>`}\n </tbody></table></div>\n <p class="page-desc">Showing a bounded sample for performance. Full details remain under Technical Details.</p>\n </section>`+techBlock(data,"depsTech");\n}\n\nfunction renderHealth(data,dna){\n const indicators=(data&&data.indicators)||[];\n const testFw=(dna&&dna.testFrameworks)||[];\n const testFiles=dna&&dna.counts?dna.counts.testFiles:null;\n return pageHeader("Health","Review testing signals and descriptive engineering indicators.","")+\n `<section class="section"><h2>Testing</h2>${testFw.length||(testFiles>0)?`<div class="meta-row">${testFw.map(t=>badge(t,"ok")).join("")}${badge("Test files: "+(testFiles??"Unknown"))}</div>`:emptyState("No test framework detected","DNA did not identify a configured test runner.","Inspect package scripts or add a recognizable test setup.")}</section>`+\n `<section class="section"><h2>Indicators</h2>${indicators.length?`<div class="grid">${indicators.map(i=>`<article class="card"><h3>${esc(i.label||i.id)}</h3><div>${truthBadge(i.truth)}</div><p>${esc(i.value)}</p></article>`).join("")}</div>`:emptyState("No health indicators","No descriptive health indicators are available yet.","Open DNA to review detected test frameworks and structure.")}</section>`+\n ((data&&data.limitations&&data.limitations.length)?`<section class="section"><h2>Limitations</h2><ul>${data.limitations.map(l=>`<li>${esc(l)}</li>`).join("")}</ul></section>`:"")+\n techBlock(data,"healthTech");\n}\n\nfunction renderRequirements(data){\n const reqs=(data&&(data.requirements||data.items))||[];\n return pageHeader("Requirements","Trace requirements to local evidence. Live trackers remain EXTERNAL.","")+\n (reqs.length?`<section class="section"><div class="table-wrap"><table class="data"><thead><tr><th>Requirement</th><th>Status</th><th>Truth</th><th>Evidence</th></tr></thead><tbody>${reqs.slice(0,80).map(r=>`<tr><td>${esc(r.title||r.id||r.text||"\u2014")}</td><td>${esc(r.status||"\u2014")}</td><td>${truthBadge(r.truth)}</td><td class="mono">${esc((r.evidence&&r.evidence[0]&&r.evidence[0].path)||r.path||"\u2014")}</td></tr>`).join("")}</tbody></table></div></section>`:emptyState("No requirements traced","No local requirement docs/adapters produced entries.","Add markdown requirements or open Technical Details."))+\n techBlock(data,"reqTech");\n}\n\nfunction renderListPage(title,desc,items,mapRow,emptyTitle,emptyWhy){\n return pageHeader(title,desc,"")+(items&&items.length?`<section class="section"><div class="table-wrap"><table class="data"><thead><tr>${mapRow.headers.map(h=>`<th>${esc(h)}</th>`).join("")}</tr></thead><tbody>${items.slice(0,mapRow.limit||80).map(mapRow.row).join("")}</tbody></table></div></section>`:emptyState(emptyTitle,emptyWhy))+techBlock(mapRow.raw,"listTech");\n}\n\nfunction renderFeatures(data, opts){\n const deferred=opts&&opts.deferred;\n if(deferred){\n return pageHeader("Features","Discover feature signals detected in this project.","")+\n emptyState("Feature analysis is available on demand","It performs a deeper project scan and can take longer on large repositories.","It is not started automatically.")+\n `<p style="margin-top:.75rem"><button type="button" class="btn btn-accent" id="featuresRun">Run feature analysis</button></p>`+\n techBlock({note:"Deferred until explicitly requested"},"featTech");\n }\n const feats=(data&&(data.features||data.items||data.entries))||[];\n return renderListPage("Features","Feature intelligence from repository evidence.",feats,{\n headers:["Feature","Truth","Evidence"],limit:80,raw:data,\n row:f=>`<tr><td>${esc(f.name||f.id||f.title||"\u2014")}</td><td>${truthBadge(f.truth)}</td><td class="mono">${esc(f.path||(f.evidence&&f.evidence[0]&&f.evidence[0].path)||"\u2014")}</td></tr>`\n },"No features detected","Feature intelligence returned no entries for this project.");\n}\n\nfunction renderDecisions(data){\n const dec=(data&&(data.decisions||data.items))||[];\n const projectName=(state.dna&&state.dna.name)||"Current project";\n function decisionSummary(d){\n const raw=d.bodyExcerpt||d.decision||d.summary||d.context||d.reason||"";\n if(!raw||typeof raw!=="string")return "";\n const lines=raw.split(/\\r?\\n/).map(l=>l.trim()).filter(Boolean);\n const statusLine=lines.find(l=>/\\*\\*Status\\*\\*|\\*\\*\u0421\u0442\u0430\u0442\u0443\u0441\\*\\*|Status:|\u0421\u0442\u0430\u0442\u0443\u0441:/i.test(l));\n const prose=lines.filter(l=>!l.startsWith("#")&&!l.startsWith(">")&&!l.startsWith("- **")&&!l.startsWith("##")).slice(0,3);\n let text=(statusLine?statusLine.replace(/^\\-\\s*/,""):"")+(prose.length?" "+prose.join(" "):"");\n text=text.replace(/\\*\\*/g,"").replace(/\\s+/g," ").trim();\n if(!text){text=lines.filter(l=>!l.startsWith("#")).slice(0,4).join(" ").replace(/\\*\\*/g,"").replace(/\\s+/g," ").trim();}\n if(text.length>220)text=text.slice(0,217)+"\u2026";\n return text;\n }\n function decisionPath(d){\n const ev=(d.evidence&&d.evidence[0]&&d.evidence[0].path)||d.path||d.id||"";\n return shortPath(String(ev));\n }\n const meaning=(d)=>d.truthMeaning||"File evidence was found and parsed for the current project. This does not prove the architectural decision is correct.";\n return pageHeader("Decisions","Review architecture decision records attributed to this project.","")+\n `<p class="ask-banner"><strong>VERIFIED</strong> means AgentDoctor found and parsed this project\'s decision file \u2014 not that the architecture choice is independently proven correct.</p>`+\n (dec.length?`<div class="grid">${dec.slice(0,40).map(d=>{\n const summary=decisionSummary(d);\n const srcKind=d.sourceKind||d.source||"adr_file";\n const own=d.ownership||"project_owned";\n return `<article class="card"><h3>${esc(d.title||d.id||"Decision")}</h3><div>${truthBadge(d.truth)}${badge(srcKind)}${badge(own,"truth")}</div><p>${esc(summary||"No excerpt available for this decision.")}</p><p class="path-sec">Project: ${esc(projectName)}</p><p class="path-sec">Evidence: ${esc(decisionPath(d))}</p><p class="scope-note">${esc(meaning(d))}</p></article>`;\n }).join("")}</div><p class="scope-note">Showing up to 40 project-owned decisions. Nested repositories and private validation checkouts are excluded.</p>`:emptyState("No project decisions recorded","No architecture decision records were found in the current project\'s owned tree.","Add ADR markdown under docs/adr/ or adr/ for this repository."))+\n techBlock(data,"decTech");\n}\nfunction renderEvolution(data){\n const events=(data&&data.events)||[];\n return pageHeader("Evolution","See how the project changed over time from git evidence.", badge(data&&data.gitAvailable?"Git available":"Git unavailable",data&&data.gitAvailable?"ok":"unknown"))+\n (events.length?`<section class="section"><div class="table-wrap"><table class="data"><thead><tr><th>Date</th><th>Author</th><th>Subject</th><th>Files</th><th>Truth</th></tr></thead><tbody>${events.slice(0,50).map(e=>`<tr><td class="mono">${esc(e.date||"\u2014")}</td><td>${esc(e.author||"\u2014")}</td><td>${esc(e.subject||"\u2014")}</td><td>${esc(e.filesTouched??"\u2014")}</td><td>${truthBadge(e.truth)}</td></tr>`).join("")}</tbody></table></div></section>`:emptyState("No evolution events","Git timeline empty or git unavailable.","Initialize git or open another project."))+\n ((data&&data.trends&&data.trends.length)?`<section class="section"><h2>Trends</h2><div class="grid">${data.trends.map(t=>`<article class="card"><h3>${esc(t.label||t.id)}</h3><div>${badge(t.direction||"\u2014")}${truthBadge(t.truth)}</div><p>${esc(t.note||t.metric||"")}</p></article>`).join("")}</div></section>`:"")+\n techBlock(data,"evoTech");\n}\n\nfunction renderMemory(data){\n const hits=(data&&data.hits)||[];\n return pageHeader("Memory","Search project knowledge for this workspace. Neural memory remains EXTERNAL.","")+\n (hits.length?`<section class="section"><div class="table-wrap"><table class="data"><thead><tr><th>Hit</th><th>Score</th><th>Source</th></tr></thead><tbody>${hits.slice(0,50).map(h=>`<tr><td>${esc(h.text||h.title||h.id||"\u2014")}</td><td>${esc(h.score??"\u2014")}</td><td class="mono">${esc(h.path||h.source||"\u2014")}</td></tr>`).join("")}</tbody></table></div></section>`:emptyState("No memory matches","No project knowledge entries matched this query.","Try another term, or build more project context first."))+\n techBlock(data,"memTech");\n}\n\nfunction renderTwin(data){\n return pageHeader("Digital Twin","Inspect a deterministic project snapshot. Live runtime sync is EXTERNAL.", "")+\n `<section class="section"><h2>Deterministic project snapshot</h2><div class="grid">\n ${card("Snapshot",data?"Healthy":"Unknown",data&&data.truth?data.truth:"INFERRED",data?"Local project snapshot is available.":"Snapshot not available.","#twin","")}\n ${card("Live runtime","Unknown","EXTERNAL","Live APM, Kubernetes, or cloud sync is not connected.","#system","System")}\n </div></section>`+\n (data&&data.components?`<section class="section"><h2>Components</h2><div class="chips">${[].concat(data.components).slice(0,40).map(c=>badge(typeof c==="string"?c:(c.name||c.id||"component"))).join("")}</div></section>`:"")+\n techBlock(data,"twinTech");\n}\n\nfunction renderForensic(data){\n return pageHeader("Forensics","Investigate project evidence in a read-only forensic view.", badge("Read-only","ok"))+\n `<section class="section"><h2>Analysis</h2>${data?`<p class="page-desc">Forensic analysis is available. Expand Technical Details for full evidence structures.</p><div class="meta-row">${badge("Findings: "+((data.findings&&data.findings.length)||(data.events&&data.events.length)||"see JSON"))}</div>`:emptyState("No forensic report","No forensic analysis is available for this project yet.","Retry after analysis, or review Evidence and Security.")}</section>`+\n techBlock(data,"forTech");\n}\n\nfunction renderIncident(data){\n const items=(data&&(data.incidents||data.hypotheses||data.items))||[];\n return pageHeader("Incidents","Review incident hypotheses from available project evidence.","")+\n (items.length?`<div class="grid">${items.slice(0,30).map(i=>`<article class="card"><h3>${esc(i.title||i.id||"Incident")}</h3><div>${badge(i.status||"recorded")}${truthBadge(i.truth)}</div><p>${esc(i.summary||i.description||"\u2014")}</p></article>`).join("")}</div>`:emptyState("No incidents detected / recorded","No incident hypotheses were produced for this project.","This is not a claim that production never fails \u2014 only that no local incident model entries exist."))+\n techBlock(data,"incTech");\n}\n\nfunction renderInfra(data){\n return pageHeader("Infrastructure","Review local infrastructure signals. Cloud, Kubernetes, and APM remain EXTERNAL.","")+\n `<section class="section"><h2>Local project signals</h2>${data?`<p class="page-desc">Local infrastructure signals were detected. Expand Technical Details for the full analysis.</p>`:emptyState("No local infrastructure signals","No infrastructure files or markers were detected in this project.","Docker, compose, or IaC files will appear here when present.")}</section>`+\n `<section class="section"><h2>External runtime</h2>${emptyState("Live runtime is EXTERNAL","Kubernetes, APM, and cloud live intelligence are not connected here.","AgentDoctor will not invent runtime metrics.")}</section>`+\n techBlock(data,"infraTech");\n}\n\nfunction renderWhatIf(data){\n return pageHeader("What-if","Explore possible changes and their expected impact. Predictions are not verified facts.", badge("Prediction \u2260 certainty","attention"))+\n `<div class="uncertainty">\n ${card("Observed","\u2014","VERIFIED","Facts grounded in repository evidence.","","")}\n ${card("Inferred","\u2014","INFERRED","Relationships suggested by analysis.","","")}\n ${card("Predicted","\u2014","UNKNOWN","Scenario impact \u2014 not certainty.","","")}\n </div>\n <section class="section"><h2>Scenario</h2>${data?`<p class="page-desc">Default target analysis loaded. Treat predicted impact as a hypothesis.</p><div class="meta-row">${truthBadge(data.truth||"INFERRED")}${badge(data.target||"target: src")}${badge("Not verified fact","attention")}</div>`:emptyState("No scenario result","No what-if analysis is available yet.","Retry, or open Graph/Map for structural context first.")}</section>`+\n techBlock(data,"whatTech");\n}\n\nfunction renderSearch(data){\n const hits=(data&&data.hits)||[];\n return pageHeader("Search","Search files, symbols, and concepts in this project.","")+\n `<section class="section"><div style="display:flex;gap:.5rem;flex-wrap:wrap"><input id="searchQ" class="btn filter-input" style="flex:1;min-width:180px" placeholder="Search symbols & concepts\u2026" aria-label="Search query" value="${esc((data&&data.query)||"main")}" /><button type="button" class="btn btn-accent" id="searchGo">Search</button></div></section>`+\n (hits.length?`<section class="section"><div class="table-wrap"><table class="data"><thead><tr><th>Hit</th><th>Kind</th><th>Path</th><th>Score</th><th>Truth</th></tr></thead><tbody>${hits.slice(0,50).map(h=>`<tr><td>${esc(h.text||h.symbol||h.name||"\u2014")}</td><td>${esc(h.kind||"\u2014")}</td><td class="mono">${esc(h.path||"\u2014")}</td><td>${esc(h.score??"\u2014")}</td><td>${truthBadge(h.truth)}</td></tr>`).join("")}</tbody></table></div></section>`:emptyState("No hits","No search hits for this query.","Try another term."))+\n techBlock(data,"searchTech");\n}\n\nfunction renderDoctors(bundle){\n const docs=[\n {name:"API Doctor",data:bundle&&bundle.api,desc:"REST/OpenAPI/GraphQL static surface"},\n {name:"Database Doctor",data:bundle&&bundle.db,desc:"Schema/migration/ORM file evidence"},\n {name:"Event Doctor",data:bundle&&bundle.events,desc:"Queue/event file evidence"},\n ];\n return pageHeader("Doctors","Run specialized static diagnostics for API, database, and events.","")+\n `<div class="grid">${docs.map(d=>`<article class="card"><h3>${esc(d.name)}</h3><p>${esc(d.desc)}</p><div>${d.data?badge("Available","ok"):badge("Unavailable","unknown")}</div><p class="path-sec">${d.data?"Analysis available \u2014 see Technical Details.":"No analysis available."}</p></article>`).join("")}</div>`+\n techBlock(bundle,"docTech");\n}\n\n\nfunction renderEvidence(scan,brain){\n const findings=(scan&&scan.findings)||[];\n const hasBrain=!!(brain&&brain.hasSnapshot);\n const rows=findings.slice(0,50).map(f=>{\n const sev=f.severity||f.severityLabel||"unknown";\n const file=f.file||f.path||f.location||"Unknown";\n const msg=f.message||f.title||f.rule||"Finding";\n const truth=f.truth||f.truthStatus||"INFERRED";\n return `<tr><td>${esc(sev)}</td><td>${esc(msg)}</td><td class="mono">${esc(shortPath(String(file)))}</td><td>${truthBadge(truth)}</td></tr>`;\n }).join("");\n const empty=findings.length?"":emptyState("No scan findings recorded","Run a project scan from the CLI or refresh Home to collect evidence-backed findings.");\n const table=findings.length?`<div class="table-wrap"><table class="data"><thead><tr><th>Severity</th><th>Finding</th><th>File</th><th>Truth</th></tr></thead><tbody>${rows}</tbody></table></div>`:empty;\n const brainHtml=hasBrain\n ?`<p>Brain snapshot is available for this project. Evidence is scoped to the active workspace.</p>`\n :emptyState("No brain snapshot","Project Brain has not produced a snapshot yet. Analyze the project to build durable evidence.");\n return pageHeader("Evidence","Trace claims back to scan findings and project brain evidence.",badge(String(findings.length)+" findings")+badge(hasBrain?"Brain present":"Brain absent","truth"))+\n `<section class="section"><h2>Scan findings</h2>${table}</section>`+\n `<section class="section"><h2>Project Brain</h2>${brainHtml}</section>`+\n techBlock({scan,brain},"evidenceTech");\n}\n\nfunction renderLearn(dna,brain){\n return pageHeader("Learn","Understand this project, prepare explanations, and study with guided steps.","")+\n `<section class="section"><h2>What this project is</h2>\n <p>This repository is <strong>${esc((dna&&dna.name)||"Unknown")}</strong> (<strong>${esc((dna&&dna.projectType)||"Unknown")}</strong>).</p>\n <p class="page-desc">Languages: ${esc(joinList((dna&&dna.languages)||[],8))}. Frameworks: ${esc(joinList((dna&&dna.frameworks)||[],8))}.</p>\n <div class="meta-row">${badge(dna&&dna.git&&dna.git.present?"Git present":"Git not detected")}${badge(brain&&brain.hasSnapshot?"Brain ready":"Brain not ready yet","truth")}</div>\n </section>`+\n `<section class="section"><h2>How to study it</h2><ol>\n <li><a href="#dna">DNA</a> \u2014 technologies and structure.</li>\n <li><a href="#map">Map</a> \u2014 major parts of the codebase.</li>\n <li><a href="#chat">Project Chat</a> \u2014 ask \u201chow does X work?\u201d (ask-only; truth-labeled).</li>\n <li><a href="#security">Security</a> \u2014 findings with evidence.</li>\n <li><a href="#evidence">Evidence</a> \u2014 where claims come from.</li>\n </ol></section>`+\n `<section class="section"><h2>Viva / explanation prep</h2>\n <p class="page-desc">Practice explaining: purpose, architecture, key modules, tests, and risks. Prefer evidence-backed answers from Chat and DNA over guessing.</p>\n <p class="scope-note">CLI also supports <span class="mono">agentdoctor learn</span> and viva-style prompts.</p>\n </section>`+\n techBlock({dna,brain},"learnTech");\n}\n\nfunction renderSystem(status){\n return pageHeader("System","Review safety posture, adapters, and system information.", badge("Read-only","ok")+badge("Local-only","ok"))+\n `<section class="section"><h2>Safety</h2><div class="safety-row">${badge("Local-only")}${badge("Read-only dashboard")}${badge("No cloud writes")}${badge("Evaluate-only policy")}</div>\n <p class="page-desc" style="margin-top:.6rem">${esc((status&&status.actionPolicyNote)||"Action Policy Evaluator is evaluate-only.")}</p>\n <p class="page-desc">${esc((status&&status.roleNote)||"Query ?user= is a localDevIdentityHint only.")}</p>\n </section>`+\n `<section class="section"><h2>Runtime</h2><div class="meta-row">${badge("v"+((status&&status.ops&&status.ops.version)||EMBEDDED_VERSION))}${badge("contracts "+((status&&status.contractsVersion)||"Unknown"))}${badge(status&&status.loopbackOnly?"loopback":"non-loopback",status&&status.loopbackOnly?"ok":"attention")}</div>\n <p class="path-sec">${esc(shortPath(status&&status.root))}</p>\n <h3>Adapters</h3><div class="chips">${((status&&status.adapters)||[]).map(a=>badge(a)).join("")||badge("None")}</div>\n </section>`+\n techBlock(status,"sysTech");\n}\n\nfunction renderChatShell(){\n return pageHeader("Project Chat","Ask questions about this project. Answers include truth labels and evidence when available.","")+\n `<div class="ask-banner"><strong>Ask-only.</strong> Dashboard Chat explains the project. It cannot edit files, run shell commands, or apply changes. Use CLI/MCP agent flows when you need approved actions.</div>\n <div class="chat-layout">\n <div>\n <div id="chatLog" class="chat-log"><div class="chat-msg muted">Ask how this repository works. Answers use project DNA, map, brain, and analyzers when available.</div></div>\n <div class="composer"><label class="path-sec" for="chatInput">Question</label><textarea id="chatInput" placeholder="How does authentication work?"></textarea><button type="button" class="btn btn-accent" id="chatAsk">Ask</button></div>\n </div>\n <div class="section" style="margin:0"><h2>Evidence</h2><p class="scope-note">Truth claims, citations, and limitations for the latest answer.</p><pre class="pre" id="chatEvidence">No answer yet.</pre></div>\n </div>`;\n}\n\nasync function loadRoute(route){\n const gen=++state.loadGen;\n const root=document.getElementById("pageRoot");\n const still=()=>gen===state.loadGen && state.route===route;\n root.innerHTML=loadingState("Loading "+(PAGES[route]?PAGES[route].label:route)+"\\u2026");\n try{\n if(route==="home"){\n if(!still())return;\n root.innerHTML=renderHomeView(deriveHome(state.status,state.dna,null,null,null,null,null))+`<p class="page-desc">Loading project context\\u2026</p>`;\n await ensureContext();\n if(!still())return;\n root.innerHTML=renderHomeView(deriveHome(state.status,state.dna,null,null,null,null,null))+`<p class="page-desc">Loading scan/security/deps\\u2026</p>`;\n // Stagger heavier analyzers so the single-threaded dashboard server stays responsive.\n let brain=null,health=null,deps=null,scan=null,security=null;\n try{brain=await fetchJson("/api/brain",{timeoutMs:15000});}catch{}\n if(!still())return;\n root.innerHTML=renderHomeView(deriveHome(state.status,state.dna,null,brain,null,null,null))+`<p class="page-desc">Loading health and dependencies\u2026</p>`;\n try{health=await fetchJson("/api/health",{timeoutMs:15000});}catch{}\n if(!still())return;\n try{deps=await fetchJson("/api/deps",{timeoutMs:15000});}catch{}\n if(!still())return;\n root.innerHTML=renderHomeView(deriveHome(state.status,state.dna,null,brain,null,deps,health))+`<p class="page-desc">Loading scan and security\u2026</p>`;\n try{scan=await fetchJson("/api/scan",{timeoutMs:20000});}catch{}\n if(!still())return;\n try{security=await fetchJson("/api/security",{timeoutMs:20000});}catch{}\n if(!still())return;\n root.innerHTML=renderHomeView(deriveHome(state.status,state.dna,scan,brain,security,deps,health));\n bindCopy();\n return;\n }\n await ensureContext();\n if(!still())return;\n async function paint(html){ if(!still())return; root.innerHTML=html; bindCopy(); }\n if(route==="chat"){await paint(renderChatShell());bindChat();return;}\n if(route==="dna"){await paint(renderDna(state.dna||await cached("dna","/api/dna")));return;}\n if(route==="graph"){await paint(renderGraph(await cached("graph","/api/v2/graph",{timeoutMs:20000})));return;}\n if(route==="map"){await paint(renderMap(await cached("map","/api/map",{timeoutMs:20000})));return;}\n if(route==="security"){await paint(renderSecurity(await cached("security","/api/security",{timeoutMs:20000})));return;}\n if(route==="deps"){await paint(renderDeps(await cached("deps","/api/deps",{timeoutMs:20000})));bindDepsFilter();return;}\n if(route==="health"){await paint(renderHealth(await cached("health","/api/health",{timeoutMs:15000}),state.dna));return;}\n if(route==="requirements"){await paint(renderRequirements(await cached("requirements","/api/requirements",{timeoutMs:20000})));return;}\n if(route==="features"){if(state.cache.features){await paint(renderFeatures(state.cache.features));}else{await paint(renderFeatures(null,{deferred:true}));document.getElementById("featuresRun")?.addEventListener("click",async()=>{const root=document.getElementById("pageRoot");root.innerHTML=loadingState("Running feature analysis\u2026");try{const data=await cached("features","/api/features",{timeoutMs:60000});if(state.route==="features"){root.innerHTML=renderFeatures(data);bindCopy();}}catch(err){if(state.route==="features")root.innerHTML=errorState(err&&err.message?err.message:String(err));}}); }return;}\n if(route==="evidence"){const [scan,brain]=await Promise.all([cached("scan","/api/scan",{timeoutMs:20000}).catch(()=>null),cached("brain","/api/brain",{timeoutMs:15000}).catch(()=>null)]);await paint(renderEvidence(scan,brain));return;}\n if(route==="decisions"){await paint(renderDecisions(await cached("decisions","/api/decisions",{timeoutMs:15000})));return;}\n if(route==="evolution"){await paint(renderEvolution(await cached("evolution","/api/evolution",{timeoutMs:20000})));return;}\n if(route==="memory"){await paint(renderMemory(await cached("memory","/api/memory?q=change",{timeoutMs:15000})));return;}\n if(route==="twin"){await paint(renderTwin(await cached("twin","/api/twin",{timeoutMs:15000})));return;}\n if(route==="forensic"){await paint(renderForensic(await cached("forensic","/api/forensic",{timeoutMs:20000})));return;}\n if(route==="incident"){await paint(renderIncident(await cached("incident","/api/incident",{timeoutMs:15000})));return;}\n if(route==="infra"){await paint(renderInfra(await cached("infra","/api/infra",{timeoutMs:15000})));return;}\n if(route==="whatif"){await paint(renderWhatIf(await cached("whatif","/api/what-if?target=src",{timeoutMs:20000})));return;}\n if(route==="search"){await paint(renderSearch(await cached("search","/api/search?q=main",{timeoutMs:15000})));bindSearch();return;}\n if(route==="doctors"){\n const [api,db,events]=await Promise.all([\n cached("apiDoctor","/api/api-doctor",{timeoutMs:15000}).catch(()=>null),\n cached("database","/api/database",{timeoutMs:15000}).catch(()=>null),\n cached("events","/api/events",{timeoutMs:15000}).catch(()=>null),\n ]);\n await paint(renderDoctors({api,db,events}));return;\n }\n if(route==="learn"){await paint(renderLearn(state.dna, await cached("brain","/api/brain",{timeoutMs:15000}).catch(()=>null)));return;}\n if(route==="system"){await paint(renderSystem(state.status));return;}\n if(still()) root.innerHTML=errorState("Unknown route");\n }catch(err){\n if(!still())return;\n const msg=(err&&err.name==="AbortError")?"This analysis timed out. The project may be too large for the current local analysis window.":(err&&err.message?err.message:String(err));\n root.innerHTML=errorState(msg,(err&&err.name==="AbortError")?"Try again, or open DNA / System while investigating.":undefined);\n }\n}\n\nfunction bindCopy(){\n document.querySelectorAll("[data-copy]").forEach(btn=>{\n btn.addEventListener("click",async()=>{\n const id=btn.getAttribute("data-copy");\n const pre=document.getElementById(id)||document.getElementById(id+"Json")||btn.parentElement.parentElement.querySelector("pre");\n try{await navigator.clipboard.writeText(pre?pre.textContent:"");btn.textContent="Copied";setTimeout(()=>btn.textContent="Copy JSON",1200);}catch{btn.textContent="Copy failed";}\n });\n });\n}\nfunction bindDepsFilter(){\n const input=document.getElementById("depsFilter"); if(!input)return;\n input.addEventListener("input",()=>{\n const q=input.value.trim().toLowerCase();\n document.querySelectorAll("#depsTable tbody tr").forEach(tr=>{tr.style.display=!q||(tr.getAttribute("data-name")||"").includes(q)?"":"none";});\n });\n}\nfunction bindSearch(){\n const go=()=>{const q=(document.getElementById("searchQ").value||"main").trim()||"main";state.cache["search"]=null;cached("search","/api/search?q="+encodeURIComponent(q)).then(data=>{document.getElementById("pageRoot").innerHTML=renderSearch(data);bindCopy();bindSearch();});};\n document.getElementById("searchGo")?.addEventListener("click",go);\n document.getElementById("searchQ")?.addEventListener("keydown",e=>{if(e.key==="Enter")go();});\n}\nfunction bindChat(){\n document.getElementById("chatAsk")?.addEventListener("click",async()=>{\n const question=document.getElementById("chatInput").value.trim(); if(!question)return;\n const log=document.getElementById("chatLog");\n log.innerHTML+=`<div class="chat-msg user"><strong>You</strong><p>${esc(question)}</p></div>`;\n document.getElementById("chatEvidence").textContent="\u2026";\n try{\n const res=await fetch("/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({question})});\n const data=await res.json();\n log.innerHTML+=`<div class="chat-msg"><strong>AgentDoctor</strong><p>${esc(data.message||JSON.stringify(data))}</p></div>`;\n document.getElementById("chatEvidence").textContent=JSON.stringify({truthClaims:data.truthClaims||[],citations:data.citations||[],limitations:data.limitations||[],status:data.status},null,2);\n log.scrollTop=log.scrollHeight;\n }catch(err){log.innerHTML+=`<div class="chat-msg error">${esc(String(err))}</div>`;}\n });\n}\n\n/* Shell interactions */\nfunction toggleSidebar(){\n document.getElementById("app").classList.toggle("collapsed");\n const sb=document.getElementById("sidebar");\n if(window.matchMedia("(max-width:980px)").matches){\n sb.classList.toggle("open");\n document.getElementById("drawerBackdrop").classList.toggle("open",sb.classList.contains("open"));\n }\n}\nfunction openPalette(){document.getElementById("palette").classList.add("open");const input=document.getElementById("paletteInput");input.value="";renderPalette("");input.focus();}\nfunction closePalette(){document.getElementById("palette").classList.remove("open");}\nfunction renderPalette(q){\n const query=q.trim().toLowerCase();\n const pages=Object.values(PAGES).filter(p=>!query||p.label.toLowerCase().includes(query)||p.id.includes(query)||p.group.toLowerCase().includes(query));\n const list=document.getElementById("paletteList");\n list.innerHTML=pages.map((p,i)=>`<button type="button" class="palette-item${i===0?" active":""}" data-go="${esc(p.id)}"><span>${esc(p.label)}</span><small>${esc(p.group)}</small></button>`).join("")+\n `<button type="button" class="palette-item" data-search="${esc(q)}"><span>Search project for \u201c${esc(q||"\u2026")}\u201d</span><small>Search API</small></button>`;\n list.querySelectorAll("[data-go]").forEach(b=>b.addEventListener("click",()=>{closePalette();setRoute(b.getAttribute("data-go"));}));\n list.querySelectorAll("[data-search]").forEach(b=>b.addEventListener("click",()=>{closePalette();const qq=b.getAttribute("data-search")||"main";state.cache.search=null;setRoute("search");setTimeout(()=>{const input=document.getElementById("searchQ");if(input){input.value=qq;document.getElementById("searchGo")?.click();}},0);}));\n}\n\nfunction boot(){\n renderSidebar();\n document.getElementById("btnCollapse").addEventListener("click",toggleSidebar);\n document.getElementById("btnMenu").addEventListener("click",toggleSidebar);\n document.getElementById("drawerBackdrop").addEventListener("click",()=>{document.getElementById("sidebar").classList.remove("open");document.getElementById("drawerBackdrop").classList.remove("open");});\n document.getElementById("btnCommand").addEventListener("click",openPalette);\n document.getElementById("paletteInput").addEventListener("input",e=>renderPalette(e.target.value));\n document.getElementById("paletteInput").addEventListener("keydown",e=>{\n const items=[...document.querySelectorAll(".palette-item")];\n if(!items.length)return;\n const cur=items.findIndex(i=>i.classList.contains("active"));\n if(e.key==="ArrowDown"){e.preventDefault();const n=items[(cur+1)%items.length];items.forEach(i=>i.classList.remove("active"));n.classList.add("active");n.scrollIntoView({block:"nearest"});}\n if(e.key==="ArrowUp"){e.preventDefault();const n=items[(cur<=0?items.length-1:cur-1)];items.forEach(i=>i.classList.remove("active"));n.classList.add("active");n.scrollIntoView({block:"nearest"});}\n if(e.key==="Enter"){e.preventDefault();(items.find(i=>i.classList.contains("active"))||items[0]).click();}\n });\n document.getElementById("palette").addEventListener("click",e=>{if(e.target.id==="palette")closePalette();});\n window.addEventListener("keydown",e=>{\n if((e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==="k"){e.preventDefault();openPalette();}\n if(e.key==="Escape")closePalette();\n });\n window.addEventListener("hashchange",()=>setRoute((location.hash||"#home").slice(1)||"home",{fromHash:true}));\n setRoute((location.hash||"#home").slice(1)||"home");\n}\nboot();';
|