@jefuriiij/synthra 0.2.1 → 0.3.1
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 +42 -0
- package/dist/cli/index.js +520 -267
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/index.js +151 -29
- package/dist/dashboard/index.js.map +1 -1
- package/dist/server/index.js +279 -93
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -18,7 +18,7 @@ var init_package = __esm({
|
|
|
18
18
|
"package.json"() {
|
|
19
19
|
package_default = {
|
|
20
20
|
name: "@jefuriiij/synthra",
|
|
21
|
-
version: "0.
|
|
21
|
+
version: "0.3.1",
|
|
22
22
|
publishConfig: {
|
|
23
23
|
access: "public"
|
|
24
24
|
},
|
|
@@ -146,7 +146,125 @@ function isFree(port) {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
// src/dashboard/delta.ts
|
|
149
|
-
import { readFile as
|
|
149
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
150
|
+
|
|
151
|
+
// src/learn/store.ts
|
|
152
|
+
import { appendFile, mkdir, readFile, writeFile } from "fs/promises";
|
|
153
|
+
import { dirname } from "path";
|
|
154
|
+
|
|
155
|
+
// src/learn/usage.ts
|
|
156
|
+
var LEARN_SCHEMA_VERSION = 1;
|
|
157
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
158
|
+
function halfLifeMs() {
|
|
159
|
+
const env = Number(process.env.SYN_LEARN_HALFLIFE_DAYS);
|
|
160
|
+
const days = Number.isFinite(env) && env > 0 ? env : 7;
|
|
161
|
+
return days * DAY_MS;
|
|
162
|
+
}
|
|
163
|
+
function weightFor(source) {
|
|
164
|
+
switch (source) {
|
|
165
|
+
case "register_edit":
|
|
166
|
+
return 2;
|
|
167
|
+
case "read":
|
|
168
|
+
return 1;
|
|
169
|
+
default:
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function emptyStore() {
|
|
174
|
+
return {
|
|
175
|
+
schema_version: LEARN_SCHEMA_VERSION,
|
|
176
|
+
asOf: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
177
|
+
files: {}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function decayFactor(fromTs, toMs, hl) {
|
|
181
|
+
const fromMs = Date.parse(fromTs);
|
|
182
|
+
if (!Number.isFinite(fromMs)) return 1;
|
|
183
|
+
const dt = toMs - fromMs;
|
|
184
|
+
if (dt <= 0) return 1;
|
|
185
|
+
return Math.exp(-(Math.LN2 / hl) * dt);
|
|
186
|
+
}
|
|
187
|
+
function foldEvent(store, ev) {
|
|
188
|
+
const w = weightFor(ev.source);
|
|
189
|
+
if (w <= 0 || !ev.path) return store;
|
|
190
|
+
const tMs = Date.parse(ev.ts);
|
|
191
|
+
if (!Number.isFinite(tMs)) return store;
|
|
192
|
+
const hl = halfLifeMs();
|
|
193
|
+
const prev = store.files[ev.path];
|
|
194
|
+
if (prev) {
|
|
195
|
+
const decayed = prev.decayed * decayFactor(prev.lastTs, tMs, hl) + w;
|
|
196
|
+
store.files[ev.path] = { count: prev.count + 1, decayed, lastTs: ev.ts };
|
|
197
|
+
} else {
|
|
198
|
+
store.files[ev.path] = { count: 1, decayed: w, lastTs: ev.ts };
|
|
199
|
+
}
|
|
200
|
+
return store;
|
|
201
|
+
}
|
|
202
|
+
function effectiveScores(store, nowMs) {
|
|
203
|
+
const hl = halfLifeMs();
|
|
204
|
+
const out = /* @__PURE__ */ new Map();
|
|
205
|
+
for (const [path, stat6] of Object.entries(store.files)) {
|
|
206
|
+
const eff = stat6.decayed * decayFactor(stat6.lastTs, nowMs, hl);
|
|
207
|
+
if (eff > 0.01) out.set(path, eff);
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
function recomputeFromLog(events) {
|
|
212
|
+
const store = emptyStore();
|
|
213
|
+
for (const ev of events) foldEvent(store, ev);
|
|
214
|
+
return store;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/learn/store.ts
|
|
218
|
+
async function readLearnStore(path) {
|
|
219
|
+
try {
|
|
220
|
+
const raw = await readFile(path, "utf8");
|
|
221
|
+
const parsed = JSON.parse(raw);
|
|
222
|
+
if (parsed.schema_version !== LEARN_SCHEMA_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
|
|
223
|
+
return emptyStore();
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
schema_version: LEARN_SCHEMA_VERSION,
|
|
227
|
+
asOf: typeof parsed.asOf === "string" ? parsed.asOf : emptyStore().asOf,
|
|
228
|
+
files: parsed.files
|
|
229
|
+
};
|
|
230
|
+
} catch {
|
|
231
|
+
return emptyStore();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function writeLearnStore(path, store) {
|
|
235
|
+
try {
|
|
236
|
+
await mkdir(dirname(path), { recursive: true });
|
|
237
|
+
await writeFile(path, JSON.stringify(store, null, 2) + "\n", "utf8");
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function readAccessLog(path) {
|
|
242
|
+
try {
|
|
243
|
+
const raw = await readFile(path, "utf8");
|
|
244
|
+
const out = [];
|
|
245
|
+
for (const line of raw.split("\n")) {
|
|
246
|
+
const t = line.trim();
|
|
247
|
+
if (!t) continue;
|
|
248
|
+
try {
|
|
249
|
+
const ev = JSON.parse(t);
|
|
250
|
+
if (ev && typeof ev.ts === "string" && typeof ev.path === "string" && typeof ev.source === "string") {
|
|
251
|
+
out.push(ev);
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
} catch {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function appendAccess(path, ev) {
|
|
262
|
+
try {
|
|
263
|
+
await mkdir(dirname(path), { recursive: true });
|
|
264
|
+
await appendFile(path, JSON.stringify(ev) + "\n", "utf8");
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
}
|
|
150
268
|
|
|
151
269
|
// src/shared/paths.ts
|
|
152
270
|
import { join } from "path";
|
|
@@ -167,6 +285,7 @@ function resolvePaths(projectRoot) {
|
|
|
167
285
|
toolLog: join(graphDir, "tool_log.jsonl"),
|
|
168
286
|
accessLog: join(graphDir, "access_log.jsonl"),
|
|
169
287
|
learnStore: join(graphDir, "learn_store.json"),
|
|
288
|
+
parseCache: join(graphDir, "parse_cache.json"),
|
|
170
289
|
mcpPort: join(graphDir, "mcp_port"),
|
|
171
290
|
mcpServerLog: join(graphDir, "mcp_server.log"),
|
|
172
291
|
mcpServerErrLog: join(graphDir, "mcp_server.err.log"),
|
|
@@ -209,15 +328,15 @@ function estimateCostUsd(usage) {
|
|
|
209
328
|
}
|
|
210
329
|
|
|
211
330
|
// src/shared/project-registry.ts
|
|
212
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
331
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
213
332
|
import { homedir } from "os";
|
|
214
|
-
import { basename, dirname, join as join2 } from "path";
|
|
333
|
+
import { basename, dirname as dirname2, join as join2 } from "path";
|
|
215
334
|
var REGISTRY_DIR = join2(homedir(), ".synthra");
|
|
216
335
|
var REGISTRY_PATH = join2(REGISTRY_DIR, "projects.json");
|
|
217
336
|
var SCHEMA_VERSION = 1;
|
|
218
337
|
async function readRegistry() {
|
|
219
338
|
try {
|
|
220
|
-
const raw = await
|
|
339
|
+
const raw = await readFile2(REGISTRY_PATH, "utf8");
|
|
221
340
|
const parsed = JSON.parse(raw);
|
|
222
341
|
if (!Array.isArray(parsed.projects)) return { schema_version: SCHEMA_VERSION, projects: [] };
|
|
223
342
|
return { schema_version: parsed.schema_version ?? SCHEMA_VERSION, projects: parsed.projects };
|
|
@@ -226,8 +345,8 @@ async function readRegistry() {
|
|
|
226
345
|
}
|
|
227
346
|
}
|
|
228
347
|
async function writeRegistry(registry) {
|
|
229
|
-
await
|
|
230
|
-
await
|
|
348
|
+
await mkdir2(dirname2(REGISTRY_PATH), { recursive: true });
|
|
349
|
+
await writeFile2(REGISTRY_PATH, JSON.stringify(registry, null, 2) + "\n", "utf8");
|
|
231
350
|
}
|
|
232
351
|
async function recordProject(projectRoot) {
|
|
233
352
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -264,9 +383,12 @@ function countToolCalls(entries) {
|
|
|
264
383
|
}
|
|
265
384
|
return out;
|
|
266
385
|
}
|
|
386
|
+
function topHotFiles(store, nowMs, limit = 8) {
|
|
387
|
+
return [...effectiveScores(store, nowMs).entries()].map(([path, score2]) => ({ path, score: Math.round(score2 * 10) / 10 })).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
388
|
+
}
|
|
267
389
|
async function readJsonl(path) {
|
|
268
390
|
try {
|
|
269
|
-
const text = await
|
|
391
|
+
const text = await readFile3(path, "utf8");
|
|
270
392
|
return text.split(/\r?\n/).filter((l) => l.length > 0).map((l) => {
|
|
271
393
|
try {
|
|
272
394
|
return JSON.parse(l);
|
|
@@ -299,6 +421,7 @@ function summarize(p) {
|
|
|
299
421
|
}
|
|
300
422
|
const blocked = p.gates.filter((g) => g.decision === "block").length;
|
|
301
423
|
const saved = blocked * AVG_TOKENS_PER_BLOCKED_GREP;
|
|
424
|
+
const now = Date.now();
|
|
302
425
|
return {
|
|
303
426
|
path: p.path,
|
|
304
427
|
name: p.name,
|
|
@@ -314,6 +437,8 @@ function summarize(p) {
|
|
|
314
437
|
estimated_cost_usd: Math.round(costUsd * 100) / 100,
|
|
315
438
|
total_tool_calls: p.tools.length,
|
|
316
439
|
tool_calls: countToolCalls(p.tools),
|
|
440
|
+
hot_files: topHotFiles(p.learn, now),
|
|
441
|
+
hot_files_total: effectiveScores(p.learn, now).size,
|
|
317
442
|
models
|
|
318
443
|
};
|
|
319
444
|
}
|
|
@@ -323,13 +448,14 @@ function dedupeEnabled() {
|
|
|
323
448
|
}
|
|
324
449
|
async function loadProjectFiles(path, name, lastSeen) {
|
|
325
450
|
const paths = resolvePaths(path);
|
|
326
|
-
const [rawTokens, gates, tools] = await Promise.all([
|
|
451
|
+
const [rawTokens, gates, tools, learn] = await Promise.all([
|
|
327
452
|
readJsonl(paths.tokenLog),
|
|
328
453
|
readJsonl(paths.gateLog),
|
|
329
|
-
readJsonl(paths.toolLog)
|
|
454
|
+
readJsonl(paths.toolLog),
|
|
455
|
+
readLearnStore(paths.learnStore)
|
|
330
456
|
]);
|
|
331
457
|
const tokens = dedupeEnabled() ? dedupeTokens(rawTokens) : rawTokens;
|
|
332
|
-
return { path, name, last_seen: lastSeen, tokens, gates, tools };
|
|
458
|
+
return { path, name, last_seen: lastSeen, tokens, gates, tools, learn };
|
|
333
459
|
}
|
|
334
460
|
function dedupeTokens(entries) {
|
|
335
461
|
const score2 = (model) => {
|
|
@@ -392,7 +518,8 @@ async function computeDashboardData(activePaths, recentN = 500) {
|
|
|
392
518
|
last_seen: null,
|
|
393
519
|
tokens: [],
|
|
394
520
|
gates: [],
|
|
395
|
-
tools: []
|
|
521
|
+
tools: [],
|
|
522
|
+
learn: emptyStore()
|
|
396
523
|
};
|
|
397
524
|
const activeStats = summarize(activeFiles);
|
|
398
525
|
let g_in = 0, g_out = 0, g_cr = 0, g_cc = 0, g_gate = 0, g_block = 0, g_cost = 0, g_turns = 0, g_tools = 0;
|
|
@@ -477,6 +604,7 @@ var public_default = `<!doctype html>
|
|
|
477
604
|
<meta charset="UTF-8" />
|
|
478
605
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
479
606
|
<title>Synthra \xB7 Dashboard</title>
|
|
607
|
+
<link rel="icon" href="./favicon.svg" type="image/svg+xml" />
|
|
480
608
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
481
609
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
482
610
|
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
@@ -571,6 +699,9 @@ var public_default = `<!doctype html>
|
|
|
571
699
|
</div>
|
|
572
700
|
</div>
|
|
573
701
|
|
|
702
|
+
<!-- Savings + Cost, side by side -->
|
|
703
|
+
<div class="hero-row">
|
|
704
|
+
|
|
574
705
|
<!-- Savings hero -->
|
|
575
706
|
<div class="card savings has-tooltip" data-tooltip="What Synthra has saved you, as a deliberately conservative floor estimate. Each time the gate blocks an exploratory Grep/Glob, we credit 500 tokens \xD7 $3 per million-token input rate. Real savings are usually higher because the formula ignores cache thrash and follow-up Reads that the block also prevents. The audit line below shows the exact math live.">
|
|
576
707
|
<div class="card-head">
|
|
@@ -598,6 +729,26 @@ var public_default = `<!doctype html>
|
|
|
598
729
|
</div>
|
|
599
730
|
</div>
|
|
600
731
|
|
|
732
|
+
<!-- Cost hero (moved beside Savings) -->
|
|
733
|
+
<div class="card cost-hero has-tooltip" data-tooltip="Your all-time Claude spend across every project Synthra has tracked on this machine. Token counts come from Claude's transcript JSONL files; dollar amounts are computed by multiplying those counts by Anthropic's published per-model rates. See the FAQ for full rate tables.">
|
|
734
|
+
<div class="card-head">
|
|
735
|
+
<div class="card-eyebrow">Total spend \xB7 <em>all time</em></div>
|
|
736
|
+
</div>
|
|
737
|
+
<div class="big-money" id="big-cost">$0.<em>00</em></div>
|
|
738
|
+
<div class="cost-sub">
|
|
739
|
+
<div class="cs-row">
|
|
740
|
+
<span class="cs-k">Tokens (in+out)</span>
|
|
741
|
+
<span class="cs-v" id="cs-tokens">0</span>
|
|
742
|
+
</div>
|
|
743
|
+
<div class="cs-row">
|
|
744
|
+
<span class="cs-k">Avg / turn</span>
|
|
745
|
+
<span class="cs-v" id="cs-avg">$0.00</span>
|
|
746
|
+
</div>
|
|
747
|
+
</div>
|
|
748
|
+
</div>
|
|
749
|
+
|
|
750
|
+
</div><!-- /hero-row -->
|
|
751
|
+
|
|
601
752
|
<!-- Recent turns -->
|
|
602
753
|
<div class="card turns-card has-tooltip" data-tooltip="Every conversational turn Synthra has observed across all your projects, newest first. Each row shows when, which project, which model, and how the cost broke down between fresh input, generated output, and cache.">
|
|
603
754
|
<div class="card-head">
|
|
@@ -633,24 +784,6 @@ var public_default = `<!doctype html>
|
|
|
633
784
|
<!-- ===== Right ===== -->
|
|
634
785
|
<aside class="col-right">
|
|
635
786
|
|
|
636
|
-
<!-- Cost hero -->
|
|
637
|
-
<div class="card cost-hero has-tooltip" data-tooltip="Your all-time Claude spend across every project Synthra has tracked on this machine. Token counts come from Claude's transcript JSONL files; dollar amounts are computed by multiplying those counts by Anthropic's published per-model rates. See the FAQ for full rate tables.">
|
|
638
|
-
<div class="card-head">
|
|
639
|
-
<div class="card-eyebrow">Total spend \xB7 <em>all time</em></div>
|
|
640
|
-
</div>
|
|
641
|
-
<div class="big-money" id="big-cost">$0.<em>00</em></div>
|
|
642
|
-
<div class="cost-sub">
|
|
643
|
-
<div class="cs-row">
|
|
644
|
-
<span class="cs-k">Tokens (in+out)</span>
|
|
645
|
-
<span class="cs-v" id="cs-tokens">0</span>
|
|
646
|
-
</div>
|
|
647
|
-
<div class="cs-row">
|
|
648
|
-
<span class="cs-k">Avg / turn</span>
|
|
649
|
-
<span class="cs-v" id="cs-avg">$0.00</span>
|
|
650
|
-
</div>
|
|
651
|
-
</div>
|
|
652
|
-
</div>
|
|
653
|
-
|
|
654
787
|
<!-- Graph tool usage -->
|
|
655
788
|
<div class="card tools-card has-tooltip" data-tooltip="How often Claude actually used Synthra's graph tools (graph_continue / graph_read / \u2026) across all projects. A positive usage signal \u2014 unlike the Moat's block count, it captures every time Claude reached for the graph instead of running a Grep.">
|
|
656
789
|
<div class="card-head">
|
|
@@ -671,6 +804,16 @@ var public_default = `<!doctype html>
|
|
|
671
804
|
<div class="gate-mini" id="gate-mini"></div>
|
|
672
805
|
</div>
|
|
673
806
|
|
|
807
|
+
<!-- Hot files (usage-learning) -->
|
|
808
|
+
<div class="card tools-card has-tooltip" data-tooltip="Files Synthra has learned you work in most, weighted by recent use \u2014 it ranks these higher in retrieval. Scoped to the active project; decays over ~7 days, so the list reflects what's hot now.">
|
|
809
|
+
<div class="card-head">
|
|
810
|
+
<div class="card-eyebrow">Hot <em>files</em></div>
|
|
811
|
+
<div class="card-meta" id="hot-files-project">active project</div>
|
|
812
|
+
</div>
|
|
813
|
+
<div class="moat-value"><span id="hot-files-total">0</span> <em>tracked</em></div>
|
|
814
|
+
<div class="cost-sub" id="hot-files-list"></div>
|
|
815
|
+
</div>
|
|
816
|
+
|
|
674
817
|
</aside>
|
|
675
818
|
</main>
|
|
676
819
|
|
|
@@ -1042,6 +1185,40 @@ var public_default = `<!doctype html>
|
|
|
1042
1185
|
el.appendChild(frag);
|
|
1043
1186
|
}
|
|
1044
1187
|
|
|
1188
|
+
function shortenPath(p) {
|
|
1189
|
+
const parts = String(p).split('/');
|
|
1190
|
+
return parts.length <= 2 ? p : '\u2026/' + parts.slice(-2).join('/');
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function renderHotFiles(active) {
|
|
1194
|
+
const stats = (active && active.stats) || {};
|
|
1195
|
+
const files = stats.hot_files || [];
|
|
1196
|
+
$('#hot-files-total').innerHTML = fmt(stats.hot_files_total || 0);
|
|
1197
|
+
const proj = $('#hot-files-project');
|
|
1198
|
+
if (proj) proj.textContent = (active && active.project_name) || 'active project';
|
|
1199
|
+
const el = $('#hot-files-list');
|
|
1200
|
+
el.innerHTML = '';
|
|
1201
|
+
if (!files.length) {
|
|
1202
|
+
el.innerHTML = '<div class="empty">No usage learned yet \u2014 Synthra learns as you read/edit files.</div>';
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const frag = document.createDocumentFragment();
|
|
1206
|
+
for (const f of files) {
|
|
1207
|
+
const row = document.createElement('div');
|
|
1208
|
+
row.className = 'cs-row';
|
|
1209
|
+
row.title = f.path;
|
|
1210
|
+
const k = document.createElement('span');
|
|
1211
|
+
k.className = 'cs-k path';
|
|
1212
|
+
k.textContent = shortenPath(f.path);
|
|
1213
|
+
const v = document.createElement('span');
|
|
1214
|
+
v.className = 'cs-v';
|
|
1215
|
+
v.textContent = String(f.score);
|
|
1216
|
+
row.append(k, v);
|
|
1217
|
+
frag.appendChild(row);
|
|
1218
|
+
}
|
|
1219
|
+
el.appendChild(frag);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1045
1222
|
function renderTurns(turns) {
|
|
1046
1223
|
const tbody = $('#turns-body');
|
|
1047
1224
|
const empty = $('#turns-empty');
|
|
@@ -1308,6 +1485,7 @@ var public_default = `<!doctype html>
|
|
|
1308
1485
|
renderCostHero(data.global);
|
|
1309
1486
|
renderMoat(data.global);
|
|
1310
1487
|
renderToolUsage(data.global);
|
|
1488
|
+
renderHotFiles(data.active);
|
|
1311
1489
|
renderTurns(turns);
|
|
1312
1490
|
renderGateMini(gates);
|
|
1313
1491
|
|
|
@@ -1396,7 +1574,10 @@ var public_default = `<!doctype html>
|
|
|
1396
1574
|
`;
|
|
1397
1575
|
|
|
1398
1576
|
// src/dashboard/public/style.css
|
|
1399
|
-
var style_default = '/* Synthra dashboard \xB7 v0.2 \xB7 Cool Marine\n Darkened surfaces; brand blue reserved for hero elements only.\n Layout: top nav + hero strip + 3-column main, fits 1280\xD7720. */\n\n:root {\n /* Core palette */\n --ink: #04081A;\n --navy: #0A1530;\n --navy-2: #122549;\n --deep-blue: #1B3A78;\n --blue: #2C5DB8;\n --blue-bright: #5C8FE6;\n --sky: #9BC2EF;\n --mist: #D7E6F7;\n --bone: #F4F7FC;\n\n /* Text */\n --text: #ECF2FB;\n --text-dim: #A9BBD6;\n --text-mute: #6D80A0;\n\n /* Rules / dividers */\n --rule: rgba(155, 194, 239, .14);\n --rule-2: rgba(155, 194, 239, .06);\n --rule-hover: rgba(155, 194, 239, .28);\n\n /* Surfaces (darker than v0.1.2) */\n --surface-1: rgba(18, 37, 73, .14);\n --surface-2: rgba(18, 37, 73, .22);\n --surface-3: rgba(4, 8, 26, .55);\n\n /* Signal accents (OKLCH shared chroma) */\n --signal-cyan: oklch(78% 0.14 220);\n --signal-amber: oklch(78% 0.14 75);\n --signal-rose: oklch(70% 0.14 20);\n --signal-green: oklch(75% 0.14 155);\n --signal-violet: oklch(72% 0.14 285);\n\n /* Model family colors */\n --c-opus: #FF6338;\n --c-sonnet: #FFB938;\n --c-haiku: #7438FF;\n --c-unknown: #12CBF5;\n\n /* Money */\n --money: var(--signal-green);\n\n /* Type */\n --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;\n --font-serif: "Instrument Serif", "Times New Roman", serif;\n --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;\n}\n\n/* ============================================================\n Reset + base\n ============================================================ */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n}\n\nhtml,\nbody {\n height: 100vh;\n overflow: hidden;\n}\n\nbody {\n background: var(--ink);\n color: var(--text);\n font-family: var(--font-sans);\n font-size: 13px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n text-rendering: optimizeLegibility;\n display: grid;\n grid-template-rows: auto 1fr auto;\n position: relative;\n}\n\n/* Layered backdrop \u2014 quieter */\nbody::before,\nbody::after {\n content: "";\n position: fixed;\n inset: 0;\n pointer-events: none;\n z-index: 0;\n}\n\nbody::before {\n background-image: radial-gradient(circle, rgba(155, 194, 239, .06) 1px, transparent 1.2px);\n background-size: 22px 22px;\n}\n\nbody::after {\n background:\n radial-gradient(60% 40% at 50% 105%, rgba(44, 93, 184, .16) 0%, rgba(10, 21, 48, 0) 65%),\n radial-gradient(30% 25% at 50% 0%, rgba(92, 143, 230, .06) 0%, transparent 70%);\n}\n\nbody>* {\n position: relative;\n z-index: 1;\n}\n\nbutton {\n font: inherit;\n cursor: pointer;\n border: 0;\n background: transparent;\n color: inherit;\n}\n\na {\n color: inherit;\n text-decoration: none;\n}\n\n/* ============================================================\n Top nav\n ============================================================ */\n.topnav {\n display: grid;\n grid-template-columns: 1fr auto 1fr;\n align-items: center;\n height: 52px;\n padding: 0 24px;\n border-bottom: 1px solid var(--rule);\n background: linear-gradient(180deg, rgba(4, 8, 26, .7), rgba(4, 8, 26, .4));\n backdrop-filter: blur(10px);\n}\n\n.brand {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.brand-mark {\n width: 22px;\n height: 22px;\n border-radius: 7px;\n background: radial-gradient(120% 120% at 30% 30%, #6FA6E8 0%, #2C5DB8 45%, #0A1530 100%);\n box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .22), 0 4px 12px -6px #2C5DB8;\n}\n\n.brand-name {\n font-size: 15px;\n font-weight: 600;\n letter-spacing: -0.01em;\n color: var(--mist);\n}\n\n.brand-name em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n color: var(--sky);\n letter-spacing: 0;\n}\n\n.brand-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-left: 6px;\n padding-left: 10px;\n border-left: 1px solid var(--rule);\n}\n\n.top-right {\n display: flex;\n align-items: center;\n gap: 12px;\n grid-column: 2;\n justify-self: center;\n}\n\n.topnav-right {\n grid-column: 3;\n justify-self: end;\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.port-badge {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding: 6px 10px;\n border: 1px solid var(--rule);\n border-radius: 999px;\n background: rgba(4, 8, 26, .55);\n}\n\n.port-badge .mono {\n color: var(--text-dim);\n letter-spacing: 0.04em;\n text-transform: none;\n}\n\n.faq-btn {\n width: 30px;\n height: 30px;\n border-radius: 50%;\n border: 1px solid var(--rule);\n background: rgba(4, 8, 26, .55);\n color: var(--text-dim);\n font-family: var(--font-mono);\n font-size: 13px;\n font-weight: 500;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n transition: background 180ms, border-color 180ms, color 180ms, transform 180ms;\n}\n\n.faq-btn:hover {\n background: rgba(155, 194, 239, .10);\n border-color: var(--rule-hover);\n color: var(--mist);\n transform: translateY(-1px);\n}\n\n.status-pill {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 6px 12px;\n border: 1px solid var(--rule);\n border-radius: 999px;\n background: rgba(4, 8, 26, .55);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-dim);\n transition: border-color 240ms ease;\n}\n\n.status-pill:has(.dot.live) {\n border-color: rgba(155, 194, 239, .45);\n color: var(--mist);\n animation: pill-glow 2.4s ease-in-out infinite;\n}\n\n.status-pill:has(.dot.dead) {\n border-color: rgba(220, 90, 90, .40);\n color: oklch(80% 0.10 20);\n}\n\n@keyframes pill-glow {\n\n 0%,\n 100% {\n box-shadow: 0 0 14px -4px rgba(155, 194, 239, .30), inset 0 0 12px -8px rgba(155, 194, 239, .30);\n }\n\n 50% {\n box-shadow: 0 0 26px -2px rgba(155, 194, 239, .55), inset 0 0 18px -6px rgba(155, 194, 239, .45);\n }\n}\n\n.dot {\n width: 7px;\n height: 7px;\n border-radius: 2px;\n background: var(--text-mute);\n transition: background 200ms;\n}\n\n.dot.live {\n background: var(--signal-cyan);\n animation: dot-pulse 1.8s ease-in-out infinite;\n}\n\n.dot.dead {\n background: var(--signal-rose);\n box-shadow: 0 0 0 3px rgba(220, 90, 90, .10);\n}\n\n@keyframes dot-pulse {\n\n 0%,\n 100% {\n box-shadow:\n 0 0 0 3px rgba(155, 194, 239, .10),\n 0 0 6px rgba(155, 194, 239, .50);\n }\n\n 50% {\n box-shadow:\n 0 0 0 6px rgba(155, 194, 239, .05),\n 0 0 14px rgba(155, 194, 239, .90);\n }\n}\n\n/* ============================================================\n Hero strip\n ============================================================ */\n.hero-strip {\n display: flex;\n align-items: center;\n gap: 24px;\n padding: 14px 24px;\n border-bottom: 1px solid var(--rule);\n background: linear-gradient(90deg, rgba(27, 58, 120, .10) 0%, rgba(4, 8, 26, 0) 100%);\n position: relative;\n overflow: hidden;\n}\n\n.hero-spacer {\n flex: 1;\n}\n\n.date-block {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.d-day {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 38px;\n line-height: 1;\n letter-spacing: -0.04em;\n color: var(--mist);\n}\n\n.d-rest {\n display: flex;\n flex-direction: column;\n gap: 2px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-dim);\n}\n\n.d-rest .d-mute {\n color: var(--text-mute);\n}\n\n.active-block {\n display: flex;\n flex-direction: column;\n gap: 2px;\n text-align: right;\n max-width: 360px;\n overflow: hidden;\n}\n\n.ab-label {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.ab-value {\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--mist);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 360px;\n}\n\n/* ============================================================\n Main grid\n ============================================================ */\n.grid-main {\n display: grid;\n grid-template-columns: 260px 1fr 340px;\n gap: 16px;\n padding: 16px 24px;\n min-height: 0;\n z-index: 10;\n}\n\n.col-left,\n.col-center,\n.col-right {\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-height: 0;\n}\n\n/* ============================================================\n Panels / cards \u2014 darker\n ============================================================ */\n.panel,\n.card {\n position: relative;\n border: 1px solid var(--rule);\n border-radius: 14px;\n background: var(--surface-1);\n padding: 14px 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-height: 0;\n transition: border-color 180ms ease, background 180ms ease;\n}\n\n.card.has-tooltip {\n cursor: help;\n}\n\n.card.has-tooltip:hover {\n border-color: var(--rule-hover);\n background: var(--surface-2);\n}\n\n.card-head {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 12px;\n}\n\n.card-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.card-eyebrow em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 12px;\n color: var(--sky);\n letter-spacing: 0;\n text-transform: none;\n}\n\n.card-meta {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n/* Legend panel */\n.panel {\n padding: 14px 14px 16px;\n gap: 14px;\n flex-shrink: 0;\n}\n\n.p-head {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding-bottom: 10px;\n border-bottom: 1px solid var(--rule-2);\n}\n\n.p-section {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.p-section+.p-section {\n padding-top: 12px;\n border-top: 1px solid var(--rule-2);\n}\n\n.ps-head {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-bottom: 4px;\n}\n\n.check {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 3px 6px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-dim);\n letter-spacing: 0.02em;\n}\n\nbutton.check {\n border: 0;\n background: transparent;\n width: 100%;\n text-align: left;\n}\n\n.check-clickable {\n cursor: pointer;\n border-radius: 6px;\n padding: 5px 6px;\n transition: background 140ms, color 140ms, transform 140ms;\n}\n\n.check-clickable .pf-arrow {\n margin-left: auto;\n color: var(--text-mute);\n font-family: var(--font-mono);\n font-size: 12px;\n transition: color 140ms, transform 140ms;\n}\n\n.check-clickable:hover {\n background: rgba(155, 194, 239, .07);\n color: var(--mist);\n}\n\n.check-clickable:hover .pf-arrow {\n color: var(--sky);\n transform: translateX(2px);\n}\n\n.dot-sq {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n background: var(--text-mute);\n flex-shrink: 0;\n}\n\n.dot-sq.opus {\n background: var(--c-opus);\n}\n\n.dot-sq.sonnet {\n background: var(--c-sonnet);\n}\n\n.dot-sq.haiku {\n background: var(--c-haiku);\n}\n\n.dot-sq.unknown {\n background: var(--c-unknown);\n}\n\n.proj-filter {\n display: flex;\n flex-direction: column;\n gap: 1px;\n max-height: 90px;\n overflow-y: auto;\n}\n\n.pf-name {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 140px;\n}\n\n/* ============================================================\n Donut card (model usage)\n ============================================================ */\n.donut-card {\n flex: 1;\n gap: 10px;\n}\n\n.donut-wrap {\n position: relative;\n width: 140px;\n height: 140px;\n margin: 4px auto 0;\n}\n\n.donut {\n width: 100%;\n height: 100%;\n display: block;\n}\n\n.donut-track {\n fill: none;\n stroke: rgba(155, 194, 239, .07);\n stroke-width: 14;\n}\n\n.donut-seg {\n transition: stroke-dashoffset 400ms ease, stroke-dasharray 400ms ease;\n}\n\n.donut-center {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n pointer-events: none;\n}\n\n.donut-total {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 26px;\n letter-spacing: -0.02em;\n color: var(--mist);\n line-height: 1;\n}\n\n.donut-total-k {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-top: 4px;\n}\n\n.donut-legend {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 6px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n}\n\n.dl-row {\n display: grid;\n grid-template-columns: auto 1fr auto;\n align-items: center;\n gap: 8px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-dim);\n}\n\n.dl-dot {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n}\n\n.dl-name {\n color: var(--text-dim);\n}\n\n.dl-pct {\n color: var(--mist);\n font-weight: 500;\n}\n\n/* ============================================================\n Center column \u2014 Metric strip (no card chrome, divider-separated)\n ============================================================ */\n.metric-strip {\n display: grid;\n grid-template-columns: repeat(5, 1fr);\n border: 1px solid var(--rule);\n border-radius: 14px;\n background: var(--surface-1);\n overflow: hidden;\n flex-shrink: 0;\n}\n\n.metric-item {\n padding: 14px 18px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n cursor: help;\n border-right: 1px solid var(--rule-2);\n transition: background 200ms ease;\n min-width: 0;\n}\n.metric-item:last-child { border-right: 0; }\n.metric-item:hover { background: var(--surface-2); }\n\n.m-label {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.m-value {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 26px;\n letter-spacing: -0.025em;\n color: var(--mist);\n line-height: 1;\n}\n\n.m-value em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n color: var(--sky);\n letter-spacing: -0.005em;\n}\n\n/* ============================================================\n Savings card\n ============================================================ */\n.card.savings {\n flex-shrink: 0;\n gap: 12px;\n background:\n linear-gradient(180deg, rgba(123, 255, 199, .05) 0%, rgba(4, 8, 26, .20) 50%),\n var(--surface-1);\n border-color: rgba(123, 255, 199, .18);\n}\n\n.card.savings:hover {\n border-color: rgba(123, 255, 199, .32);\n}\n\n.savings-body {\n display: grid;\n grid-template-columns: auto 1fr;\n align-items: center;\n gap: 18px;\n}\n\n.savings-figure {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.savings-money {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 34px;\n letter-spacing: -0.035em;\n line-height: 1;\n color: var(--money);\n}\n\n.savings-tokens {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-top: 4px;\n}\n\n.savings-bar {\n position: relative;\n height: 8px;\n border-radius: 999px;\n overflow: hidden;\n background: var(--surface-3);\n display: flex;\n}\n\n.savings-actual {\n height: 100%;\n background: rgba(215, 230, 247, .55);\n transition: width 500ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.savings-saved {\n height: 100%;\n background: var(--signal-green);\n transition: width 500ms cubic-bezier(0.16, 1, 0.3, 1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12);\n}\n\n.savings-legend {\n grid-column: 2;\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 24px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.08em;\n color: var(--text-mute);\n margin-top: 8px;\n}\n\n.sl-row {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.sl-row b {\n color: var(--mist);\n font-weight: 500;\n letter-spacing: 0.04em;\n}\n\n.sl-dot {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n}\n\n.sl-dot.actual {\n background: var(--mist);\n}\n\n.sl-dot.saved {\n background: var(--signal-green);\n}\n\n/* ============================================================\n Recent turns table\n ============================================================ */\n.turns-card {\n flex: 1;\n padding: 0;\n overflow: hidden;\n}\n\n.turns-card .card-head {\n padding: 14px 16px 10px;\n border-bottom: 1px solid var(--rule-2);\n}\n\n.turns-scroll {\n flex: 1;\n overflow-y: auto;\n min-height: 0;\n}\n\n.turns-table {\n width: 100%;\n border-collapse: collapse;\n}\n\n.turns-table thead th {\n position: sticky;\n top: 0;\n background: var(--ink);\n padding: 9px 16px;\n text-align: left;\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n font-weight: 500;\n border-bottom: 1px solid var(--rule);\n z-index: 1;\n}\n\n.turns-table thead th.num {\n text-align: right;\n}\n\n.turns-table tbody td {\n padding: 8px 16px;\n border-bottom: 1px solid var(--rule-2);\n color: var(--text-dim);\n font-size: 12px;\n}\n\n.turns-table tbody td.num {\n text-align: right;\n font-family: var(--font-mono);\n}\n\n.turns-table tbody td.cost {\n color: var(--money);\n font-family: var(--font-mono);\n font-weight: 500;\n}\n\n.turns-table tbody td.ts {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n}\n\n.turns-table tbody td.proj {\n color: var(--mist);\n}\n\n.turns-table tbody tr:hover {\n background: rgba(155, 194, 239, .03);\n}\n\n.turns-table tbody tr:last-child td {\n border-bottom: 0;\n}\n\n/* Model pills */\n.model-pill {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 2px 8px;\n border-radius: 999px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.04em;\n border: 1px solid var(--rule);\n background: rgba(4, 8, 26, .5);\n color: var(--mist);\n}\n\n.model-pill .sq {\n width: 6px;\n height: 6px;\n border-radius: 2px;\n background: var(--text-mute);\n}\n\n.model-pill.opus {\n color: #FF8A66;\n border-color: rgba(255, 99, 56, .32);\n background: rgba(255, 99, 56, .07);\n}\n\n.model-pill.opus .sq {\n background: var(--c-opus);\n}\n\n.model-pill.sonnet {\n color: #FFC766;\n border-color: rgba(255, 185, 56, .32);\n background: rgba(255, 185, 56, .07);\n}\n\n.model-pill.sonnet .sq {\n background: var(--c-sonnet);\n}\n\n.model-pill.haiku {\n color: #A878FF;\n border-color: rgba(116, 56, 255, .42);\n background: rgba(116, 56, 255, .10);\n}\n\n.model-pill.haiku .sq {\n background: var(--c-haiku);\n}\n\n.model-pill.unknown {\n color: #5BDDF7;\n border-color: rgba(18, 203, 245, .32);\n background: rgba(18, 203, 245, .07);\n font-style: italic;\n}\n\n.model-pill.unknown .sq {\n background: var(--c-unknown);\n}\n\n/* ============================================================\n Right column \u2014 Cost hero\n ============================================================ */\n.cost-hero {\n position: relative;\n overflow: hidden;\n background:\n radial-gradient(120% 80% at 50% 110%, rgba(44, 93, 184, .18) 0%, rgba(4, 8, 26, .20) 60%),\n var(--surface-1);\n padding: 16px 18px 18px;\n gap: 10px;\n flex-shrink: 0;\n}\n\n.big-money {\n position: relative;\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 42px;\n letter-spacing: -0.035em;\n line-height: 1;\n color: var(--money);\n margin-top: 2px;\n}\n\n.big-money em {\n font-family: inherit;\n font-style: normal;\n font-weight: inherit;\n color: inherit;\n letter-spacing: inherit;\n opacity: 1;\n}\n\n.cost-sub {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 6px;\n margin-top: 4px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n}\n\n.cs-row {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n font-family: var(--font-mono);\n font-size: 11px;\n}\n\n.cs-k {\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.cs-v {\n color: var(--mist);\n}\n\n/* ============================================================\n Moat card\n ============================================================ */\n.moat {\n flex: 1;\n gap: 8px;\n}\n\n.moat-value {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 34px;\n letter-spacing: -0.03em;\n line-height: 1;\n color: var(--mist);\n margin-top: 2px;\n}\n\n.moat-value em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 18px;\n color: var(--sky);\n letter-spacing: 0;\n margin-left: 6px;\n}\n\n.gate-mini {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 6px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n.gate-row {\n display: grid;\n grid-template-columns: auto auto 1fr;\n align-items: center;\n gap: 8px;\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--text-dim);\n padding: 3px 0;\n}\n\n.gate-row .g-ts {\n color: var(--text-mute);\n font-size: 9px;\n min-width: 38px;\n}\n\n.gate-row .g-decision {\n font-size: 9px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: 999px;\n}\n\n.gate-row .g-decision.block {\n color: var(--signal-rose);\n background: rgba(220, 90, 90, .06);\n}\n\n.gate-row .g-decision.allow {\n color: var(--text-mute);\n background: rgba(155, 194, 239, .03);\n}\n\n.gate-row .g-q {\n color: var(--mist);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* ============================================================\n Tooltips\n ============================================================ */\n.has-tooltip {\n position: relative;\n}\n\n/* Global JS-positioned tooltip \u2014 viewport-clamped */\n.global-tooltip {\n position: fixed;\n top: 0;\n left: 0;\n background: linear-gradient(180deg, rgba(18, 37, 73, .98), rgba(10, 21, 48, .98));\n color: var(--mist);\n border: 1px solid var(--rule-hover);\n border-radius: 12px;\n padding: 14px 16px;\n font-family: var(--font-sans);\n font-size: 15px;\n font-weight: 400;\n text-transform: none;\n letter-spacing: 0;\n white-space: normal;\n width: 320px;\n max-width: calc(100vw - 24px);\n text-align: left;\n line-height: 1.55;\n box-shadow: 0 16px 36px rgba(0, 0, 0, .7);\n backdrop-filter: blur(10px);\n z-index: 99999;\n opacity: 0;\n pointer-events: none;\n transform: translateY(6px);\n transition: opacity 180ms ease, transform 180ms ease;\n}\n\n.global-tooltip.on {\n opacity: 1;\n transform: translateY(0);\n}\n\n/* ============================================================\n Footer\n ============================================================ */\n.foot {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 8px 24px;\n border-top: 1px solid var(--rule);\n background: linear-gradient(0deg, rgba(4, 8, 26, .7), rgba(4, 8, 26, .4));\n backdrop-filter: blur(10px);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.foot em {\n font-family: var(--font-serif);\n font-style: italic;\n text-transform: none;\n letter-spacing: 0;\n color: var(--sky);\n font-size: 12px;\n}\n\n.foot .mono {\n color: var(--text-dim);\n text-transform: none;\n letter-spacing: 0.04em;\n}\n\n/* ============================================================\n Empty state\n ============================================================ */\n.empty {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.06em;\n color: var(--text-mute);\n text-align: center;\n padding: 16px 8px;\n font-style: italic;\n text-transform: none;\n}\n\n/* Scrollbar styling */\n.turns-scroll::-webkit-scrollbar,\n.proj-chart::-webkit-scrollbar,\n.gate-mini::-webkit-scrollbar {\n width: 6px;\n}\n\n.turns-scroll::-webkit-scrollbar-thumb,\n.proj-chart::-webkit-scrollbar-thumb,\n.gate-mini::-webkit-scrollbar-thumb {\n background: var(--rule);\n border-radius: 999px;\n}\n\n.turns-scroll::-webkit-scrollbar-track,\n.proj-chart::-webkit-scrollbar-track,\n.gate-mini::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.hidden {\n display: none !important;\n}\n\n/* ============================================================\n Staggered cascade on first paint (one-time, MOTION 6)\n ============================================================ */\n@keyframes cascade-in {\n from { opacity: 0; transform: translateY(10px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n .col-left > *,\n .col-center > *,\n .col-right > * {\n opacity: 0;\n animation: cascade-in 520ms cubic-bezier(0.16, 1, 0.3, 1) forwards;\n will-change: transform, opacity;\n }\n .col-left > *:nth-child(1) { animation-delay: 0ms; }\n .col-left > *:nth-child(2) { animation-delay: 120ms; }\n .col-center > *:nth-child(1) { animation-delay: 40ms; }\n .col-center > *:nth-child(2) { animation-delay: 140ms; }\n .col-center > *:nth-child(3) { animation-delay: 240ms; }\n .col-right > *:nth-child(1) { animation-delay: 80ms; }\n .col-right > *:nth-child(2) { animation-delay: 200ms; }\n .col-right > *:nth-child(3) { animation-delay: 320ms; }\n\n /* Clear will-change after animation completes */\n .col-left > *,\n .col-center > *,\n .col-right > * {\n animation-fill-mode: forwards;\n }\n}\n\n/* ============================================================\n Source / basis annotations\n ============================================================ */\n.card-source {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.08em;\n text-transform: lowercase;\n color: var(--text-mute);\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin-top: auto;\n padding-top: 8px;\n border-top: 1px solid var(--rule-2);\n width: 100%;\n}\n\n.src-badge {\n font-family: var(--font-mono);\n font-size: 8px;\n font-weight: 500;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: 4px;\n flex-shrink: 0;\n}\n\n.src-badge.verified {\n color: var(--signal-green);\n background: rgba(123, 255, 199, .08);\n border: 1px solid rgba(123, 255, 199, .25);\n}\n\n.src-badge.estimated,\n.src-badge.estimated.floor {\n color: var(--signal-amber);\n background: rgba(255, 185, 56, .10);\n border: 1px solid rgba(255, 185, 56, .30);\n}\n\n.src-badge.priced {\n color: var(--signal-cyan);\n background: rgba(155, 194, 239, .08);\n border: 1px solid rgba(155, 194, 239, .25);\n}\n\n/* Eyebrow that contains a badge */\n.card-eyebrow .src-badge {\n margin-left: 4px;\n}\n\n/* Savings audit row \u2014 live formula reveal */\n.savings-audit {\n margin-top: 10px;\n padding: 10px 12px;\n border: 1px dashed rgba(255, 185, 56, .25);\n border-radius: 8px;\n background: rgba(255, 185, 56, .04);\n font-family: var(--font-mono);\n font-size: 10.5px;\n letter-spacing: 0.04em;\n color: var(--text-mute);\n text-align: center;\n}\n\n.savings-audit b {\n color: var(--text-dim);\n font-weight: 500;\n}\n\n.savings-audit .audit-result {\n color: var(--money);\n}\n\n/* ============================================================\n FAQ dialog\n ============================================================ */\n.dialog.dialog-faq {\n max-width: min(80vw, 1100px);\n width: 100%;\n max-height: 86vh;\n display: flex;\n flex-direction: column;\n padding: 28px 32px 24px;\n gap: 6px;\n}\n\n.dialog.dialog-faq .dialog-path {\n margin-bottom: 4px;\n word-break: normal;\n overflow-wrap: anywhere;\n}\n\n.faq-content {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n margin-top: 18px;\n padding-right: 8px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.faq-content::-webkit-scrollbar {\n width: 6px;\n}\n\n.faq-content::-webkit-scrollbar-thumb {\n background: var(--rule);\n border-radius: 999px;\n}\n\n.faq-content::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.faq-content details {\n border: 1px solid var(--rule);\n border-radius: 12px;\n background: var(--surface-1);\n overflow: hidden;\n transition: background 180ms, border-color 180ms;\n flex-shrink: 0;\n}\n\n.faq-content details:hover {\n border-color: rgba(155, 194, 239, .22);\n}\n\n.faq-content details[open] {\n background: var(--surface-2);\n border-color: var(--rule-hover);\n}\n\n.faq-content summary {\n cursor: pointer;\n padding: 14px 20px;\n font-family: var(--font-sans);\n font-size: 14px;\n font-weight: 500;\n color: var(--mist);\n list-style: none;\n display: flex;\n align-items: center;\n gap: 12px;\n user-select: none;\n}\n\n.faq-content summary::-webkit-details-marker {\n display: none;\n}\n\n.faq-content summary::before {\n content: "\u203A";\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n color: var(--text-mute);\n font-family: var(--font-mono);\n font-size: 14px;\n transition: transform 220ms ease, color 220ms ease;\n}\n\n.faq-content details[open] summary::before {\n transform: rotate(90deg);\n color: var(--sky);\n}\n\n.faq-content .faq-body {\n padding: 0 22px 20px 46px;\n color: var(--text-dim);\n font-size: 13.5px;\n line-height: 1.7;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.faq-content .faq-body p {\n margin: 0;\n}\n\n.faq-content .faq-body ul {\n margin: 0;\n padding-left: 20px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.faq-content .faq-body li {\n margin: 0;\n}\n\n.faq-content .faq-body b,\n.faq-content .faq-body strong {\n color: var(--mist);\n font-weight: 500;\n}\n\n.faq-content .faq-body code {\n font-family: var(--font-mono);\n font-size: 12px;\n background: rgba(155, 194, 239, .08);\n padding: 2px 6px;\n border-radius: 4px;\n color: var(--mist);\n border: 1px solid rgba(155, 194, 239, .12);\n word-break: break-word;\n}\n\n.faq-content .faq-body a {\n color: var(--blue-bright);\n text-decoration: underline;\n text-decoration-color: rgba(92, 143, 230, .40);\n text-underline-offset: 3px;\n transition: color 140ms, text-decoration-color 140ms;\n}\n\n.faq-content .faq-body a:hover {\n color: var(--mist);\n text-decoration-color: var(--sky);\n}\n\n.faq-content .faq-body table {\n width: 100%;\n border-collapse: collapse;\n margin: 4px 0;\n font-size: 13px;\n table-layout: fixed;\n}\n\n.faq-content .faq-body thead td {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding-bottom: 6px;\n border-bottom: 1px solid var(--rule);\n font-weight: 500;\n}\n\n.faq-content .faq-body td {\n padding: 9px 10px;\n border-bottom: 1px solid var(--rule-2);\n vertical-align: top;\n word-break: break-word;\n}\n\n.faq-content .faq-body tr:last-child td {\n border-bottom: 0;\n}\n\n.faq-content .faq-body td:first-child {\n color: var(--text-dim);\n width: 38%;\n}\n\n.faq-content .faq-body td:first-child code {\n font-size: 11.5px;\n}\n\n.faq-content .faq-body .formula-box {\n font-family: var(--font-mono);\n font-size: 12.5px;\n background: rgba(255, 185, 56, .06);\n padding: 12px 14px;\n border-radius: 8px;\n border: 1px dashed rgba(255, 185, 56, .30);\n color: var(--mist);\n letter-spacing: 0.02em;\n}\n\n.faq-content .faq-body .link-list {\n list-style: none;\n padding-left: 0;\n}\n\n.faq-content .faq-body .link-list li {\n padding-left: 18px;\n position: relative;\n}\n\n.faq-content .faq-body .link-list li::before {\n content: "\u203A";\n position: absolute;\n left: 0;\n color: var(--sky);\n font-family: var(--font-mono);\n}\n\n.faq-content .faq-body .warning {\n margin-top: 14px;\n padding: 12px 14px;\n background: rgba(255, 185, 56, .06);\n border: 1px solid rgba(255, 185, 56, .25);\n border-left: 3px solid var(--signal-amber);\n border-radius: 8px;\n font-size: 12.5px;\n color: var(--text-dim);\n}\n\n.faq-content .faq-body .warning .icon {\n color: var(--signal-amber);\n margin-right: 8px;\n font-weight: 500;\n}\n\n/* ============================================================\n Project dialog\n ============================================================ */\n.dialog-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(4, 8, 26, .78);\n backdrop-filter: blur(10px);\n z-index: 10000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n animation: dlg-fade 180ms ease;\n}\n\n@keyframes dlg-fade {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n}\n\n.dialog {\n position: relative;\n width: 100%;\n max-width: 520px;\n background:\n radial-gradient(120% 80% at 50% 0%, rgba(44, 93, 184, .22) 0%, rgba(4, 8, 26, .20) 60%),\n linear-gradient(180deg, rgba(18, 37, 73, .88) 0%, rgba(10, 21, 48, .96) 100%);\n border: 1px solid var(--rule-hover);\n border-radius: 18px;\n padding: 28px 32px 32px;\n box-shadow:\n 0 30px 80px -20px rgba(0, 0, 0, .7),\n inset 0 1px 0 rgba(255, 255, 255, .04);\n animation: dlg-rise 220ms cubic-bezier(.2, .7, .2, 1);\n}\n\n@keyframes dlg-rise {\n from {\n opacity: 0;\n transform: translateY(8px) scale(.98);\n }\n\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n.dialog-close {\n position: absolute;\n top: 14px;\n right: 14px;\n width: 30px;\n height: 30px;\n border-radius: 50%;\n color: var(--text-mute);\n font-size: 22px;\n line-height: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 180ms, color 180ms;\n}\n\n.dialog-close:hover {\n background: rgba(155, 194, 239, .10);\n color: var(--mist);\n}\n\n.dialog-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-bottom: 10px;\n}\n\n.dialog-eyebrow em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 12px;\n color: var(--sky);\n letter-spacing: 0;\n text-transform: none;\n}\n\n.dialog-name {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 28px;\n letter-spacing: -0.025em;\n color: var(--mist);\n line-height: 1.1;\n}\n\n.dialog-path {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n margin-top: 6px;\n word-break: break-all;\n}\n\n.dialog-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 18px 24px;\n margin-top: 22px;\n padding-top: 20px;\n border-top: 1px solid var(--rule-2);\n}\n\n.dg-cell {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.dg-k {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.dg-v {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 22px;\n letter-spacing: -0.02em;\n color: var(--mist);\n line-height: 1;\n}\n\n.dg-v.money {\n color: var(--money);\n}\n\n.dg-v-sm {\n font-size: 13px;\n font-family: var(--font-mono);\n font-weight: 400;\n color: var(--text-dim);\n letter-spacing: 0;\n}\n\n/* ============================================================\n v0.3 visual refresh\n - merged model-family legend into donut (count column)\n - Projects -> colored bar chart\n - elevated Savings card\n ============================================================ */\n\n/* Left column sizing: donut natural height, projects fills + scrolls */\n.donut-card { flex: 0 0 auto; }\n\n/* Donut legend now carries a count column */\n.dl-row {\n grid-template-columns: auto 1fr auto auto;\n gap: 8px;\n padding: 1px 0;\n}\n.dl-count {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--text-mute);\n letter-spacing: 0.04em;\n font-variant-numeric: tabular-nums;\n}\n.dl-pct {\n min-width: 30px;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* ---- Projects bar chart ---- */\n.projects-card {\n flex: 1 1 auto;\n min-height: 0;\n gap: 10px;\n}\n.proj-chart {\n display: flex;\n flex-direction: column;\n gap: 9px;\n overflow-y: auto;\n min-height: 0;\n flex: 1;\n padding-right: 2px;\n}\n.proj-row {\n display: flex;\n flex-direction: column;\n gap: 7px;\n width: 100%;\n text-align: left;\n padding: 8px;\n border-radius: 9px;\n background: transparent;\n border: 0;\n cursor: pointer;\n transition: background 150ms ease;\n}\n.proj-row:hover { background: rgba(155, 194, 239, .055); }\n.pr-top {\n display: grid;\n grid-template-columns: auto 1fr auto auto;\n align-items: center;\n gap: 9px;\n}\n.pr-dot {\n width: 9px;\n height: 9px;\n border-radius: 3px;\n background: var(--pc, var(--sky));\n box-shadow: 0 0 9px -1px var(--pc, var(--sky));\n flex-shrink: 0;\n}\n.pr-name {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--text-dim);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n letter-spacing: 0.01em;\n transition: color 150ms ease;\n}\n.proj-row:hover .pr-name { color: var(--mist); }\n.pr-turns {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--mist);\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.02em;\n}\n.pr-arrow {\n font-family: var(--font-mono);\n font-size: 13px;\n color: var(--text-mute);\n transition: color 150ms ease, transform 150ms ease;\n}\n.proj-row:hover .pr-arrow {\n color: var(--pc, var(--sky));\n transform: translateX(2px);\n}\n.pr-bar {\n position: relative;\n height: 5px;\n border-radius: 999px;\n background: var(--surface-3);\n overflow: hidden;\n}\n.pr-fill {\n display: block;\n height: 100%;\n border-radius: 999px;\n background: linear-gradient(90deg,\n color-mix(in oklch, var(--pc, var(--sky)) 45%, transparent) 0%,\n var(--pc, var(--sky)) 100%);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18);\n transition: width 640ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n/* ---- Elevated Savings card (priority focus) ---- */\n.card.savings {\n background:\n radial-gradient(120% 140% at 10% -10%, rgba(123, 255, 199, .14) 0%, rgba(4, 8, 26, .08) 44%),\n linear-gradient(180deg, rgba(123, 255, 199, .05) 0%, rgba(4, 8, 26, .20) 52%),\n var(--surface-1);\n border-color: rgba(123, 255, 199, .24);\n box-shadow:\n inset 0 1px 0 rgba(123, 255, 199, .08),\n 0 20px 46px -30px rgba(123, 255, 199, .55);\n}\n.card.savings:hover {\n border-color: rgba(123, 255, 199, .38);\n}\n.savings-money {\n font-size: 40px;\n text-shadow: 0 0 26px rgba(123, 255, 199, .22);\n}\n.savings-bar { height: 9px; }\n.savings-saved {\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, .18),\n 0 0 12px -2px var(--signal-green);\n}\n\n/* Project dialog: name gets a project-colored accent dot */\n.dialog-name.has-accent {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.dialog-name.has-accent::before {\n content: "";\n width: 12px;\n height: 12px;\n border-radius: 4px;\n background: var(--pc, var(--sky));\n box-shadow: 0 0 12px -1px var(--pc, var(--sky));\n flex-shrink: 0;\n}\n\n\n/* ============================================================\n v0.3.1 \u2014 date + active project folded into the top nav\n ============================================================ */\n\n/* Let the right cluster shrink so the active path can ellipsize */\n.topnav-right { min-width: 0; }\n\n/* Compact date beside the brand */\n.nav-date {\n display: inline-flex;\n align-items: baseline;\n gap: 6px;\n margin-left: 8px;\n padding-left: 12px;\n border-left: 1px solid var(--rule);\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n white-space: nowrap;\n}\n.nav-date .nd-day { color: var(--mist); font-weight: 500; }\n.nav-date .nd-weekday { color: var(--text-dim); }\n.nav-date .nd-month { color: var(--text-mute); }\n\n/* Active project, compact, tail-truncated */\n.nav-active {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n max-width: 300px;\n padding-right: 12px;\n margin-right: 2px;\n border-right: 1px solid var(--rule);\n cursor: help;\n}\n.na-label {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n flex-shrink: 0;\n}\n.na-value {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--mist);\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n /* keep the project folder (tail) visible, ellipsize the drive prefix */\n direction: rtl;\n text-align: left;\n}\n\n/* Tighten nav on narrow widths */\n@media (max-width: 1100px) {\n .nav-active { max-width: 200px; }\n .nav-date .nd-month { display: none; }\n}\n\n\n/* Column headers signal they are hover-explainable */\n.turns-table thead th.has-tooltip { cursor: help; }\n.turns-table thead th.has-tooltip:hover { color: var(--text-dim); }\n\n/* ---- Recent-turns pager ---- */\n.turns-pager {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 10px;\n padding: 8px 2px 0;\n margin-top: 6px;\n border-top: 1px solid var(--rule-2);\n}\n.turns-pager.hidden { display: none; }\n.turns-pager button {\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.04em;\n color: var(--text-dim);\n background: rgba(4, 8, 26, .55);\n border: 1px solid var(--rule);\n border-radius: 7px;\n padding: 4px 10px;\n cursor: pointer;\n transition: background 150ms, border-color 150ms, color 150ms, transform 150ms;\n}\n.turns-pager button:hover:not(:disabled) {\n background: rgba(155, 194, 239, .10);\n border-color: var(--rule-hover);\n color: var(--mist);\n transform: translateY(-1px);\n}\n.turns-pager button:disabled {\n opacity: .35;\n cursor: default;\n}\n#turns-page-label {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n letter-spacing: 0.04em;\n font-variant-numeric: tabular-nums;\n min-width: 84px;\n text-align: center;\n}\n';
|
|
1577
|
+
var style_default = '/* Synthra dashboard \xB7 v0.2 \xB7 Cool Marine\n Darkened surfaces; brand blue reserved for hero elements only.\n Layout: top nav + hero strip + 3-column main, fits 1280\xD7720. */\n\n:root {\n /* Core palette */\n --ink: #04081A;\n --navy: #0A1530;\n --navy-2: #122549;\n --deep-blue: #1B3A78;\n --blue: #2C5DB8;\n --blue-bright: #5C8FE6;\n --sky: #9BC2EF;\n --mist: #D7E6F7;\n --bone: #F4F7FC;\n\n /* Text */\n --text: #ECF2FB;\n --text-dim: #A9BBD6;\n --text-mute: #6D80A0;\n\n /* Rules / dividers */\n --rule: rgba(155, 194, 239, .14);\n --rule-2: rgba(155, 194, 239, .06);\n --rule-hover: rgba(155, 194, 239, .28);\n\n /* Surfaces (darker than v0.1.2) */\n --surface-1: rgba(18, 37, 73, .14);\n --surface-2: rgba(18, 37, 73, .22);\n --surface-3: rgba(4, 8, 26, .55);\n\n /* Signal accents (OKLCH shared chroma) */\n --signal-cyan: oklch(78% 0.14 220);\n --signal-amber: oklch(78% 0.14 75);\n --signal-rose: oklch(70% 0.14 20);\n --signal-green: oklch(75% 0.14 155);\n --signal-violet: oklch(72% 0.14 285);\n\n /* Model family colors */\n --c-opus: #FF6338;\n --c-sonnet: #FFB938;\n --c-haiku: #7438FF;\n --c-unknown: #12CBF5;\n\n /* Money */\n --money: var(--signal-green);\n\n /* Type */\n --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;\n --font-serif: "Instrument Serif", "Times New Roman", serif;\n --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;\n}\n\n/* ============================================================\n Reset + base\n ============================================================ */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n}\n\nhtml,\nbody {\n height: 100vh;\n overflow: hidden;\n}\n\nbody {\n background: var(--ink);\n color: var(--text);\n font-family: var(--font-sans);\n font-size: 13px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n text-rendering: optimizeLegibility;\n display: grid;\n grid-template-rows: auto 1fr auto;\n position: relative;\n}\n\n/* Layered backdrop \u2014 quieter */\nbody::before,\nbody::after {\n content: "";\n position: fixed;\n inset: 0;\n pointer-events: none;\n z-index: 0;\n}\n\nbody::before {\n background-image: radial-gradient(circle, rgba(155, 194, 239, .06) 1px, transparent 1.2px);\n background-size: 22px 22px;\n}\n\nbody::after {\n background:\n radial-gradient(60% 40% at 50% 105%, rgba(44, 93, 184, .16) 0%, rgba(10, 21, 48, 0) 65%),\n radial-gradient(30% 25% at 50% 0%, rgba(92, 143, 230, .06) 0%, transparent 70%);\n}\n\nbody>* {\n position: relative;\n z-index: 1;\n}\n\nbutton {\n font: inherit;\n cursor: pointer;\n border: 0;\n background: transparent;\n color: inherit;\n}\n\na {\n color: inherit;\n text-decoration: none;\n}\n\n/* ============================================================\n Top nav\n ============================================================ */\n.topnav {\n display: grid;\n grid-template-columns: 1fr auto 1fr;\n align-items: center;\n height: 52px;\n padding: 0 24px;\n border-bottom: 1px solid var(--rule);\n background: linear-gradient(180deg, rgba(4, 8, 26, .7), rgba(4, 8, 26, .4));\n backdrop-filter: blur(10px);\n}\n\n.brand {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.brand-mark {\n width: 22px;\n height: 22px;\n border-radius: 7px;\n background: radial-gradient(120% 120% at 30% 30%, #6FA6E8 0%, #2C5DB8 45%, #0A1530 100%);\n box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .22), 0 4px 12px -6px #2C5DB8;\n}\n\n.brand-name {\n font-size: 15px;\n font-weight: 600;\n letter-spacing: -0.01em;\n color: var(--mist);\n}\n\n.brand-name em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n color: var(--sky);\n letter-spacing: 0;\n}\n\n.brand-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-left: 6px;\n padding-left: 10px;\n border-left: 1px solid var(--rule);\n}\n\n.top-right {\n display: flex;\n align-items: center;\n gap: 12px;\n grid-column: 2;\n justify-self: center;\n}\n\n.topnav-right {\n grid-column: 3;\n justify-self: end;\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.port-badge {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding: 6px 10px;\n border: 1px solid var(--rule);\n border-radius: 999px;\n background: rgba(4, 8, 26, .55);\n}\n\n.port-badge .mono {\n color: var(--text-dim);\n letter-spacing: 0.04em;\n text-transform: none;\n}\n\n.faq-btn {\n width: 30px;\n height: 30px;\n border-radius: 50%;\n border: 1px solid var(--rule);\n background: rgba(4, 8, 26, .55);\n color: var(--text-dim);\n font-family: var(--font-mono);\n font-size: 13px;\n font-weight: 500;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n transition: background 180ms, border-color 180ms, color 180ms, transform 180ms;\n}\n\n.faq-btn:hover {\n background: rgba(155, 194, 239, .10);\n border-color: var(--rule-hover);\n color: var(--mist);\n transform: translateY(-1px);\n}\n\n.status-pill {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 6px 12px;\n border: 1px solid var(--rule);\n border-radius: 999px;\n background: rgba(4, 8, 26, .55);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-dim);\n transition: border-color 240ms ease;\n}\n\n.status-pill:has(.dot.live) {\n border-color: rgba(155, 194, 239, .45);\n color: var(--mist);\n animation: pill-glow 2.4s ease-in-out infinite;\n}\n\n.status-pill:has(.dot.dead) {\n border-color: rgba(220, 90, 90, .40);\n color: oklch(80% 0.10 20);\n}\n\n@keyframes pill-glow {\n\n 0%,\n 100% {\n box-shadow: 0 0 14px -4px rgba(155, 194, 239, .30), inset 0 0 12px -8px rgba(155, 194, 239, .30);\n }\n\n 50% {\n box-shadow: 0 0 26px -2px rgba(155, 194, 239, .55), inset 0 0 18px -6px rgba(155, 194, 239, .45);\n }\n}\n\n.dot {\n width: 7px;\n height: 7px;\n border-radius: 2px;\n background: var(--text-mute);\n transition: background 200ms;\n}\n\n.dot.live {\n background: var(--signal-cyan);\n animation: dot-pulse 1.8s ease-in-out infinite;\n}\n\n.dot.dead {\n background: var(--signal-rose);\n box-shadow: 0 0 0 3px rgba(220, 90, 90, .10);\n}\n\n@keyframes dot-pulse {\n\n 0%,\n 100% {\n box-shadow:\n 0 0 0 3px rgba(155, 194, 239, .10),\n 0 0 6px rgba(155, 194, 239, .50);\n }\n\n 50% {\n box-shadow:\n 0 0 0 6px rgba(155, 194, 239, .05),\n 0 0 14px rgba(155, 194, 239, .90);\n }\n}\n\n/* ============================================================\n Hero strip\n ============================================================ */\n.hero-strip {\n display: flex;\n align-items: center;\n gap: 24px;\n padding: 14px 24px;\n border-bottom: 1px solid var(--rule);\n background: linear-gradient(90deg, rgba(27, 58, 120, .10) 0%, rgba(4, 8, 26, 0) 100%);\n position: relative;\n overflow: hidden;\n}\n\n.hero-spacer {\n flex: 1;\n}\n\n.date-block {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.d-day {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 38px;\n line-height: 1;\n letter-spacing: -0.04em;\n color: var(--mist);\n}\n\n.d-rest {\n display: flex;\n flex-direction: column;\n gap: 2px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-dim);\n}\n\n.d-rest .d-mute {\n color: var(--text-mute);\n}\n\n.active-block {\n display: flex;\n flex-direction: column;\n gap: 2px;\n text-align: right;\n max-width: 360px;\n overflow: hidden;\n}\n\n.ab-label {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.ab-value {\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--mist);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 360px;\n}\n\n/* ============================================================\n Main grid\n ============================================================ */\n.grid-main {\n display: grid;\n grid-template-columns: 260px 1fr 340px;\n gap: 16px;\n padding: 16px 24px;\n min-height: 0;\n z-index: 10;\n}\n\n.col-left,\n.col-center,\n.col-right {\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-height: 0;\n}\n\n/* Savings + Cost share one row at the top of the center column */\n.hero-row {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 12px;\n align-items: stretch;\n flex-shrink: 0;\n}\n\n.hero-row > .card { flex-shrink: 1; }\n\n@media (max-width: 1100px) {\n .hero-row { grid-template-columns: 1fr; }\n}\n\n/* ============================================================\n Panels / cards \u2014 darker\n ============================================================ */\n.panel,\n.card {\n position: relative;\n border: 1px solid var(--rule);\n border-radius: 14px;\n background: var(--surface-1);\n padding: 14px 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-height: 0;\n transition: border-color 180ms ease, background 180ms ease;\n}\n\n.card.has-tooltip {\n cursor: help;\n}\n\n.card.has-tooltip:hover {\n border-color: var(--rule-hover);\n background: var(--surface-2);\n}\n\n.card-head {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 12px;\n}\n\n.card-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.card-eyebrow em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 12px;\n color: var(--sky);\n letter-spacing: 0;\n text-transform: none;\n}\n\n.card-meta {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n/* Legend panel */\n.panel {\n padding: 14px 14px 16px;\n gap: 14px;\n flex-shrink: 0;\n}\n\n.p-head {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding-bottom: 10px;\n border-bottom: 1px solid var(--rule-2);\n}\n\n.p-section {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.p-section+.p-section {\n padding-top: 12px;\n border-top: 1px solid var(--rule-2);\n}\n\n.ps-head {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-bottom: 4px;\n}\n\n.check {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 3px 6px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-dim);\n letter-spacing: 0.02em;\n}\n\nbutton.check {\n border: 0;\n background: transparent;\n width: 100%;\n text-align: left;\n}\n\n.check-clickable {\n cursor: pointer;\n border-radius: 6px;\n padding: 5px 6px;\n transition: background 140ms, color 140ms, transform 140ms;\n}\n\n.check-clickable .pf-arrow {\n margin-left: auto;\n color: var(--text-mute);\n font-family: var(--font-mono);\n font-size: 12px;\n transition: color 140ms, transform 140ms;\n}\n\n.check-clickable:hover {\n background: rgba(155, 194, 239, .07);\n color: var(--mist);\n}\n\n.check-clickable:hover .pf-arrow {\n color: var(--sky);\n transform: translateX(2px);\n}\n\n.dot-sq {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n background: var(--text-mute);\n flex-shrink: 0;\n}\n\n.dot-sq.opus {\n background: var(--c-opus);\n}\n\n.dot-sq.sonnet {\n background: var(--c-sonnet);\n}\n\n.dot-sq.haiku {\n background: var(--c-haiku);\n}\n\n.dot-sq.unknown {\n background: var(--c-unknown);\n}\n\n.proj-filter {\n display: flex;\n flex-direction: column;\n gap: 1px;\n max-height: 90px;\n overflow-y: auto;\n}\n\n.pf-name {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 140px;\n}\n\n/* ============================================================\n Donut card (model usage)\n ============================================================ */\n.donut-card {\n flex: 1;\n gap: 10px;\n}\n\n.donut-wrap {\n position: relative;\n width: 140px;\n height: 140px;\n margin: 4px auto 0;\n}\n\n.donut {\n width: 100%;\n height: 100%;\n display: block;\n}\n\n.donut-track {\n fill: none;\n stroke: rgba(155, 194, 239, .07);\n stroke-width: 14;\n}\n\n.donut-seg {\n transition: stroke-dashoffset 400ms ease, stroke-dasharray 400ms ease;\n}\n\n.donut-center {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n pointer-events: none;\n}\n\n.donut-total {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 26px;\n letter-spacing: -0.02em;\n color: var(--mist);\n line-height: 1;\n}\n\n.donut-total-k {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-top: 4px;\n}\n\n.donut-legend {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 6px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n}\n\n.dl-row {\n display: grid;\n grid-template-columns: auto 1fr auto;\n align-items: center;\n gap: 8px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-dim);\n}\n\n.dl-dot {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n}\n\n.dl-name {\n color: var(--text-dim);\n}\n\n.dl-pct {\n color: var(--mist);\n font-weight: 500;\n}\n\n/* ============================================================\n Center column \u2014 Metric strip (no card chrome, divider-separated)\n ============================================================ */\n.metric-strip {\n display: grid;\n grid-template-columns: repeat(5, 1fr);\n border: 1px solid var(--rule);\n border-radius: 14px;\n background: var(--surface-1);\n overflow: hidden;\n flex-shrink: 0;\n}\n\n.metric-item {\n padding: 14px 18px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n cursor: help;\n border-right: 1px solid var(--rule-2);\n transition: background 200ms ease;\n min-width: 0;\n}\n.metric-item:last-child { border-right: 0; }\n.metric-item:hover { background: var(--surface-2); }\n\n.m-label {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.m-value {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 26px;\n letter-spacing: -0.025em;\n color: var(--mist);\n line-height: 1;\n}\n\n.m-value em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n color: var(--sky);\n letter-spacing: -0.005em;\n}\n\n/* ============================================================\n Savings card\n ============================================================ */\n.card.savings {\n flex-shrink: 0;\n gap: 12px;\n background:\n linear-gradient(180deg, rgba(123, 255, 199, .05) 0%, rgba(4, 8, 26, .20) 50%),\n var(--surface-1);\n border-color: rgba(123, 255, 199, .18);\n}\n\n.card.savings:hover {\n border-color: rgba(123, 255, 199, .32);\n}\n\n.savings-body {\n display: grid;\n grid-template-columns: auto 1fr;\n align-items: center;\n gap: 18px;\n}\n\n.savings-figure {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.savings-money {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 34px;\n letter-spacing: -0.035em;\n line-height: 1;\n color: var(--money);\n}\n\n.savings-tokens {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-top: 4px;\n}\n\n.savings-bar {\n position: relative;\n height: 8px;\n border-radius: 999px;\n overflow: hidden;\n background: var(--surface-3);\n display: flex;\n}\n\n.savings-actual {\n height: 100%;\n background: rgba(215, 230, 247, .55);\n transition: width 500ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.savings-saved {\n height: 100%;\n background: var(--signal-green);\n transition: width 500ms cubic-bezier(0.16, 1, 0.3, 1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12);\n}\n\n.savings-legend {\n grid-column: 2;\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 24px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.08em;\n color: var(--text-mute);\n margin-top: 8px;\n}\n\n.sl-row {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.sl-row b {\n color: var(--mist);\n font-weight: 500;\n letter-spacing: 0.04em;\n}\n\n.sl-dot {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n}\n\n.sl-dot.actual {\n background: var(--mist);\n}\n\n.sl-dot.saved {\n background: var(--signal-green);\n}\n\n/* ============================================================\n Recent turns table\n ============================================================ */\n.turns-card {\n flex: 1;\n padding: 0;\n overflow: hidden;\n}\n\n.turns-card .card-head {\n padding: 14px 16px 10px;\n border-bottom: 1px solid var(--rule-2);\n}\n\n.turns-scroll {\n flex: 1;\n overflow-y: auto;\n min-height: 0;\n}\n\n.turns-table {\n width: 100%;\n border-collapse: collapse;\n}\n\n.turns-table thead th {\n position: sticky;\n top: 0;\n background: var(--ink);\n padding: 9px 16px;\n text-align: left;\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n font-weight: 500;\n border-bottom: 1px solid var(--rule);\n z-index: 1;\n}\n\n.turns-table thead th.num {\n text-align: right;\n}\n\n.turns-table tbody td {\n padding: 8px 16px;\n border-bottom: 1px solid var(--rule-2);\n color: var(--text-dim);\n font-size: 12px;\n}\n\n.turns-table tbody td.num {\n text-align: right;\n font-family: var(--font-mono);\n}\n\n.turns-table tbody td.cost {\n color: var(--money);\n font-family: var(--font-mono);\n font-weight: 500;\n}\n\n.turns-table tbody td.ts {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n}\n\n.turns-table tbody td.proj {\n color: var(--mist);\n}\n\n.turns-table tbody tr:hover {\n background: rgba(155, 194, 239, .03);\n}\n\n.turns-table tbody tr:last-child td {\n border-bottom: 0;\n}\n\n/* Model pills */\n.model-pill {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 2px 8px;\n border-radius: 999px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.04em;\n border: 1px solid var(--rule);\n background: rgba(4, 8, 26, .5);\n color: var(--mist);\n}\n\n.model-pill .sq {\n width: 6px;\n height: 6px;\n border-radius: 2px;\n background: var(--text-mute);\n}\n\n.model-pill.opus {\n color: #FF8A66;\n border-color: rgba(255, 99, 56, .32);\n background: rgba(255, 99, 56, .07);\n}\n\n.model-pill.opus .sq {\n background: var(--c-opus);\n}\n\n.model-pill.sonnet {\n color: #FFC766;\n border-color: rgba(255, 185, 56, .32);\n background: rgba(255, 185, 56, .07);\n}\n\n.model-pill.sonnet .sq {\n background: var(--c-sonnet);\n}\n\n.model-pill.haiku {\n color: #A878FF;\n border-color: rgba(116, 56, 255, .42);\n background: rgba(116, 56, 255, .10);\n}\n\n.model-pill.haiku .sq {\n background: var(--c-haiku);\n}\n\n.model-pill.unknown {\n color: #5BDDF7;\n border-color: rgba(18, 203, 245, .32);\n background: rgba(18, 203, 245, .07);\n font-style: italic;\n}\n\n.model-pill.unknown .sq {\n background: var(--c-unknown);\n}\n\n/* ============================================================\n Right column \u2014 Cost hero\n ============================================================ */\n.cost-hero {\n position: relative;\n overflow: hidden;\n background:\n radial-gradient(120% 80% at 50% 110%, rgba(44, 93, 184, .18) 0%, rgba(4, 8, 26, .20) 60%),\n var(--surface-1);\n padding: 16px 18px 18px;\n gap: 10px;\n flex-shrink: 0;\n}\n\n.big-money {\n position: relative;\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 42px;\n letter-spacing: -0.035em;\n line-height: 1;\n color: var(--money);\n margin-top: 2px;\n}\n\n.big-money em {\n font-family: inherit;\n font-style: normal;\n font-weight: inherit;\n color: inherit;\n letter-spacing: inherit;\n opacity: 1;\n}\n\n.cost-sub {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 6px;\n margin-top: 4px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n}\n\n.cs-row {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n font-family: var(--font-mono);\n font-size: 11px;\n}\n\n.cs-k {\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n/* File paths stay as-is (no uppercase/wide tracking) and may overflow \u2014 clip. */\n.cs-k.path {\n text-transform: none;\n letter-spacing: 0.01em;\n font-size: 11px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 14rem;\n}\n\n.cs-v {\n color: var(--mist);\n}\n\n/* ============================================================\n Moat card\n ============================================================ */\n.moat {\n flex: 1;\n gap: 8px;\n}\n\n/* Hot-files list: cap height + scroll so a long list never squeezes the Moat */\n#hot-files-list {\n max-height: 190px;\n overflow-y: auto;\n}\n\n.moat-value {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 34px;\n letter-spacing: -0.03em;\n line-height: 1;\n color: var(--mist);\n margin-top: 2px;\n}\n\n.moat-value em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 18px;\n color: var(--sky);\n letter-spacing: 0;\n margin-left: 6px;\n}\n\n.gate-mini {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 6px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n.gate-row {\n display: grid;\n grid-template-columns: auto auto 1fr;\n align-items: center;\n gap: 8px;\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--text-dim);\n padding: 3px 0;\n}\n\n.gate-row .g-ts {\n color: var(--text-mute);\n font-size: 9px;\n min-width: 38px;\n}\n\n.gate-row .g-decision {\n font-size: 9px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: 999px;\n}\n\n.gate-row .g-decision.block {\n color: var(--signal-rose);\n background: rgba(220, 90, 90, .06);\n}\n\n.gate-row .g-decision.allow {\n color: var(--text-mute);\n background: rgba(155, 194, 239, .03);\n}\n\n.gate-row .g-q {\n color: var(--mist);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* ============================================================\n Tooltips\n ============================================================ */\n.has-tooltip {\n position: relative;\n}\n\n/* Global JS-positioned tooltip \u2014 viewport-clamped */\n.global-tooltip {\n position: fixed;\n top: 0;\n left: 0;\n background: linear-gradient(180deg, rgba(18, 37, 73, .98), rgba(10, 21, 48, .98));\n color: var(--mist);\n border: 1px solid var(--rule-hover);\n border-radius: 12px;\n padding: 14px 16px;\n font-family: var(--font-sans);\n font-size: 15px;\n font-weight: 400;\n text-transform: none;\n letter-spacing: 0;\n white-space: normal;\n width: 320px;\n max-width: calc(100vw - 24px);\n text-align: left;\n line-height: 1.55;\n box-shadow: 0 16px 36px rgba(0, 0, 0, .7);\n backdrop-filter: blur(10px);\n z-index: 99999;\n opacity: 0;\n pointer-events: none;\n transform: translateY(6px);\n transition: opacity 180ms ease, transform 180ms ease;\n}\n\n.global-tooltip.on {\n opacity: 1;\n transform: translateY(0);\n}\n\n/* ============================================================\n Footer\n ============================================================ */\n.foot {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 8px 24px;\n border-top: 1px solid var(--rule);\n background: linear-gradient(0deg, rgba(4, 8, 26, .7), rgba(4, 8, 26, .4));\n backdrop-filter: blur(10px);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.foot em {\n font-family: var(--font-serif);\n font-style: italic;\n text-transform: none;\n letter-spacing: 0;\n color: var(--sky);\n font-size: 12px;\n}\n\n.foot .mono {\n color: var(--text-dim);\n text-transform: none;\n letter-spacing: 0.04em;\n}\n\n/* ============================================================\n Empty state\n ============================================================ */\n.empty {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.06em;\n color: var(--text-mute);\n text-align: center;\n padding: 16px 8px;\n font-style: italic;\n text-transform: none;\n}\n\n/* Scrollbar styling */\n.turns-scroll::-webkit-scrollbar,\n.proj-chart::-webkit-scrollbar,\n.gate-mini::-webkit-scrollbar,\n#hot-files-list::-webkit-scrollbar {\n width: 6px;\n}\n\n.turns-scroll::-webkit-scrollbar-thumb,\n.proj-chart::-webkit-scrollbar-thumb,\n.gate-mini::-webkit-scrollbar-thumb,\n#hot-files-list::-webkit-scrollbar-thumb {\n background: var(--rule);\n border-radius: 999px;\n}\n\n.turns-scroll::-webkit-scrollbar-track,\n.proj-chart::-webkit-scrollbar-track,\n.gate-mini::-webkit-scrollbar-track,\n#hot-files-list::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.hidden {\n display: none !important;\n}\n\n/* ============================================================\n Staggered cascade on first paint (one-time, MOTION 6)\n ============================================================ */\n@keyframes cascade-in {\n from { opacity: 0; transform: translateY(10px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n .col-left > *,\n .col-center > *,\n .col-right > * {\n opacity: 0;\n animation: cascade-in 520ms cubic-bezier(0.16, 1, 0.3, 1) forwards;\n will-change: transform, opacity;\n }\n .col-left > *:nth-child(1) { animation-delay: 0ms; }\n .col-left > *:nth-child(2) { animation-delay: 120ms; }\n .col-center > *:nth-child(1) { animation-delay: 40ms; }\n .col-center > *:nth-child(2) { animation-delay: 140ms; }\n .col-center > *:nth-child(3) { animation-delay: 240ms; }\n .col-right > *:nth-child(1) { animation-delay: 80ms; }\n .col-right > *:nth-child(2) { animation-delay: 200ms; }\n .col-right > *:nth-child(3) { animation-delay: 320ms; }\n .col-right > *:nth-child(4) { animation-delay: 440ms; }\n\n /* Clear will-change after animation completes */\n .col-left > *,\n .col-center > *,\n .col-right > * {\n animation-fill-mode: forwards;\n }\n}\n\n/* ============================================================\n Source / basis annotations\n ============================================================ */\n.card-source {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.08em;\n text-transform: lowercase;\n color: var(--text-mute);\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin-top: auto;\n padding-top: 8px;\n border-top: 1px solid var(--rule-2);\n width: 100%;\n}\n\n.src-badge {\n font-family: var(--font-mono);\n font-size: 8px;\n font-weight: 500;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: 4px;\n flex-shrink: 0;\n}\n\n.src-badge.verified {\n color: var(--signal-green);\n background: rgba(123, 255, 199, .08);\n border: 1px solid rgba(123, 255, 199, .25);\n}\n\n.src-badge.estimated,\n.src-badge.estimated.floor {\n color: var(--signal-amber);\n background: rgba(255, 185, 56, .10);\n border: 1px solid rgba(255, 185, 56, .30);\n}\n\n.src-badge.priced {\n color: var(--signal-cyan);\n background: rgba(155, 194, 239, .08);\n border: 1px solid rgba(155, 194, 239, .25);\n}\n\n/* Eyebrow that contains a badge */\n.card-eyebrow .src-badge {\n margin-left: 4px;\n}\n\n/* Savings audit row \u2014 live formula reveal */\n.savings-audit {\n margin-top: 10px;\n padding: 10px 12px;\n border: 1px dashed rgba(255, 185, 56, .25);\n border-radius: 8px;\n background: rgba(255, 185, 56, .04);\n font-family: var(--font-mono);\n font-size: 10.5px;\n letter-spacing: 0.04em;\n color: var(--text-mute);\n text-align: center;\n}\n\n.savings-audit b {\n color: var(--text-dim);\n font-weight: 500;\n}\n\n.savings-audit .audit-result {\n color: var(--money);\n}\n\n/* ============================================================\n FAQ dialog\n ============================================================ */\n.dialog.dialog-faq {\n max-width: min(80vw, 1100px);\n width: 100%;\n max-height: 86vh;\n display: flex;\n flex-direction: column;\n padding: 28px 32px 24px;\n gap: 6px;\n}\n\n.dialog.dialog-faq .dialog-path {\n margin-bottom: 4px;\n word-break: normal;\n overflow-wrap: anywhere;\n}\n\n.faq-content {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n margin-top: 18px;\n padding-right: 8px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.faq-content::-webkit-scrollbar {\n width: 6px;\n}\n\n.faq-content::-webkit-scrollbar-thumb {\n background: var(--rule);\n border-radius: 999px;\n}\n\n.faq-content::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.faq-content details {\n border: 1px solid var(--rule);\n border-radius: 12px;\n background: var(--surface-1);\n overflow: hidden;\n transition: background 180ms, border-color 180ms;\n flex-shrink: 0;\n}\n\n.faq-content details:hover {\n border-color: rgba(155, 194, 239, .22);\n}\n\n.faq-content details[open] {\n background: var(--surface-2);\n border-color: var(--rule-hover);\n}\n\n.faq-content summary {\n cursor: pointer;\n padding: 14px 20px;\n font-family: var(--font-sans);\n font-size: 14px;\n font-weight: 500;\n color: var(--mist);\n list-style: none;\n display: flex;\n align-items: center;\n gap: 12px;\n user-select: none;\n}\n\n.faq-content summary::-webkit-details-marker {\n display: none;\n}\n\n.faq-content summary::before {\n content: "\u203A";\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n color: var(--text-mute);\n font-family: var(--font-mono);\n font-size: 14px;\n transition: transform 220ms ease, color 220ms ease;\n}\n\n.faq-content details[open] summary::before {\n transform: rotate(90deg);\n color: var(--sky);\n}\n\n.faq-content .faq-body {\n padding: 0 22px 20px 46px;\n color: var(--text-dim);\n font-size: 13.5px;\n line-height: 1.7;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.faq-content .faq-body p {\n margin: 0;\n}\n\n.faq-content .faq-body ul {\n margin: 0;\n padding-left: 20px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.faq-content .faq-body li {\n margin: 0;\n}\n\n.faq-content .faq-body b,\n.faq-content .faq-body strong {\n color: var(--mist);\n font-weight: 500;\n}\n\n.faq-content .faq-body code {\n font-family: var(--font-mono);\n font-size: 12px;\n background: rgba(155, 194, 239, .08);\n padding: 2px 6px;\n border-radius: 4px;\n color: var(--mist);\n border: 1px solid rgba(155, 194, 239, .12);\n word-break: break-word;\n}\n\n.faq-content .faq-body a {\n color: var(--blue-bright);\n text-decoration: underline;\n text-decoration-color: rgba(92, 143, 230, .40);\n text-underline-offset: 3px;\n transition: color 140ms, text-decoration-color 140ms;\n}\n\n.faq-content .faq-body a:hover {\n color: var(--mist);\n text-decoration-color: var(--sky);\n}\n\n.faq-content .faq-body table {\n width: 100%;\n border-collapse: collapse;\n margin: 4px 0;\n font-size: 13px;\n table-layout: fixed;\n}\n\n.faq-content .faq-body thead td {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding-bottom: 6px;\n border-bottom: 1px solid var(--rule);\n font-weight: 500;\n}\n\n.faq-content .faq-body td {\n padding: 9px 10px;\n border-bottom: 1px solid var(--rule-2);\n vertical-align: top;\n word-break: break-word;\n}\n\n.faq-content .faq-body tr:last-child td {\n border-bottom: 0;\n}\n\n.faq-content .faq-body td:first-child {\n color: var(--text-dim);\n width: 38%;\n}\n\n.faq-content .faq-body td:first-child code {\n font-size: 11.5px;\n}\n\n.faq-content .faq-body .formula-box {\n font-family: var(--font-mono);\n font-size: 12.5px;\n background: rgba(255, 185, 56, .06);\n padding: 12px 14px;\n border-radius: 8px;\n border: 1px dashed rgba(255, 185, 56, .30);\n color: var(--mist);\n letter-spacing: 0.02em;\n}\n\n.faq-content .faq-body .link-list {\n list-style: none;\n padding-left: 0;\n}\n\n.faq-content .faq-body .link-list li {\n padding-left: 18px;\n position: relative;\n}\n\n.faq-content .faq-body .link-list li::before {\n content: "\u203A";\n position: absolute;\n left: 0;\n color: var(--sky);\n font-family: var(--font-mono);\n}\n\n.faq-content .faq-body .warning {\n margin-top: 14px;\n padding: 12px 14px;\n background: rgba(255, 185, 56, .06);\n border: 1px solid rgba(255, 185, 56, .25);\n border-left: 3px solid var(--signal-amber);\n border-radius: 8px;\n font-size: 12.5px;\n color: var(--text-dim);\n}\n\n.faq-content .faq-body .warning .icon {\n color: var(--signal-amber);\n margin-right: 8px;\n font-weight: 500;\n}\n\n/* ============================================================\n Project dialog\n ============================================================ */\n.dialog-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(4, 8, 26, .78);\n backdrop-filter: blur(10px);\n z-index: 10000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n animation: dlg-fade 180ms ease;\n}\n\n@keyframes dlg-fade {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n}\n\n.dialog {\n position: relative;\n width: 100%;\n max-width: 520px;\n background:\n radial-gradient(120% 80% at 50% 0%, rgba(44, 93, 184, .22) 0%, rgba(4, 8, 26, .20) 60%),\n linear-gradient(180deg, rgba(18, 37, 73, .88) 0%, rgba(10, 21, 48, .96) 100%);\n border: 1px solid var(--rule-hover);\n border-radius: 18px;\n padding: 28px 32px 32px;\n box-shadow:\n 0 30px 80px -20px rgba(0, 0, 0, .7),\n inset 0 1px 0 rgba(255, 255, 255, .04);\n animation: dlg-rise 220ms cubic-bezier(.2, .7, .2, 1);\n}\n\n@keyframes dlg-rise {\n from {\n opacity: 0;\n transform: translateY(8px) scale(.98);\n }\n\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n.dialog-close {\n position: absolute;\n top: 14px;\n right: 14px;\n width: 30px;\n height: 30px;\n border-radius: 50%;\n color: var(--text-mute);\n font-size: 22px;\n line-height: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 180ms, color 180ms;\n}\n\n.dialog-close:hover {\n background: rgba(155, 194, 239, .10);\n color: var(--mist);\n}\n\n.dialog-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-bottom: 10px;\n}\n\n.dialog-eyebrow em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 12px;\n color: var(--sky);\n letter-spacing: 0;\n text-transform: none;\n}\n\n.dialog-name {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 28px;\n letter-spacing: -0.025em;\n color: var(--mist);\n line-height: 1.1;\n}\n\n.dialog-path {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n margin-top: 6px;\n word-break: break-all;\n}\n\n.dialog-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 18px 24px;\n margin-top: 22px;\n padding-top: 20px;\n border-top: 1px solid var(--rule-2);\n}\n\n.dg-cell {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.dg-k {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.dg-v {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 22px;\n letter-spacing: -0.02em;\n color: var(--mist);\n line-height: 1;\n}\n\n.dg-v.money {\n color: var(--money);\n}\n\n.dg-v-sm {\n font-size: 13px;\n font-family: var(--font-mono);\n font-weight: 400;\n color: var(--text-dim);\n letter-spacing: 0;\n}\n\n/* ============================================================\n v0.3 visual refresh\n - merged model-family legend into donut (count column)\n - Projects -> colored bar chart\n - elevated Savings card\n ============================================================ */\n\n/* Left column sizing: donut natural height, projects fills + scrolls */\n.donut-card { flex: 0 0 auto; }\n\n/* Donut legend now carries a count column */\n.dl-row {\n grid-template-columns: auto 1fr auto auto;\n gap: 8px;\n padding: 1px 0;\n}\n.dl-count {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--text-mute);\n letter-spacing: 0.04em;\n font-variant-numeric: tabular-nums;\n}\n.dl-pct {\n min-width: 30px;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* ---- Projects bar chart ---- */\n.projects-card {\n flex: 1 1 auto;\n min-height: 0;\n gap: 10px;\n}\n.proj-chart {\n display: flex;\n flex-direction: column;\n gap: 9px;\n overflow-y: auto;\n min-height: 0;\n flex: 1;\n padding-right: 2px;\n}\n.proj-row {\n display: flex;\n flex-direction: column;\n gap: 7px;\n width: 100%;\n text-align: left;\n padding: 8px;\n border-radius: 9px;\n background: transparent;\n border: 0;\n cursor: pointer;\n transition: background 150ms ease;\n}\n.proj-row:hover { background: rgba(155, 194, 239, .055); }\n.pr-top {\n display: grid;\n grid-template-columns: auto 1fr auto auto;\n align-items: center;\n gap: 9px;\n}\n.pr-dot {\n width: 9px;\n height: 9px;\n border-radius: 3px;\n background: var(--pc, var(--sky));\n box-shadow: 0 0 9px -1px var(--pc, var(--sky));\n flex-shrink: 0;\n}\n.pr-name {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--text-dim);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n letter-spacing: 0.01em;\n transition: color 150ms ease;\n}\n.proj-row:hover .pr-name { color: var(--mist); }\n.pr-turns {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--mist);\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.02em;\n}\n.pr-arrow {\n font-family: var(--font-mono);\n font-size: 13px;\n color: var(--text-mute);\n transition: color 150ms ease, transform 150ms ease;\n}\n.proj-row:hover .pr-arrow {\n color: var(--pc, var(--sky));\n transform: translateX(2px);\n}\n.pr-bar {\n position: relative;\n height: 5px;\n border-radius: 999px;\n background: var(--surface-3);\n overflow: hidden;\n}\n.pr-fill {\n display: block;\n height: 100%;\n border-radius: 999px;\n background: linear-gradient(90deg,\n color-mix(in oklch, var(--pc, var(--sky)) 45%, transparent) 0%,\n var(--pc, var(--sky)) 100%);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18);\n transition: width 640ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n/* ---- Elevated Savings card (priority focus) ---- */\n.card.savings {\n background:\n radial-gradient(120% 140% at 10% -10%, rgba(123, 255, 199, .14) 0%, rgba(4, 8, 26, .08) 44%),\n linear-gradient(180deg, rgba(123, 255, 199, .05) 0%, rgba(4, 8, 26, .20) 52%),\n var(--surface-1);\n border-color: rgba(123, 255, 199, .24);\n box-shadow:\n inset 0 1px 0 rgba(123, 255, 199, .08),\n 0 20px 46px -30px rgba(123, 255, 199, .55);\n}\n.card.savings:hover {\n border-color: rgba(123, 255, 199, .38);\n}\n.savings-money {\n font-size: 40px;\n text-shadow: 0 0 26px rgba(123, 255, 199, .22);\n}\n.savings-bar { height: 9px; }\n.savings-saved {\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, .18),\n 0 0 12px -2px var(--signal-green);\n}\n\n/* Project dialog: name gets a project-colored accent dot */\n.dialog-name.has-accent {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.dialog-name.has-accent::before {\n content: "";\n width: 12px;\n height: 12px;\n border-radius: 4px;\n background: var(--pc, var(--sky));\n box-shadow: 0 0 12px -1px var(--pc, var(--sky));\n flex-shrink: 0;\n}\n\n\n/* ============================================================\n v0.3.1 \u2014 date + active project folded into the top nav\n ============================================================ */\n\n/* Let the right cluster shrink so the active path can ellipsize */\n.topnav-right { min-width: 0; }\n\n/* Compact date beside the brand */\n.nav-date {\n display: inline-flex;\n align-items: baseline;\n gap: 6px;\n margin-left: 8px;\n padding-left: 12px;\n border-left: 1px solid var(--rule);\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n white-space: nowrap;\n}\n.nav-date .nd-day { color: var(--mist); font-weight: 500; }\n.nav-date .nd-weekday { color: var(--text-dim); }\n.nav-date .nd-month { color: var(--text-mute); }\n\n/* Active project, compact, tail-truncated */\n.nav-active {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n max-width: 300px;\n padding-right: 12px;\n margin-right: 2px;\n border-right: 1px solid var(--rule);\n cursor: help;\n}\n.na-label {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n flex-shrink: 0;\n}\n.na-value {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--mist);\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n /* keep the project folder (tail) visible, ellipsize the drive prefix */\n direction: rtl;\n text-align: left;\n}\n\n/* Tighten nav on narrow widths */\n@media (max-width: 1100px) {\n .nav-active { max-width: 200px; }\n .nav-date .nd-month { display: none; }\n}\n\n\n/* Column headers signal they are hover-explainable */\n.turns-table thead th.has-tooltip { cursor: help; }\n.turns-table thead th.has-tooltip:hover { color: var(--text-dim); }\n\n/* ---- Recent-turns pager ---- */\n.turns-pager {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 10px;\n padding: 8px 2px 0;\n margin-top: 6px;\n border-top: 1px solid var(--rule-2);\n}\n.turns-pager.hidden { display: none; }\n.turns-pager button {\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.04em;\n color: var(--text-dim);\n background: rgba(4, 8, 26, .55);\n border: 1px solid var(--rule);\n border-radius: 7px;\n padding: 4px 10px;\n cursor: pointer;\n transition: background 150ms, border-color 150ms, color 150ms, transform 150ms;\n}\n.turns-pager button:hover:not(:disabled) {\n background: rgba(155, 194, 239, .10);\n border-color: var(--rule-hover);\n color: var(--mist);\n transform: translateY(-1px);\n}\n.turns-pager button:disabled {\n opacity: .35;\n cursor: default;\n}\n#turns-page-label {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n letter-spacing: 0.04em;\n font-variant-numeric: tabular-nums;\n min-width: 84px;\n text-align: center;\n}\n';
|
|
1578
|
+
|
|
1579
|
+
// src/dashboard/public/favicon.svg
|
|
1580
|
+
var favicon_default = '<svg width="107" height="107" viewBox="0 0 107 107" fill="none" xmlns="http://www.w3.org/2000/svg">\n<rect x="0.5" y="0.5" width="106" height="106" rx="7.5" fill="url(#paint0_radial_21_11)"/>\n<rect x="0.5" y="0.5" width="106" height="106" rx="7.5" stroke="url(#paint1_linear_21_11)"/>\n<path d="M26.408 72.558C25.5813 72.558 24.6513 72.4753 23.618 72.31C22.626 72.1447 21.6753 71.938 20.766 71.69C19.898 71.442 19.216 71.1733 18.72 70.884C18.4307 70.7187 18.2033 70.5533 18.038 70.388C17.914 70.1813 17.852 69.8507 17.852 69.396L17.542 59.662C17.542 58.8353 17.8107 58.422 18.348 58.422C18.8027 58.422 19.1127 58.794 19.278 59.538L19.836 61.894C21.2413 67.8047 23.866 70.76 27.71 70.76C29.7767 70.76 31.4093 70.078 32.608 68.714C33.848 67.3087 34.468 65.3453 34.468 62.824C34.468 60.3853 33.8273 58.112 32.546 56.004C31.306 53.896 29.3013 51.8087 26.532 49.742C23.4733 47.5513 21.2413 45.4433 19.836 43.418C18.4307 41.3513 17.728 39.1607 17.728 36.846C17.728 33.7873 18.782 31.3487 20.89 29.53C23.0393 27.67 25.7673 26.74 29.074 26.74C30.314 26.74 31.5127 26.8847 32.67 27.174C33.8273 27.4633 34.778 27.8767 35.522 28.414C35.77 28.5793 35.956 28.7653 36.08 28.972C36.2453 29.1787 36.328 29.4473 36.328 29.778L36.514 39.14C36.514 39.8427 36.2453 40.194 35.708 40.194C35.336 40.194 35.0673 39.9047 34.902 39.326L34.468 37.776C33.5587 34.5933 32.608 32.2787 31.616 30.832C30.6653 29.344 29.2807 28.6 27.462 28.6C25.6847 28.6 24.2587 29.1787 23.184 30.336C22.1507 31.4933 21.634 33.126 21.634 35.234C21.634 37.0113 22.2127 38.706 23.37 40.318C24.5273 41.93 26.5527 43.8933 29.446 46.208C32.546 48.7293 34.7987 51.168 36.204 53.524C37.6507 55.8387 38.374 58.3187 38.374 60.964C38.374 63.2787 37.8573 65.304 36.824 67.04C35.7907 68.776 34.3647 70.14 32.546 71.132C30.7687 72.0827 28.7227 72.558 26.408 72.558ZM44.1831 84.71C43.0671 84.71 42.1784 84.3173 41.5171 83.532C40.8558 82.788 40.5251 81.92 40.5251 80.928C40.5251 80.1427 40.7524 79.4813 41.2071 78.944C41.6618 78.4067 42.2818 78.138 43.0671 78.138C43.7284 78.138 44.2038 78.2827 44.4931 78.572C44.7824 78.8613 44.9891 79.192 45.1131 79.564C45.2371 79.936 45.3611 80.2667 45.4851 80.556C45.6091 80.8453 45.8571 80.99 46.2291 80.99C46.6838 80.99 47.0971 80.6593 47.4691 79.998C47.8411 79.378 48.3578 78.0347 49.0191 75.968C49.4324 74.728 49.6804 73.6533 49.7631 72.744C49.8871 71.8347 49.8664 70.8633 49.7011 69.83C49.5771 68.7967 49.2671 67.536 48.7711 66.048L42.0131 44.534C41.7238 43.542 41.4344 42.9013 41.1451 42.612C40.8971 42.3227 40.4838 42.116 39.9051 41.992L38.9751 41.806C38.4378 41.682 38.1691 41.4133 38.1691 41C38.1691 40.5867 38.4584 40.38 39.0371 40.38H49.2671C49.8458 40.38 50.1351 40.5867 50.1351 41C50.1351 41.4547 49.8664 41.7233 49.3291 41.806L48.3371 41.93C47.3451 42.054 46.7044 42.302 46.4151 42.674C46.1671 43.0047 46.1878 43.6247 46.4771 44.534L51.9331 62.7C52.0571 63.1133 52.2431 63.32 52.4911 63.32C52.7804 63.32 52.9871 63.1133 53.1111 62.7L58.3811 45.65C58.7118 44.534 58.7944 43.7073 58.6291 43.17C58.5051 42.5913 57.9264 42.2193 56.8931 42.054L55.5911 41.806C55.0538 41.682 54.7851 41.4133 54.7851 41C54.7851 40.5867 55.0744 40.38 55.6531 40.38H63.5271C64.1058 40.38 64.3951 40.6073 64.3951 41.062C64.3951 41.4753 64.1471 41.7647 63.6511 41.93L63.0931 42.116C62.4731 42.3227 61.9564 42.6947 61.5431 43.232C61.1298 43.7693 60.6958 44.6993 60.2411 46.022L51.0031 74.852C50.0938 77.7453 49.2671 79.8947 48.5231 81.3C47.8204 82.7053 47.1384 83.6147 46.4771 84.028C45.8158 84.4827 45.0511 84.71 44.1831 84.71ZM64.9342 72C64.3142 72 64.0042 71.7727 64.0042 71.318C64.0042 70.946 64.2729 70.698 64.8102 70.574L65.5542 70.45C66.4222 70.2847 66.9802 70.0367 67.2282 69.706C67.5176 69.334 67.6622 68.714 67.6622 67.846V47.014C67.6622 46.27 67.5382 45.774 67.2902 45.526C67.0836 45.2367 66.6909 45.0507 66.1122 44.968L64.8102 44.782C64.2729 44.7407 64.0042 44.5133 64.0042 44.1C64.0042 43.7693 64.3349 43.542 64.9962 43.418C66.2776 43.2113 67.2489 42.8807 67.9102 42.426C68.6129 41.9713 69.3362 41.4133 70.0802 40.752C70.4522 40.38 70.7622 40.194 71.0102 40.194C71.3822 40.194 71.5682 40.442 71.5682 40.938V43.728C71.5682 44.1 71.6922 44.348 71.9402 44.472C72.2296 44.5547 72.5396 44.4307 72.8702 44.1C74.5236 42.5293 75.9702 41.4547 77.2102 40.876C78.4916 40.2973 79.8349 40.008 81.2402 40.008C83.1002 40.008 84.5882 40.6693 85.7042 41.992C86.8202 43.2733 87.3782 45.1747 87.3782 47.696V67.846C87.3782 68.714 87.5022 69.334 87.7502 69.706C88.0396 70.0367 88.6182 70.264 89.4862 70.388L90.8502 70.574C91.3049 70.6567 91.5322 70.9047 91.5322 71.318C91.5322 71.7727 91.2842 72 90.7882 72H80.3722C79.7936 72 79.5042 71.7727 79.5042 71.318C79.5042 70.9047 79.7316 70.6567 80.1862 70.574L81.0542 70.45C81.9222 70.326 82.4802 70.078 82.7282 69.706C83.0176 69.334 83.1622 68.714 83.1622 67.846V48.378C83.1622 46.3527 82.7902 44.9267 82.0462 44.1C81.3022 43.2733 80.2482 42.86 78.8842 42.86C77.5616 42.86 76.3629 43.2527 75.2882 44.038C74.2549 44.782 73.4282 45.7947 72.8082 47.076C72.1882 48.316 71.8782 49.7007 71.8782 51.23V67.846C71.8782 68.714 72.0022 69.334 72.2502 69.706C72.5396 70.078 73.1182 70.3053 73.9862 70.388L75.6602 70.574C76.1149 70.6567 76.3422 70.884 76.3422 71.256C76.3422 71.752 76.0322 72 75.4122 72H64.9342Z" fill="white"/>\n<defs>\n<radialGradient id="paint0_radial_21_11" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(14 16) rotate(43.5498) scale(167.638 156.2)">\n<stop stop-color="#2C5DB8"/>\n<stop offset="1" stop-color="#04081A"/>\n</radialGradient>\n<linearGradient id="paint1_linear_21_11" x1="1.77511" y1="1.50277e-06" x2="105.225" y2="107" gradientUnits="userSpaceOnUse">\n<stop stop-color="#5C8FE6"/>\n<stop offset="1" stop-color="#335080"/>\n</linearGradient>\n</defs>\n</svg>\n';
|
|
1400
1581
|
|
|
1401
1582
|
// src/dashboard/server.ts
|
|
1402
1583
|
var FALLBACK_RANGE = 9;
|
|
@@ -1416,6 +1597,11 @@ async function startDashboard(paths, preferredPort = 8901) {
|
|
|
1416
1597
|
c.header("Cache-Control", "no-cache");
|
|
1417
1598
|
return c.body(style_default);
|
|
1418
1599
|
});
|
|
1600
|
+
app.get("/favicon.svg", (c) => {
|
|
1601
|
+
c.header("Content-Type", "image/svg+xml; charset=utf-8");
|
|
1602
|
+
c.header("Cache-Control", "public, max-age=86400");
|
|
1603
|
+
return c.body(favicon_default);
|
|
1604
|
+
});
|
|
1419
1605
|
app.get("/health", (c) => c.json({ ok: true }));
|
|
1420
1606
|
app.get("/data", async (c) => {
|
|
1421
1607
|
const data = await computeDashboardData(paths, RECENT_N);
|
|
@@ -1434,8 +1620,8 @@ async function startDashboard(paths, preferredPort = 8901) {
|
|
|
1434
1620
|
}
|
|
1435
1621
|
|
|
1436
1622
|
// src/hooks/installer.ts
|
|
1437
|
-
import { mkdir as
|
|
1438
|
-
import { dirname as
|
|
1623
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
1624
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
1439
1625
|
|
|
1440
1626
|
// src/hooks/scripts/pre-compact.ps1
|
|
1441
1627
|
var pre_compact_default = '# PreCompact hook \u2014 Windows PowerShell.\n# Re-injects the primer after Claude auto-compacts. Same logic as prime.ps1.\n\n$ErrorActionPreference = "SilentlyContinue"\n\n$portFile = Join-Path $PWD ".synthra-graph\\mcp_port"\nif (-not (Test-Path $portFile)) { exit 0 }\n$port = (Get-Content -Path $portFile -Raw).Trim()\nif (-not $port) { exit 0 }\n\ntry {\n $resp = Invoke-RestMethod -Uri "http://127.0.0.1:$port/prime" -Method GET -TimeoutSec 3\n if ($resp.primer) { Write-Output $resp.primer }\n} catch {\n # silent\n}\nexit 0\n';
|
|
@@ -1739,7 +1925,7 @@ function chosenScriptExt() {
|
|
|
1739
1925
|
}
|
|
1740
1926
|
async function readSettings(path) {
|
|
1741
1927
|
try {
|
|
1742
|
-
const raw = await
|
|
1928
|
+
const raw = await readFile4(path, "utf8");
|
|
1743
1929
|
return JSON.parse(raw);
|
|
1744
1930
|
} catch {
|
|
1745
1931
|
return {};
|
|
@@ -1778,18 +1964,18 @@ function mergeOurHooks(config, paths) {
|
|
|
1778
1964
|
return config;
|
|
1779
1965
|
}
|
|
1780
1966
|
async function installHooks(paths) {
|
|
1781
|
-
await
|
|
1967
|
+
await mkdir3(paths.claudeHooksDir, { recursive: true });
|
|
1782
1968
|
const scriptsWritten = [];
|
|
1783
1969
|
for (const s of SCRIPTS) {
|
|
1784
1970
|
const target = join3(paths.claudeHooksDir, `${s.baseName}${chosenScriptExt()}`);
|
|
1785
|
-
await
|
|
1971
|
+
await writeFile3(target, chosenScriptBody(s), "utf8");
|
|
1786
1972
|
scriptsWritten.push(target);
|
|
1787
1973
|
}
|
|
1788
|
-
await
|
|
1974
|
+
await mkdir3(dirname3(paths.claudeSettings), { recursive: true });
|
|
1789
1975
|
const existing = await readSettings(paths.claudeSettings);
|
|
1790
1976
|
const stripped = stripOurHooks(existing);
|
|
1791
1977
|
const merged = mergeOurHooks(stripped, paths);
|
|
1792
|
-
await
|
|
1978
|
+
await writeFile3(paths.claudeSettings, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
1793
1979
|
log.debug(`installed ${scriptsWritten.length} hook script(s) into ${paths.claudeHooksDir}`);
|
|
1794
1980
|
return { scriptsWritten, settingsUpdated: true };
|
|
1795
1981
|
}
|
|
@@ -1797,11 +1983,11 @@ async function installHooks(paths) {
|
|
|
1797
1983
|
// src/server/http.ts
|
|
1798
1984
|
import { serve as serve2 } from "@hono/node-server";
|
|
1799
1985
|
import { Hono as Hono2 } from "hono";
|
|
1800
|
-
import { writeFile as
|
|
1986
|
+
import { writeFile as writeFile11 } from "fs/promises";
|
|
1801
1987
|
|
|
1802
1988
|
// src/activity/activity-log.ts
|
|
1803
|
-
import { appendFile, mkdir as
|
|
1804
|
-
import { dirname as
|
|
1989
|
+
import { appendFile as appendFile2, mkdir as mkdir4 } from "fs/promises";
|
|
1990
|
+
import { dirname as dirname4 } from "path";
|
|
1805
1991
|
var DEFAULT_RING_SIZE = 100;
|
|
1806
1992
|
var ActivityStore = class {
|
|
1807
1993
|
ring = [];
|
|
@@ -1838,8 +2024,8 @@ var ActivityStore = class {
|
|
|
1838
2024
|
}
|
|
1839
2025
|
async persist(event) {
|
|
1840
2026
|
try {
|
|
1841
|
-
await
|
|
1842
|
-
await
|
|
2027
|
+
await mkdir4(dirname4(this.persistPath), { recursive: true });
|
|
2028
|
+
await appendFile2(this.persistPath, JSON.stringify(event) + "\n", "utf8");
|
|
1843
2029
|
} catch {
|
|
1844
2030
|
}
|
|
1845
2031
|
}
|
|
@@ -1847,7 +2033,7 @@ var ActivityStore = class {
|
|
|
1847
2033
|
|
|
1848
2034
|
// src/activity/file-watcher.ts
|
|
1849
2035
|
import chokidar from "chokidar";
|
|
1850
|
-
import { readFile as
|
|
2036
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1851
2037
|
import { join as join4, relative, sep } from "path";
|
|
1852
2038
|
import ignore from "ignore";
|
|
1853
2039
|
var ALWAYS_IGNORE = [
|
|
@@ -1870,7 +2056,7 @@ var ALWAYS_IGNORE = [
|
|
|
1870
2056
|
];
|
|
1871
2057
|
async function readIgnoreFile(path) {
|
|
1872
2058
|
try {
|
|
1873
|
-
const text = await
|
|
2059
|
+
const text = await readFile5(path, "utf8");
|
|
1874
2060
|
return text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
1875
2061
|
} catch {
|
|
1876
2062
|
return [];
|
|
@@ -1935,14 +2121,14 @@ function createFileWatcher(root, onEvent) {
|
|
|
1935
2121
|
// src/activity/git-watcher.ts
|
|
1936
2122
|
import { execFile } from "child_process";
|
|
1937
2123
|
import { watch } from "fs";
|
|
1938
|
-
import { readFile as
|
|
2124
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1939
2125
|
import { join as join5 } from "path";
|
|
1940
2126
|
import { promisify } from "util";
|
|
1941
2127
|
var execFileAsync = promisify(execFile);
|
|
1942
2128
|
var POLL_MS = 2e3;
|
|
1943
2129
|
async function readHeadBranch(projectRoot) {
|
|
1944
2130
|
try {
|
|
1945
|
-
const head = await
|
|
2131
|
+
const head = await readFile6(join5(projectRoot, ".git", "HEAD"), "utf8");
|
|
1946
2132
|
const m = head.trim().match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
1947
2133
|
return m?.[1] ?? null;
|
|
1948
2134
|
} catch {
|
|
@@ -2041,10 +2227,10 @@ function parseStatusFiles(porcelain) {
|
|
|
2041
2227
|
import { resolve } from "path";
|
|
2042
2228
|
|
|
2043
2229
|
// src/scanner/extract.ts
|
|
2044
|
-
import { dirname as
|
|
2230
|
+
import { dirname as dirname5, join as join6, posix } from "path";
|
|
2045
2231
|
|
|
2046
2232
|
// src/graph/types.ts
|
|
2047
|
-
var SCHEMA_VERSION2 =
|
|
2233
|
+
var SCHEMA_VERSION2 = 2;
|
|
2048
2234
|
|
|
2049
2235
|
// src/scanner/hash.ts
|
|
2050
2236
|
import { createHash } from "crypto";
|
|
@@ -2362,14 +2548,20 @@ async function buildGraph(root, parsed) {
|
|
|
2362
2548
|
for (const p of parsed) filesByPath.set(p.file.relPath, true);
|
|
2363
2549
|
const nodes = [];
|
|
2364
2550
|
const edges = [];
|
|
2551
|
+
const symbolsByFile = /* @__PURE__ */ new Map();
|
|
2552
|
+
const callsByFile = /* @__PURE__ */ new Map();
|
|
2365
2553
|
for (const p of parsed) {
|
|
2366
2554
|
const fileNode = toFileNode(p);
|
|
2367
2555
|
nodes.push(fileNode);
|
|
2556
|
+
const fileSymNodes = [];
|
|
2368
2557
|
for (const sym of p.symbols) {
|
|
2369
2558
|
const symNode = toSymbolNode(p, sym);
|
|
2370
2559
|
nodes.push(symNode);
|
|
2560
|
+
fileSymNodes.push(symNode);
|
|
2371
2561
|
edges.push({ from: fileNode.id, to: symNode.id, kind: "defines" });
|
|
2372
2562
|
}
|
|
2563
|
+
symbolsByFile.set(p.file.relPath, fileSymNodes);
|
|
2564
|
+
callsByFile.set(p.file.relPath, p.calls);
|
|
2373
2565
|
const importEdges = /* @__PURE__ */ new Set();
|
|
2374
2566
|
for (const spec of p.imports) {
|
|
2375
2567
|
const target = resolveImport(p.file.relPath, spec, filesByPath);
|
|
@@ -2384,6 +2576,7 @@ async function buildGraph(root, parsed) {
|
|
|
2384
2576
|
edges.push({ from: fileNode.id, to: fileId(testTargetPath), kind: "tests" });
|
|
2385
2577
|
}
|
|
2386
2578
|
}
|
|
2579
|
+
edges.push(...buildCallEdges(symbolsByFile, callsByFile));
|
|
2387
2580
|
const symbolCount = nodes.filter((n) => n.kind === "symbol").length;
|
|
2388
2581
|
const fileCount = nodes.length - symbolCount;
|
|
2389
2582
|
return {
|
|
@@ -2407,9 +2600,52 @@ function buildSymbolIndex(graph) {
|
|
|
2407
2600
|
}
|
|
2408
2601
|
return out;
|
|
2409
2602
|
}
|
|
2603
|
+
function tightestContainer(syms, line) {
|
|
2604
|
+
let best = null;
|
|
2605
|
+
for (const s of syms) {
|
|
2606
|
+
if (line < s.start_line || line > s.end_line) continue;
|
|
2607
|
+
if (!best || s.end_line - s.start_line < best.end_line - best.start_line) best = s;
|
|
2608
|
+
}
|
|
2609
|
+
return best;
|
|
2610
|
+
}
|
|
2611
|
+
function buildCallEdges(symbolsByFile, callsByFile) {
|
|
2612
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2613
|
+
for (const syms of symbolsByFile.values()) {
|
|
2614
|
+
for (const s of syms) {
|
|
2615
|
+
const list = byName.get(s.name);
|
|
2616
|
+
if (list) list.push(s);
|
|
2617
|
+
else byName.set(s.name, [s]);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
const edges = [];
|
|
2621
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2622
|
+
for (const [relPath, sites] of callsByFile) {
|
|
2623
|
+
const fileSyms = symbolsByFile.get(relPath) ?? [];
|
|
2624
|
+
for (const site of sites) {
|
|
2625
|
+
const caller = tightestContainer(fileSyms, site.line);
|
|
2626
|
+
if (!caller) continue;
|
|
2627
|
+
let callee = fileSyms.find((s) => s.name === site.callee);
|
|
2628
|
+
if (!callee) {
|
|
2629
|
+
const cands = byName.get(site.callee) ?? [];
|
|
2630
|
+
if (cands.length !== 1) continue;
|
|
2631
|
+
callee = cands[0];
|
|
2632
|
+
}
|
|
2633
|
+
if (!callee || callee.id === caller.id) continue;
|
|
2634
|
+
const key = `${caller.id}->${callee.id}`;
|
|
2635
|
+
if (seen.has(key)) continue;
|
|
2636
|
+
seen.add(key);
|
|
2637
|
+
edges.push({ from: caller.id, to: callee.id, kind: "calls" });
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
return edges;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// src/scanner/parse-cache.ts
|
|
2644
|
+
import { mkdir as mkdir5, readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
|
|
2645
|
+
import { dirname as dirname6 } from "path";
|
|
2410
2646
|
|
|
2411
2647
|
// src/scanner/parser.ts
|
|
2412
|
-
import { readFile as
|
|
2648
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2413
2649
|
import { createRequire } from "module";
|
|
2414
2650
|
import { Language, Parser } from "web-tree-sitter";
|
|
2415
2651
|
|
|
@@ -2425,10 +2661,11 @@ function cleanImport(s) {
|
|
|
2425
2661
|
async function runGenericParser(config, f, source) {
|
|
2426
2662
|
let symbols = [];
|
|
2427
2663
|
let imports = [];
|
|
2664
|
+
const calls = [];
|
|
2428
2665
|
try {
|
|
2429
2666
|
const { parser, language } = await createParser(config.grammar);
|
|
2430
2667
|
const tree = parser.parse(source);
|
|
2431
|
-
if (!tree) return { file: f, source, symbols, imports, calls
|
|
2668
|
+
if (!tree) return { file: f, source, symbols, imports, calls };
|
|
2432
2669
|
const query = new Query(language, config.query);
|
|
2433
2670
|
const matches = query.matches(tree.rootNode);
|
|
2434
2671
|
for (const match of matches) {
|
|
@@ -2453,6 +2690,14 @@ async function runGenericParser(config, f, source) {
|
|
|
2453
2690
|
});
|
|
2454
2691
|
continue;
|
|
2455
2692
|
}
|
|
2693
|
+
if (config.callCapture && config.callCalleeCapture) {
|
|
2694
|
+
const callNode = byName.get(config.callCapture);
|
|
2695
|
+
const calleeNode = byName.get(config.callCalleeCapture);
|
|
2696
|
+
if (callNode && calleeNode) {
|
|
2697
|
+
calls.push({ callee: calleeNode.text, line: callNode.startPosition.row + 1 });
|
|
2698
|
+
continue;
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2456
2701
|
if (config.importCapture) {
|
|
2457
2702
|
const imp = byName.get(config.importCapture);
|
|
2458
2703
|
if (imp) imports.push(cleanImport(imp.text));
|
|
@@ -2468,7 +2713,7 @@ async function runGenericParser(config, f, source) {
|
|
|
2468
2713
|
imports = Array.from(new Set(imports)).filter((s) => s.length > 0);
|
|
2469
2714
|
} catch {
|
|
2470
2715
|
}
|
|
2471
|
-
return { file: f, source, symbols, imports, calls
|
|
2716
|
+
return { file: f, source, symbols, imports, calls };
|
|
2472
2717
|
}
|
|
2473
2718
|
|
|
2474
2719
|
// src/scanner/parsers/c.ts
|
|
@@ -2479,6 +2724,7 @@ var QUERY = `
|
|
|
2479
2724
|
(type_definition declarator: (type_identifier) @type.name) @type
|
|
2480
2725
|
(preproc_include path: (string_literal) @import)
|
|
2481
2726
|
(preproc_include path: (system_lib_string) @import)
|
|
2727
|
+
(call_expression function: (identifier) @call.name) @call
|
|
2482
2728
|
`;
|
|
2483
2729
|
async function parseC(f, source) {
|
|
2484
2730
|
return runGenericParser(
|
|
@@ -2491,7 +2737,9 @@ async function parseC(f, source) {
|
|
|
2491
2737
|
{ declCapture: "enum", nameCapture: "enum.name", kind: "enum" },
|
|
2492
2738
|
{ declCapture: "type", nameCapture: "type.name", kind: "type" }
|
|
2493
2739
|
],
|
|
2494
|
-
importCapture: "import"
|
|
2740
|
+
importCapture: "import",
|
|
2741
|
+
callCapture: "call",
|
|
2742
|
+
callCalleeCapture: "call.name"
|
|
2495
2743
|
},
|
|
2496
2744
|
f,
|
|
2497
2745
|
source
|
|
@@ -2508,6 +2756,9 @@ var QUERY2 = `
|
|
|
2508
2756
|
(namespace_definition name: (namespace_identifier) @namespace.name) @namespace
|
|
2509
2757
|
(preproc_include path: (string_literal) @import)
|
|
2510
2758
|
(preproc_include path: (system_lib_string) @import)
|
|
2759
|
+
(call_expression function: (identifier) @call.name) @call
|
|
2760
|
+
(call_expression function: (field_expression field: (field_identifier) @call.name)) @call
|
|
2761
|
+
(call_expression function: (qualified_identifier name: (identifier) @call.name)) @call
|
|
2511
2762
|
`;
|
|
2512
2763
|
async function parseCpp(f, source) {
|
|
2513
2764
|
return runGenericParser(
|
|
@@ -2522,7 +2773,9 @@ async function parseCpp(f, source) {
|
|
|
2522
2773
|
{ declCapture: "enum", nameCapture: "enum.name", kind: "enum" },
|
|
2523
2774
|
{ declCapture: "namespace", nameCapture: "namespace.name", kind: "class" }
|
|
2524
2775
|
],
|
|
2525
|
-
importCapture: "import"
|
|
2776
|
+
importCapture: "import",
|
|
2777
|
+
callCapture: "call",
|
|
2778
|
+
callCalleeCapture: "call.name"
|
|
2526
2779
|
},
|
|
2527
2780
|
f,
|
|
2528
2781
|
source
|
|
@@ -2538,6 +2791,8 @@ var QUERY3 = `
|
|
|
2538
2791
|
(method_declaration name: (identifier) @method.name) @method
|
|
2539
2792
|
(namespace_declaration name: (_) @namespace.name) @namespace
|
|
2540
2793
|
(using_directive (_) @import)
|
|
2794
|
+
(invocation_expression function: (identifier) @call.name) @call
|
|
2795
|
+
(invocation_expression function: (member_access_expression name: (identifier) @call.name)) @call
|
|
2541
2796
|
`;
|
|
2542
2797
|
async function parseCSharp(f, source) {
|
|
2543
2798
|
return runGenericParser(
|
|
@@ -2552,7 +2807,9 @@ async function parseCSharp(f, source) {
|
|
|
2552
2807
|
{ declCapture: "method", nameCapture: "method.name", kind: "method" },
|
|
2553
2808
|
{ declCapture: "namespace", nameCapture: "namespace.name", kind: "class" }
|
|
2554
2809
|
],
|
|
2555
|
-
importCapture: "import"
|
|
2810
|
+
importCapture: "import",
|
|
2811
|
+
callCapture: "call",
|
|
2812
|
+
callCalleeCapture: "call.name"
|
|
2556
2813
|
},
|
|
2557
2814
|
f,
|
|
2558
2815
|
source
|
|
@@ -2657,6 +2914,8 @@ var QUERY5 = `
|
|
|
2657
2914
|
(method_declaration name: (field_identifier) @method.name) @method
|
|
2658
2915
|
(type_spec name: (type_identifier) @type.name) @type
|
|
2659
2916
|
(import_spec path: (interpreted_string_literal) @import)
|
|
2917
|
+
(call_expression function: (identifier) @call.name) @call
|
|
2918
|
+
(call_expression function: (selector_expression field: (field_identifier) @call.name)) @call
|
|
2660
2919
|
`;
|
|
2661
2920
|
async function parseGo(f, source) {
|
|
2662
2921
|
return runGenericParser(
|
|
@@ -2668,7 +2927,9 @@ async function parseGo(f, source) {
|
|
|
2668
2927
|
{ declCapture: "method", nameCapture: "method.name", kind: "method" },
|
|
2669
2928
|
{ declCapture: "type", nameCapture: "type.name", kind: "type" }
|
|
2670
2929
|
],
|
|
2671
|
-
importCapture: "import"
|
|
2930
|
+
importCapture: "import",
|
|
2931
|
+
callCapture: "call",
|
|
2932
|
+
callCalleeCapture: "call.name"
|
|
2672
2933
|
},
|
|
2673
2934
|
f,
|
|
2674
2935
|
source
|
|
@@ -2733,6 +2994,7 @@ var QUERY6 = `
|
|
|
2733
2994
|
(method_declaration name: (identifier) @method.name) @method
|
|
2734
2995
|
(enum_declaration name: (identifier) @enum.name) @enum
|
|
2735
2996
|
(import_declaration (scoped_identifier) @import)
|
|
2997
|
+
(method_invocation name: (identifier) @call.name) @call
|
|
2736
2998
|
`;
|
|
2737
2999
|
async function parseJava(f, source) {
|
|
2738
3000
|
return runGenericParser(
|
|
@@ -2745,7 +3007,9 @@ async function parseJava(f, source) {
|
|
|
2745
3007
|
{ declCapture: "method", nameCapture: "method.name", kind: "method" },
|
|
2746
3008
|
{ declCapture: "enum", nameCapture: "enum.name", kind: "enum" }
|
|
2747
3009
|
],
|
|
2748
|
-
importCapture: "import"
|
|
3010
|
+
importCapture: "import",
|
|
3011
|
+
callCapture: "call",
|
|
3012
|
+
callCalleeCapture: "call.name"
|
|
2749
3013
|
},
|
|
2750
3014
|
f,
|
|
2751
3015
|
source
|
|
@@ -2758,6 +3022,7 @@ var QUERY7 = `
|
|
|
2758
3022
|
(class_declaration (type_identifier) @class.name) @class
|
|
2759
3023
|
(object_declaration (type_identifier) @object.name) @object
|
|
2760
3024
|
(import_header (identifier) @import)
|
|
3025
|
+
(call_expression (simple_identifier) @call.name) @call
|
|
2761
3026
|
`;
|
|
2762
3027
|
async function parseKotlin(f, source) {
|
|
2763
3028
|
return runGenericParser(
|
|
@@ -2769,7 +3034,9 @@ async function parseKotlin(f, source) {
|
|
|
2769
3034
|
{ declCapture: "class", nameCapture: "class.name", kind: "class" },
|
|
2770
3035
|
{ declCapture: "object", nameCapture: "object.name", kind: "class" }
|
|
2771
3036
|
],
|
|
2772
|
-
importCapture: "import"
|
|
3037
|
+
importCapture: "import",
|
|
3038
|
+
callCapture: "call",
|
|
3039
|
+
callCalleeCapture: "call.name"
|
|
2773
3040
|
},
|
|
2774
3041
|
f,
|
|
2775
3042
|
source
|
|
@@ -2783,6 +3050,9 @@ var QUERY8 = `
|
|
|
2783
3050
|
(interface_declaration name: (name) @interface.name) @interface
|
|
2784
3051
|
(trait_declaration name: (name) @trait.name) @trait
|
|
2785
3052
|
(method_declaration name: (name) @method.name) @method
|
|
3053
|
+
(function_call_expression function: (name) @call.name) @call
|
|
3054
|
+
(member_call_expression name: (name) @call.name) @call
|
|
3055
|
+
(scoped_call_expression name: (name) @call.name) @call
|
|
2786
3056
|
`;
|
|
2787
3057
|
async function parsePhp(f, source) {
|
|
2788
3058
|
return runGenericParser(
|
|
@@ -2795,7 +3065,9 @@ async function parsePhp(f, source) {
|
|
|
2795
3065
|
{ declCapture: "interface", nameCapture: "interface.name", kind: "interface" },
|
|
2796
3066
|
{ declCapture: "trait", nameCapture: "trait.name", kind: "class" },
|
|
2797
3067
|
{ declCapture: "method", nameCapture: "method.name", kind: "method" }
|
|
2798
|
-
]
|
|
3068
|
+
],
|
|
3069
|
+
callCapture: "call",
|
|
3070
|
+
callCalleeCapture: "call.name"
|
|
2799
3071
|
},
|
|
2800
3072
|
f,
|
|
2801
3073
|
source
|
|
@@ -2810,6 +3082,8 @@ var QUERY9 = `
|
|
|
2810
3082
|
(import_statement name: (dotted_name) @import.module)
|
|
2811
3083
|
(import_from_statement module_name: (dotted_name) @import.from)
|
|
2812
3084
|
(import_from_statement module_name: (relative_import) @import.from)
|
|
3085
|
+
(call function: (identifier) @call.name) @call
|
|
3086
|
+
(call function: (attribute attribute: (identifier) @call.name)) @call
|
|
2813
3087
|
`;
|
|
2814
3088
|
function firstLine3(text, max = 200) {
|
|
2815
3089
|
const line = text.split(/\r?\n/, 1)[0] ?? "";
|
|
@@ -2818,10 +3092,11 @@ function firstLine3(text, max = 200) {
|
|
|
2818
3092
|
async function parsePython(f, source) {
|
|
2819
3093
|
let symbols = [];
|
|
2820
3094
|
let imports = [];
|
|
3095
|
+
const calls = [];
|
|
2821
3096
|
try {
|
|
2822
3097
|
const { parser, language } = await createParser("python");
|
|
2823
3098
|
const tree = parser.parse(source);
|
|
2824
|
-
if (!tree) return { file: f, source, symbols, imports, calls
|
|
3099
|
+
if (!tree) return { file: f, source, symbols, imports, calls };
|
|
2825
3100
|
const query = new Query3(language, QUERY9);
|
|
2826
3101
|
const matches = query.matches(tree.rootNode);
|
|
2827
3102
|
for (const match of matches) {
|
|
@@ -2854,7 +3129,15 @@ async function parsePython(f, source) {
|
|
|
2854
3129
|
continue;
|
|
2855
3130
|
}
|
|
2856
3131
|
const importNode = byName.get("import.module") ?? byName.get("import.from");
|
|
2857
|
-
if (importNode)
|
|
3132
|
+
if (importNode) {
|
|
3133
|
+
imports.push(importNode.text);
|
|
3134
|
+
continue;
|
|
3135
|
+
}
|
|
3136
|
+
const callName = byName.get("call.name");
|
|
3137
|
+
const callNode = byName.get("call");
|
|
3138
|
+
if (callName && callNode) {
|
|
3139
|
+
calls.push({ callee: callName.text, line: callNode.startPosition.row + 1 });
|
|
3140
|
+
}
|
|
2858
3141
|
}
|
|
2859
3142
|
const seen = /* @__PURE__ */ new Set();
|
|
2860
3143
|
symbols = symbols.filter((s) => {
|
|
@@ -2866,7 +3149,7 @@ async function parsePython(f, source) {
|
|
|
2866
3149
|
imports = Array.from(new Set(imports));
|
|
2867
3150
|
} catch {
|
|
2868
3151
|
}
|
|
2869
|
-
return { file: f, source, symbols, imports, calls
|
|
3152
|
+
return { file: f, source, symbols, imports, calls };
|
|
2870
3153
|
}
|
|
2871
3154
|
|
|
2872
3155
|
// src/scanner/parsers/ruby.ts
|
|
@@ -2875,6 +3158,7 @@ var QUERY10 = `
|
|
|
2875
3158
|
(singleton_method name: (identifier) @method.name) @method
|
|
2876
3159
|
(class name: (constant) @class.name) @class
|
|
2877
3160
|
(module name: (constant) @module.name) @module
|
|
3161
|
+
(call method: (identifier) @call.name) @call
|
|
2878
3162
|
`;
|
|
2879
3163
|
async function parseRuby(f, source) {
|
|
2880
3164
|
return runGenericParser(
|
|
@@ -2886,7 +3170,9 @@ async function parseRuby(f, source) {
|
|
|
2886
3170
|
{ declCapture: "method", nameCapture: "method.name", kind: "method" },
|
|
2887
3171
|
{ declCapture: "class", nameCapture: "class.name", kind: "class" },
|
|
2888
3172
|
{ declCapture: "module", nameCapture: "module.name", kind: "class" }
|
|
2889
|
-
]
|
|
3173
|
+
],
|
|
3174
|
+
callCapture: "call",
|
|
3175
|
+
callCalleeCapture: "call.name"
|
|
2890
3176
|
},
|
|
2891
3177
|
f,
|
|
2892
3178
|
source
|
|
@@ -2900,6 +3186,9 @@ var QUERY11 = `
|
|
|
2900
3186
|
(enum_item name: (type_identifier) @enum.name) @enum
|
|
2901
3187
|
(trait_item name: (type_identifier) @trait.name) @trait
|
|
2902
3188
|
(impl_item type: (type_identifier) @impl.name) @impl
|
|
3189
|
+
(call_expression function: (identifier) @call.name) @call
|
|
3190
|
+
(call_expression function: (scoped_identifier name: (identifier) @call.name)) @call
|
|
3191
|
+
(call_expression function: (field_expression field: (field_identifier) @call.name)) @call
|
|
2903
3192
|
`;
|
|
2904
3193
|
async function parseRust(f, source) {
|
|
2905
3194
|
return runGenericParser(
|
|
@@ -2912,7 +3201,9 @@ async function parseRust(f, source) {
|
|
|
2912
3201
|
{ declCapture: "enum", nameCapture: "enum.name", kind: "enum" },
|
|
2913
3202
|
{ declCapture: "trait", nameCapture: "trait.name", kind: "interface" },
|
|
2914
3203
|
{ declCapture: "impl", nameCapture: "impl.name", kind: "class" }
|
|
2915
|
-
]
|
|
3204
|
+
],
|
|
3205
|
+
callCapture: "call",
|
|
3206
|
+
callCalleeCapture: "call.name"
|
|
2916
3207
|
},
|
|
2917
3208
|
f,
|
|
2918
3209
|
source
|
|
@@ -2931,6 +3222,8 @@ var TS_QUERY = `
|
|
|
2931
3222
|
(lexical_declaration (variable_declarator name: (identifier) @const-fn.name value: [(arrow_function) (function_expression)])) @const-fn
|
|
2932
3223
|
(assignment_expression left: (member_expression property: (property_identifier) @member-fn.name) right: [(arrow_function) (function_expression)]) @member-fn
|
|
2933
3224
|
(import_statement source: (string) @import)
|
|
3225
|
+
(call_expression function: (identifier) @call.name) @call
|
|
3226
|
+
(call_expression function: (member_expression property: (property_identifier) @call.name)) @call
|
|
2934
3227
|
`;
|
|
2935
3228
|
var JS_QUERY = `
|
|
2936
3229
|
(function_declaration name: (identifier) @function.name) @function
|
|
@@ -2940,6 +3233,8 @@ var JS_QUERY = `
|
|
|
2940
3233
|
(assignment_expression left: (member_expression property: (property_identifier) @member-fn.name) right: [(arrow_function) (function_expression)]) @member-fn
|
|
2941
3234
|
(import_statement source: (string) @import)
|
|
2942
3235
|
(call_expression function: (identifier) @_require_fn arguments: (arguments . (string) @require_source))
|
|
3236
|
+
(call_expression function: (identifier) @call.name) @call
|
|
3237
|
+
(call_expression function: (member_expression property: (property_identifier) @call.name)) @call
|
|
2943
3238
|
`;
|
|
2944
3239
|
function grammarFor(ext) {
|
|
2945
3240
|
if (ext === ".tsx" || ext === ".jsx") return "tsx";
|
|
@@ -2968,10 +3263,11 @@ async function parseTypeScript(f, source) {
|
|
|
2968
3263
|
const grammar = grammarFor(f.ext);
|
|
2969
3264
|
let symbols = [];
|
|
2970
3265
|
let imports = [];
|
|
3266
|
+
const calls = [];
|
|
2971
3267
|
try {
|
|
2972
3268
|
const { parser, language } = await createParser(grammar);
|
|
2973
3269
|
const tree = parser.parse(source);
|
|
2974
|
-
if (!tree) return { file: f, source, symbols, imports, calls
|
|
3270
|
+
if (!tree) return { file: f, source, symbols, imports, calls };
|
|
2975
3271
|
const query = new Query4(language, queryFor(grammar));
|
|
2976
3272
|
const matches = query.matches(tree.rootNode);
|
|
2977
3273
|
for (const match of matches) {
|
|
@@ -2997,6 +3293,12 @@ async function parseTypeScript(f, source) {
|
|
|
2997
3293
|
const requireSource = byName.get("require_source");
|
|
2998
3294
|
if (requireFn && requireSource && requireFn.text === "require") {
|
|
2999
3295
|
imports.push(unquote(requireSource.text));
|
|
3296
|
+
continue;
|
|
3297
|
+
}
|
|
3298
|
+
const callName = byName.get("call.name");
|
|
3299
|
+
const callNode = byName.get("call");
|
|
3300
|
+
if (callName && callNode) {
|
|
3301
|
+
calls.push({ callee: callName.text, line: callNode.startPosition.row + 1 });
|
|
3000
3302
|
}
|
|
3001
3303
|
}
|
|
3002
3304
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3009,7 +3311,7 @@ async function parseTypeScript(f, source) {
|
|
|
3009
3311
|
imports = Array.from(new Set(imports));
|
|
3010
3312
|
} catch {
|
|
3011
3313
|
}
|
|
3012
|
-
return { file: f, source, symbols, imports, calls
|
|
3314
|
+
return { file: f, source, symbols, imports, calls };
|
|
3013
3315
|
}
|
|
3014
3316
|
|
|
3015
3317
|
// src/scanner/parsers/svelte.ts
|
|
@@ -3043,6 +3345,7 @@ async function parseSvelte(f, source) {
|
|
|
3043
3345
|
});
|
|
3044
3346
|
}
|
|
3045
3347
|
for (const imp of parsed.imports) out.imports.push(imp);
|
|
3348
|
+
for (const call of parsed.calls) out.calls.push({ ...call, line: call.line + offset });
|
|
3046
3349
|
}
|
|
3047
3350
|
out.symbols.push({
|
|
3048
3351
|
name: f.relPath.split("/").pop()?.replace(/\.svelte$/i, "") ?? f.relPath,
|
|
@@ -3086,6 +3389,7 @@ async function parseVue(f, source) {
|
|
|
3086
3389
|
});
|
|
3087
3390
|
}
|
|
3088
3391
|
for (const imp of parsed.imports) out.imports.push(imp);
|
|
3392
|
+
for (const call of parsed.calls) out.calls.push({ ...call, line: call.line + offset });
|
|
3089
3393
|
}
|
|
3090
3394
|
out.symbols.push({
|
|
3091
3395
|
name: f.relPath.split("/").pop()?.replace(/\.vue$/i, "") ?? f.relPath,
|
|
@@ -3142,13 +3446,7 @@ async function createParser(name) {
|
|
|
3142
3446
|
function emptyParsed(file, source) {
|
|
3143
3447
|
return { file, source, symbols: [], imports: [], calls: [] };
|
|
3144
3448
|
}
|
|
3145
|
-
async function
|
|
3146
|
-
let source;
|
|
3147
|
-
try {
|
|
3148
|
-
source = await readFile6(f.absPath, "utf8");
|
|
3149
|
-
} catch {
|
|
3150
|
-
return emptyParsed(f, "");
|
|
3151
|
-
}
|
|
3449
|
+
async function parseSource(f, source) {
|
|
3152
3450
|
switch (f.ext) {
|
|
3153
3451
|
case ".ts":
|
|
3154
3452
|
case ".tsx":
|
|
@@ -3201,8 +3499,72 @@ async function parseFile(f) {
|
|
|
3201
3499
|
}
|
|
3202
3500
|
}
|
|
3203
3501
|
|
|
3502
|
+
// src/scanner/parse-cache.ts
|
|
3503
|
+
var PARSE_CACHE_VERSION = 2;
|
|
3504
|
+
function emptyParseCache() {
|
|
3505
|
+
return { schema_version: PARSE_CACHE_VERSION, files: {} };
|
|
3506
|
+
}
|
|
3507
|
+
async function readParseCache(path) {
|
|
3508
|
+
try {
|
|
3509
|
+
const raw = await readFile8(path, "utf8");
|
|
3510
|
+
const parsed = JSON.parse(raw);
|
|
3511
|
+
if (parsed.schema_version !== PARSE_CACHE_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
|
|
3512
|
+
return emptyParseCache();
|
|
3513
|
+
}
|
|
3514
|
+
return { schema_version: PARSE_CACHE_VERSION, files: parsed.files };
|
|
3515
|
+
} catch {
|
|
3516
|
+
return emptyParseCache();
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
async function writeParseCache(path, cache) {
|
|
3520
|
+
try {
|
|
3521
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
3522
|
+
await writeFile4(path, `${JSON.stringify(cache)}
|
|
3523
|
+
`, "utf8");
|
|
3524
|
+
} catch {
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
async function incrementalParse(parsable, prev, opts = {}) {
|
|
3528
|
+
const cache = emptyParseCache();
|
|
3529
|
+
const parsed = [];
|
|
3530
|
+
let reused = 0;
|
|
3531
|
+
let reparsed = 0;
|
|
3532
|
+
let parseErrors = 0;
|
|
3533
|
+
for (const f of parsable) {
|
|
3534
|
+
let source;
|
|
3535
|
+
try {
|
|
3536
|
+
source = await readFile8(f.absPath, "utf8");
|
|
3537
|
+
} catch {
|
|
3538
|
+
continue;
|
|
3539
|
+
}
|
|
3540
|
+
const hash = fileHash(source);
|
|
3541
|
+
const cached = opts.full ? void 0 : prev.files[f.relPath];
|
|
3542
|
+
if (cached && cached.hash === hash) {
|
|
3543
|
+
parsed.push({
|
|
3544
|
+
file: f,
|
|
3545
|
+
source,
|
|
3546
|
+
symbols: cached.symbols,
|
|
3547
|
+
imports: cached.imports,
|
|
3548
|
+
calls: cached.calls
|
|
3549
|
+
});
|
|
3550
|
+
cache.files[f.relPath] = cached;
|
|
3551
|
+
reused += 1;
|
|
3552
|
+
continue;
|
|
3553
|
+
}
|
|
3554
|
+
try {
|
|
3555
|
+
const p = await parseSource(f, source);
|
|
3556
|
+
parsed.push(p);
|
|
3557
|
+
cache.files[f.relPath] = { hash, symbols: p.symbols, imports: p.imports, calls: p.calls };
|
|
3558
|
+
reparsed += 1;
|
|
3559
|
+
} catch {
|
|
3560
|
+
parseErrors += 1;
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
return { parsed, cache, reused, reparsed, parseErrors };
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3204
3566
|
// src/scanner/walker.ts
|
|
3205
|
-
import { readFile as
|
|
3567
|
+
import { readFile as readFile9, readdir, stat } from "fs/promises";
|
|
3206
3568
|
import { extname, join as join7, relative as relative2, sep as sep2 } from "path";
|
|
3207
3569
|
import ignore2 from "ignore";
|
|
3208
3570
|
var DEFAULT_IGNORE = [
|
|
@@ -3283,7 +3645,7 @@ var BINARY_EXTS = /* @__PURE__ */ new Set([
|
|
|
3283
3645
|
]);
|
|
3284
3646
|
async function readIgnoreFile2(path) {
|
|
3285
3647
|
try {
|
|
3286
|
-
const text = await
|
|
3648
|
+
const text = await readFile9(path, "utf8");
|
|
3287
3649
|
return text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
3288
3650
|
} catch {
|
|
3289
3651
|
return [];
|
|
@@ -3338,15 +3700,15 @@ async function* walk(root, options = {}) {
|
|
|
3338
3700
|
}
|
|
3339
3701
|
|
|
3340
3702
|
// src/graph/store.ts
|
|
3341
|
-
import { mkdir as
|
|
3342
|
-
import { dirname as
|
|
3703
|
+
import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
|
|
3704
|
+
import { dirname as dirname7 } from "path";
|
|
3343
3705
|
async function writeJson(path, data, pretty) {
|
|
3344
|
-
await
|
|
3706
|
+
await mkdir6(dirname7(path), { recursive: true });
|
|
3345
3707
|
const text = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
|
|
3346
|
-
await
|
|
3708
|
+
await writeFile5(path, text + "\n", "utf8");
|
|
3347
3709
|
}
|
|
3348
3710
|
async function readJson(path) {
|
|
3349
|
-
const text = await
|
|
3711
|
+
const text = await readFile10(path, "utf8");
|
|
3350
3712
|
return JSON.parse(text);
|
|
3351
3713
|
}
|
|
3352
3714
|
async function writeGraph(path, graph) {
|
|
@@ -3364,12 +3726,12 @@ async function readSymbolIndex(path) {
|
|
|
3364
3726
|
}
|
|
3365
3727
|
|
|
3366
3728
|
// src/cli/bootstrap.ts
|
|
3367
|
-
import { mkdir as
|
|
3729
|
+
import { mkdir as mkdir7, readFile as readFile12, stat as stat2, writeFile as writeFile7 } from "fs/promises";
|
|
3368
3730
|
import { basename as basename4 } from "path";
|
|
3369
3731
|
|
|
3370
3732
|
// src/hooks/claude-md.ts
|
|
3371
|
-
import { readFile as
|
|
3372
|
-
import { basename as basename3, dirname as
|
|
3733
|
+
import { readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
3734
|
+
import { basename as basename3, dirname as dirname8 } from "path";
|
|
3373
3735
|
var POLICY_VERSION = 6;
|
|
3374
3736
|
var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
|
|
3375
3737
|
var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
|
|
@@ -3531,14 +3893,14 @@ function onboardingSkeleton(projectName) {
|
|
|
3531
3893
|
async function patchClaudeMd(path, projectName) {
|
|
3532
3894
|
let existing;
|
|
3533
3895
|
try {
|
|
3534
|
-
existing = await
|
|
3896
|
+
existing = await readFile11(path, "utf8");
|
|
3535
3897
|
} catch {
|
|
3536
3898
|
existing = null;
|
|
3537
3899
|
}
|
|
3538
3900
|
const block = policyBlock();
|
|
3539
3901
|
if (existing === null) {
|
|
3540
|
-
const name = projectName || basename3(
|
|
3541
|
-
await
|
|
3902
|
+
const name = projectName || basename3(dirname8(path)) || "this project";
|
|
3903
|
+
await writeFile6(path, onboardingSkeleton(name) + "\n" + block + "\n", "utf8");
|
|
3542
3904
|
return { created: true, updated: false, skipped: false };
|
|
3543
3905
|
}
|
|
3544
3906
|
const stripped = existing.replace(ANY_BLOCK_RE, "");
|
|
@@ -3547,7 +3909,7 @@ async function patchClaudeMd(path, projectName) {
|
|
|
3547
3909
|
if (hadBlock && desired === existing) {
|
|
3548
3910
|
return { created: false, updated: false, skipped: true };
|
|
3549
3911
|
}
|
|
3550
|
-
await
|
|
3912
|
+
await writeFile6(path, desired, "utf8");
|
|
3551
3913
|
return { created: false, updated: true, skipped: false };
|
|
3552
3914
|
}
|
|
3553
3915
|
|
|
@@ -3572,13 +3934,13 @@ async function exists(path) {
|
|
|
3572
3934
|
}
|
|
3573
3935
|
async function ensureDir(path) {
|
|
3574
3936
|
const had = await exists(path);
|
|
3575
|
-
await
|
|
3937
|
+
await mkdir7(path, { recursive: true });
|
|
3576
3938
|
return !had;
|
|
3577
3939
|
}
|
|
3578
3940
|
async function patchGitignore(path) {
|
|
3579
3941
|
let existing = "";
|
|
3580
3942
|
try {
|
|
3581
|
-
existing = await
|
|
3943
|
+
existing = await readFile12(path, "utf8");
|
|
3582
3944
|
} catch {
|
|
3583
3945
|
}
|
|
3584
3946
|
const trimmed = new Set(existing.split(/\r?\n/).map((l) => l.trim()));
|
|
@@ -3587,7 +3949,7 @@ async function patchGitignore(path) {
|
|
|
3587
3949
|
const block = missing.map((m) => `# ${m.comment}
|
|
3588
3950
|
${m.entry}`).join("\n") + "\n";
|
|
3589
3951
|
const appendix = (existing.length === 0 || existing.endsWith("\n") ? "" : "\n") + (existing.length ? "\n" : "") + block;
|
|
3590
|
-
await
|
|
3952
|
+
await writeFile7(path, existing + appendix, "utf8");
|
|
3591
3953
|
return true;
|
|
3592
3954
|
}
|
|
3593
3955
|
async function bootstrap(paths) {
|
|
@@ -3661,25 +4023,22 @@ async function scanProject(projectRootRaw, opts = {}) {
|
|
|
3661
4023
|
for await (const file of walk(projectRoot)) walked.push(file);
|
|
3662
4024
|
if (verbose) log.info(` walked ${walked.length} files`);
|
|
3663
4025
|
const parsable = walked.filter((f) => PARSABLE_EXTS.has(f.ext));
|
|
3664
|
-
const
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
parseErrors += 1;
|
|
3671
|
-
if (verbose) log.debug(` parse failed: ${file.relPath} \u2014 ${err2.message}`);
|
|
3672
|
-
}
|
|
3673
|
-
}
|
|
4026
|
+
const prevCache = await readParseCache(paths.parseCache);
|
|
4027
|
+
const { parsed, cache, reused, reparsed, parseErrors } = await incrementalParse(
|
|
4028
|
+
parsable,
|
|
4029
|
+
prevCache,
|
|
4030
|
+
{ full: opts.full }
|
|
4031
|
+
);
|
|
3674
4032
|
if (verbose) {
|
|
3675
4033
|
log.info(
|
|
3676
|
-
` parsed ${parsed.length} files (${
|
|
4034
|
+
` parsed ${parsed.length} files (${reused} reused \xB7 ${reparsed} reparsed` + (parseErrors ? `, ${parseErrors} errored` : "") + `; ${walked.length - parsable.length} non-code skipped)`
|
|
3677
4035
|
);
|
|
3678
4036
|
}
|
|
3679
4037
|
const graph = await buildGraph(projectRoot, parsed);
|
|
3680
4038
|
const symbolIndex = buildSymbolIndex(graph);
|
|
3681
4039
|
await writeGraph(paths.infoGraph, graph);
|
|
3682
4040
|
await writeSymbolIndex(paths.symbolIndex, symbolIndex);
|
|
4041
|
+
await writeParseCache(paths.parseCache, cache);
|
|
3683
4042
|
if (verbose) {
|
|
3684
4043
|
log.info(
|
|
3685
4044
|
` wrote ${paths.infoGraph} \u2014 ${graph.symbol_count} symbols, ${graph.edge_count} edges`
|
|
@@ -3696,126 +4055,8 @@ async function scanProject(projectRootRaw, opts = {}) {
|
|
|
3696
4055
|
durationMs
|
|
3697
4056
|
};
|
|
3698
4057
|
}
|
|
3699
|
-
async function scanCommand(rawPath) {
|
|
3700
|
-
return scanProject(rawPath);
|
|
3701
|
-
}
|
|
3702
|
-
|
|
3703
|
-
// src/learn/store.ts
|
|
3704
|
-
import { appendFile as appendFile2, mkdir as mkdir6, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
3705
|
-
import { dirname as dirname7 } from "path";
|
|
3706
|
-
|
|
3707
|
-
// src/learn/usage.ts
|
|
3708
|
-
var LEARN_SCHEMA_VERSION = 1;
|
|
3709
|
-
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
3710
|
-
function halfLifeMs() {
|
|
3711
|
-
const env = Number(process.env.SYN_LEARN_HALFLIFE_DAYS);
|
|
3712
|
-
const days = Number.isFinite(env) && env > 0 ? env : 7;
|
|
3713
|
-
return days * DAY_MS;
|
|
3714
|
-
}
|
|
3715
|
-
function weightFor(source) {
|
|
3716
|
-
switch (source) {
|
|
3717
|
-
case "register_edit":
|
|
3718
|
-
return 2;
|
|
3719
|
-
case "read":
|
|
3720
|
-
return 1;
|
|
3721
|
-
default:
|
|
3722
|
-
return 0;
|
|
3723
|
-
}
|
|
3724
|
-
}
|
|
3725
|
-
function emptyStore() {
|
|
3726
|
-
return {
|
|
3727
|
-
schema_version: LEARN_SCHEMA_VERSION,
|
|
3728
|
-
asOf: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
3729
|
-
files: {}
|
|
3730
|
-
};
|
|
3731
|
-
}
|
|
3732
|
-
function decayFactor(fromTs, toMs, hl) {
|
|
3733
|
-
const fromMs = Date.parse(fromTs);
|
|
3734
|
-
if (!Number.isFinite(fromMs)) return 1;
|
|
3735
|
-
const dt = toMs - fromMs;
|
|
3736
|
-
if (dt <= 0) return 1;
|
|
3737
|
-
return Math.exp(-(Math.LN2 / hl) * dt);
|
|
3738
|
-
}
|
|
3739
|
-
function foldEvent(store, ev) {
|
|
3740
|
-
const w = weightFor(ev.source);
|
|
3741
|
-
if (w <= 0 || !ev.path) return store;
|
|
3742
|
-
const tMs = Date.parse(ev.ts);
|
|
3743
|
-
if (!Number.isFinite(tMs)) return store;
|
|
3744
|
-
const hl = halfLifeMs();
|
|
3745
|
-
const prev = store.files[ev.path];
|
|
3746
|
-
if (prev) {
|
|
3747
|
-
const decayed = prev.decayed * decayFactor(prev.lastTs, tMs, hl) + w;
|
|
3748
|
-
store.files[ev.path] = { count: prev.count + 1, decayed, lastTs: ev.ts };
|
|
3749
|
-
} else {
|
|
3750
|
-
store.files[ev.path] = { count: 1, decayed: w, lastTs: ev.ts };
|
|
3751
|
-
}
|
|
3752
|
-
return store;
|
|
3753
|
-
}
|
|
3754
|
-
function effectiveScores(store, nowMs) {
|
|
3755
|
-
const hl = halfLifeMs();
|
|
3756
|
-
const out = /* @__PURE__ */ new Map();
|
|
3757
|
-
for (const [path, stat6] of Object.entries(store.files)) {
|
|
3758
|
-
const eff = stat6.decayed * decayFactor(stat6.lastTs, nowMs, hl);
|
|
3759
|
-
if (eff > 0.01) out.set(path, eff);
|
|
3760
|
-
}
|
|
3761
|
-
return out;
|
|
3762
|
-
}
|
|
3763
|
-
function recomputeFromLog(events) {
|
|
3764
|
-
const store = emptyStore();
|
|
3765
|
-
for (const ev of events) foldEvent(store, ev);
|
|
3766
|
-
return store;
|
|
3767
|
-
}
|
|
3768
|
-
|
|
3769
|
-
// src/learn/store.ts
|
|
3770
|
-
async function readLearnStore(path) {
|
|
3771
|
-
try {
|
|
3772
|
-
const raw = await readFile11(path, "utf8");
|
|
3773
|
-
const parsed = JSON.parse(raw);
|
|
3774
|
-
if (parsed.schema_version !== LEARN_SCHEMA_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
|
|
3775
|
-
return emptyStore();
|
|
3776
|
-
}
|
|
3777
|
-
return {
|
|
3778
|
-
schema_version: LEARN_SCHEMA_VERSION,
|
|
3779
|
-
asOf: typeof parsed.asOf === "string" ? parsed.asOf : emptyStore().asOf,
|
|
3780
|
-
files: parsed.files
|
|
3781
|
-
};
|
|
3782
|
-
} catch {
|
|
3783
|
-
return emptyStore();
|
|
3784
|
-
}
|
|
3785
|
-
}
|
|
3786
|
-
async function writeLearnStore(path, store) {
|
|
3787
|
-
try {
|
|
3788
|
-
await mkdir6(dirname7(path), { recursive: true });
|
|
3789
|
-
await writeFile6(path, JSON.stringify(store, null, 2) + "\n", "utf8");
|
|
3790
|
-
} catch {
|
|
3791
|
-
}
|
|
3792
|
-
}
|
|
3793
|
-
async function readAccessLog(path) {
|
|
3794
|
-
try {
|
|
3795
|
-
const raw = await readFile11(path, "utf8");
|
|
3796
|
-
const out = [];
|
|
3797
|
-
for (const line of raw.split("\n")) {
|
|
3798
|
-
const t = line.trim();
|
|
3799
|
-
if (!t) continue;
|
|
3800
|
-
try {
|
|
3801
|
-
const ev = JSON.parse(t);
|
|
3802
|
-
if (ev && typeof ev.ts === "string" && typeof ev.path === "string" && typeof ev.source === "string") {
|
|
3803
|
-
out.push(ev);
|
|
3804
|
-
}
|
|
3805
|
-
} catch {
|
|
3806
|
-
}
|
|
3807
|
-
}
|
|
3808
|
-
return out;
|
|
3809
|
-
} catch {
|
|
3810
|
-
return [];
|
|
3811
|
-
}
|
|
3812
|
-
}
|
|
3813
|
-
async function appendAccess(path, ev) {
|
|
3814
|
-
try {
|
|
3815
|
-
await mkdir6(dirname7(path), { recursive: true });
|
|
3816
|
-
await appendFile2(path, JSON.stringify(ev) + "\n", "utf8");
|
|
3817
|
-
} catch {
|
|
3818
|
-
}
|
|
4058
|
+
async function scanCommand(rawPath, opts = {}) {
|
|
4059
|
+
return scanProject(rawPath, opts);
|
|
3819
4060
|
}
|
|
3820
4061
|
|
|
3821
4062
|
// src/learn/runtime.ts
|
|
@@ -3876,8 +4117,8 @@ var LearnRuntime = class _LearnRuntime {
|
|
|
3876
4117
|
};
|
|
3877
4118
|
|
|
3878
4119
|
// src/server/mcp.ts
|
|
3879
|
-
import { appendFile as appendFile3, mkdir as
|
|
3880
|
-
import { dirname as
|
|
4120
|
+
import { appendFile as appendFile3, mkdir as mkdir10 } from "fs/promises";
|
|
4121
|
+
import { dirname as dirname11 } from "path";
|
|
3881
4122
|
|
|
3882
4123
|
// src/graph/rank.ts
|
|
3883
4124
|
var KW_BASE_WEIGHT = 2;
|
|
@@ -4130,14 +4371,14 @@ async function retrieve(graph, query, options = {}) {
|
|
|
4130
4371
|
|
|
4131
4372
|
// src/memory/branches.ts
|
|
4132
4373
|
import { execFile as execFile2 } from "child_process";
|
|
4133
|
-
import { readFile as
|
|
4374
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
4134
4375
|
import { join as join8 } from "path";
|
|
4135
4376
|
import { promisify as promisify2 } from "util";
|
|
4136
4377
|
var execFileAsync2 = promisify2(execFile2);
|
|
4137
4378
|
async function currentBranch(projectRoot) {
|
|
4138
4379
|
try {
|
|
4139
4380
|
const headPath = join8(projectRoot, ".git", "HEAD");
|
|
4140
|
-
const head = await
|
|
4381
|
+
const head = await readFile13(headPath, "utf8");
|
|
4141
4382
|
const trimmed = head.trim();
|
|
4142
4383
|
const match = trimmed.match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
4143
4384
|
if (match?.[1]) return match[1];
|
|
@@ -4187,8 +4428,8 @@ function resolveBranchPaths(contextDir, branch, isDefault) {
|
|
|
4187
4428
|
}
|
|
4188
4429
|
|
|
4189
4430
|
// src/memory/context-md.ts
|
|
4190
|
-
import { mkdir as
|
|
4191
|
-
import { dirname as
|
|
4431
|
+
import { mkdir as mkdir8, readFile as readFile14, writeFile as writeFile8 } from "fs/promises";
|
|
4432
|
+
import { dirname as dirname9 } from "path";
|
|
4192
4433
|
var MAX_BULLETS = 3;
|
|
4193
4434
|
function deriveContextMd(entries, branch) {
|
|
4194
4435
|
const tasks = entries.filter((e) => e.type === "task").reverse();
|
|
@@ -4231,17 +4472,17 @@ function formatContextMd(ctx) {
|
|
|
4231
4472
|
return lines.join("\n");
|
|
4232
4473
|
}
|
|
4233
4474
|
async function writeContextMd(path, ctx) {
|
|
4234
|
-
await
|
|
4235
|
-
await
|
|
4475
|
+
await mkdir8(dirname9(path), { recursive: true });
|
|
4476
|
+
await writeFile8(path, formatContextMd(ctx), "utf8");
|
|
4236
4477
|
}
|
|
4237
4478
|
|
|
4238
4479
|
// src/memory/context-store.ts
|
|
4239
|
-
import { mkdir as
|
|
4240
|
-
import { dirname as
|
|
4480
|
+
import { mkdir as mkdir9, readFile as readFile15, writeFile as writeFile9 } from "fs/promises";
|
|
4481
|
+
import { dirname as dirname10 } from "path";
|
|
4241
4482
|
var SCHEMA_VERSION3 = 1;
|
|
4242
4483
|
async function readEntries(path) {
|
|
4243
4484
|
try {
|
|
4244
|
-
const raw = await
|
|
4485
|
+
const raw = await readFile15(path, "utf8");
|
|
4245
4486
|
const parsed = JSON.parse(raw);
|
|
4246
4487
|
return Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
4247
4488
|
} catch {
|
|
@@ -4249,9 +4490,9 @@ async function readEntries(path) {
|
|
|
4249
4490
|
}
|
|
4250
4491
|
}
|
|
4251
4492
|
async function writeEntries(path, entries) {
|
|
4252
|
-
await
|
|
4493
|
+
await mkdir9(dirname10(path), { recursive: true });
|
|
4253
4494
|
const store = { schema_version: SCHEMA_VERSION3, entries };
|
|
4254
|
-
await
|
|
4495
|
+
await writeFile9(path, JSON.stringify(store, null, 2) + "\n", "utf8");
|
|
4255
4496
|
}
|
|
4256
4497
|
async function appendEntry(path, entry) {
|
|
4257
4498
|
const entries = await readEntries(path);
|
|
@@ -4628,7 +4869,7 @@ var TOOLS = [
|
|
|
4628
4869
|
},
|
|
4629
4870
|
{
|
|
4630
4871
|
name: "blast_radius",
|
|
4631
|
-
description: "Given a file (or 'file::symbol' target), return all files that depend on it transitively via imports
|
|
4872
|
+
description: "Given a file (or 'file::symbol' target), return all files that depend on it transitively via imports, tests, and call edges (callers). Use BEFORE editing a widely-used file to see what could break. Call edges are name-resolved (precise within a file, unique-name across files) and projected to file granularity.",
|
|
4632
4873
|
inputSchema: {
|
|
4633
4874
|
type: "object",
|
|
4634
4875
|
properties: {
|
|
@@ -4640,7 +4881,7 @@ var TOOLS = [
|
|
|
4640
4881
|
},
|
|
4641
4882
|
{
|
|
4642
4883
|
name: "dead_code",
|
|
4643
|
-
description: "Return files in the project that no other file imports and no test file references \u2014 strong candidates for unused/orphaned code. File-level granularity (
|
|
4884
|
+
description: "Return files in the project that no other file imports and no test file references \u2014 strong candidates for unused/orphaned code. File-level granularity; symbol-level dead code (unused exports, on top of the call graph) is a planned follow-up. Common entry-point patterns (main, index, app, CLI, bin/) are excluded heuristically.",
|
|
4644
4885
|
inputSchema: {
|
|
4645
4886
|
type: "object",
|
|
4646
4887
|
properties: {
|
|
@@ -4686,12 +4927,24 @@ function blastRadius(args, ctx) {
|
|
|
4686
4927
|
const filePath = targetRaw.split("::", 1)[0]?.trim() ?? targetRaw;
|
|
4687
4928
|
const root = ctx.graph.nodes.find((n) => n.kind === "file" && n.path === filePath);
|
|
4688
4929
|
if (!root) return errorContent(`blast_radius: file not in graph: ${filePath}`);
|
|
4930
|
+
const fileIdBySymbol = /* @__PURE__ */ new Map();
|
|
4931
|
+
for (const n of ctx.graph.nodes) {
|
|
4932
|
+
if (n.kind === "symbol") fileIdBySymbol.set(n.id, `file:${n.file}`);
|
|
4933
|
+
}
|
|
4689
4934
|
const incoming = /* @__PURE__ */ new Map();
|
|
4935
|
+
const addIncoming = (to, from, kind) => {
|
|
4936
|
+
const list = incoming.get(to) ?? [];
|
|
4937
|
+
list.push({ from, kind });
|
|
4938
|
+
incoming.set(to, list);
|
|
4939
|
+
};
|
|
4690
4940
|
for (const e of ctx.graph.edges) {
|
|
4691
|
-
if (e.kind
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4941
|
+
if (e.kind === "imports" || e.kind === "tests") {
|
|
4942
|
+
addIncoming(e.to, e.from, e.kind);
|
|
4943
|
+
} else if (e.kind === "calls") {
|
|
4944
|
+
const fromFile = fileIdBySymbol.get(e.from);
|
|
4945
|
+
const toFile = fileIdBySymbol.get(e.to);
|
|
4946
|
+
if (fromFile && toFile && fromFile !== toFile) addIncoming(toFile, fromFile, "calls");
|
|
4947
|
+
}
|
|
4695
4948
|
}
|
|
4696
4949
|
const visited = /* @__PURE__ */ new Set([root.id]);
|
|
4697
4950
|
const hits = [];
|
|
@@ -4769,7 +5022,7 @@ _(no file is unreferenced \u2014 every file is either imported by another, has a
|
|
|
4769
5022
|
}
|
|
4770
5023
|
lines.push("");
|
|
4771
5024
|
lines.push(
|
|
4772
|
-
`
|
|
5025
|
+
`_caveat:_ this is file-level only. Symbol-level dead code (unused exports), built on the now-populated call graph, is a planned follow-up.`
|
|
4773
5026
|
);
|
|
4774
5027
|
return textContent(lines.join("\n"));
|
|
4775
5028
|
}
|
|
@@ -4921,7 +5174,7 @@ async function contextRecall(args, ctx) {
|
|
|
4921
5174
|
}
|
|
4922
5175
|
async function logToolCall(ctx, tool) {
|
|
4923
5176
|
try {
|
|
4924
|
-
await
|
|
5177
|
+
await mkdir10(dirname11(ctx.paths.toolLog), { recursive: true });
|
|
4925
5178
|
await appendFile3(
|
|
4926
5179
|
ctx.paths.toolLog,
|
|
4927
5180
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool }) + "\n",
|
|
@@ -5020,12 +5273,12 @@ async function getCommitsSince(projectRoot, sinceIso) {
|
|
|
5020
5273
|
}
|
|
5021
5274
|
|
|
5022
5275
|
// src/memory/session.ts
|
|
5023
|
-
import { mkdir as
|
|
5024
|
-
import { dirname as
|
|
5276
|
+
import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile10 } from "fs/promises";
|
|
5277
|
+
import { dirname as dirname12 } from "path";
|
|
5025
5278
|
var SESSION_SCHEMA_VERSION = 1;
|
|
5026
5279
|
async function readSession(path) {
|
|
5027
5280
|
try {
|
|
5028
|
-
const raw = await
|
|
5281
|
+
const raw = await readFile16(path, "utf8");
|
|
5029
5282
|
const parsed = JSON.parse(raw);
|
|
5030
5283
|
if (parsed.schema_version !== SESSION_SCHEMA_VERSION) return null;
|
|
5031
5284
|
return parsed;
|
|
@@ -5034,8 +5287,8 @@ async function readSession(path) {
|
|
|
5034
5287
|
}
|
|
5035
5288
|
}
|
|
5036
5289
|
async function writeSession(path, state) {
|
|
5037
|
-
await
|
|
5038
|
-
await
|
|
5290
|
+
await mkdir11(dirname12(path), { recursive: true });
|
|
5291
|
+
await writeFile10(path, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
5039
5292
|
}
|
|
5040
5293
|
|
|
5041
5294
|
// src/server/routes/context-update.ts
|
|
@@ -5080,8 +5333,8 @@ async function handleContextUpdate(req, ctx) {
|
|
|
5080
5333
|
}
|
|
5081
5334
|
|
|
5082
5335
|
// src/server/routes/gate.ts
|
|
5083
|
-
import { appendFile as appendFile4, mkdir as
|
|
5084
|
-
import { dirname as
|
|
5336
|
+
import { appendFile as appendFile4, mkdir as mkdir12 } from "fs/promises";
|
|
5337
|
+
import { dirname as dirname13 } from "path";
|
|
5085
5338
|
var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
|
|
5086
5339
|
var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
|
|
5087
5340
|
function extractQuery(toolName, input) {
|
|
@@ -5137,7 +5390,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
|
|
|
5137
5390
|
}
|
|
5138
5391
|
async function logDecision(ctx, toolName, query, decision, reason) {
|
|
5139
5392
|
try {
|
|
5140
|
-
await
|
|
5393
|
+
await mkdir12(dirname13(ctx.paths.gateLog), { recursive: true });
|
|
5141
5394
|
const entry = {
|
|
5142
5395
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5143
5396
|
tool: toolName,
|
|
@@ -5209,15 +5462,15 @@ async function handleGate(req, ctx) {
|
|
|
5209
5462
|
}
|
|
5210
5463
|
|
|
5211
5464
|
// src/server/routes/log.ts
|
|
5212
|
-
import { appendFile as appendFile5, mkdir as
|
|
5213
|
-
import { dirname as
|
|
5465
|
+
import { appendFile as appendFile5, mkdir as mkdir13 } from "fs/promises";
|
|
5466
|
+
import { dirname as dirname14 } from "path";
|
|
5214
5467
|
async function handleLog(entry, ctx) {
|
|
5215
5468
|
if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
|
|
5216
5469
|
throw new Error("log: input_tokens and output_tokens (number) are required");
|
|
5217
5470
|
}
|
|
5218
5471
|
const written_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
5219
5472
|
const record = { ...entry, written_at };
|
|
5220
|
-
await
|
|
5473
|
+
await mkdir13(dirname14(ctx.paths.tokenLog), { recursive: true });
|
|
5221
5474
|
await appendFile5(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
|
|
5222
5475
|
return { ok: true, written_at };
|
|
5223
5476
|
}
|
|
@@ -5403,7 +5656,7 @@ async function startServer(paths, options = {}) {
|
|
|
5403
5656
|
const port = options.port ?? await findFreePort();
|
|
5404
5657
|
const app = buildApp(ctx, port);
|
|
5405
5658
|
const nodeServer = serve2({ fetch: app.fetch, port, hostname: "127.0.0.1" });
|
|
5406
|
-
await
|
|
5659
|
+
await writeFile11(paths.mcpPort, String(port), "utf8");
|
|
5407
5660
|
const fileWatcher = createFileWatcher(paths.projectRoot, (e) => ctx.activity.add(e));
|
|
5408
5661
|
const gitWatcher = createGitWatcher(paths.projectRoot, async (e) => {
|
|
5409
5662
|
await ctx.activity.add(e);
|
|
@@ -5547,7 +5800,7 @@ async function dashboardCommand(rawPath) {
|
|
|
5547
5800
|
}
|
|
5548
5801
|
|
|
5549
5802
|
// src/cli/doctor-command.ts
|
|
5550
|
-
import { readFile as
|
|
5803
|
+
import { readFile as readFile17, stat as stat4 } from "fs/promises";
|
|
5551
5804
|
import { join as join10, resolve as resolve3 } from "path";
|
|
5552
5805
|
import spawn from "cross-spawn";
|
|
5553
5806
|
var ICON = { ok: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
|
|
@@ -5616,7 +5869,7 @@ async function runDoctorChecks(projectRoot) {
|
|
|
5616
5869
|
});
|
|
5617
5870
|
} else {
|
|
5618
5871
|
try {
|
|
5619
|
-
const graph = JSON.parse(await
|
|
5872
|
+
const graph = JSON.parse(await readFile17(paths.infoGraph, "utf8"));
|
|
5620
5873
|
const parts = [`${graph.symbol_count} symbols`, `${graph.file_count} files`];
|
|
5621
5874
|
let status = "ok";
|
|
5622
5875
|
const ageMs = Date.now() - Date.parse(graph.generated_at);
|
|
@@ -5657,7 +5910,7 @@ async function runDoctorChecks(projectRoot) {
|
|
|
5657
5910
|
detail: "no CLAUDE.md \u2014 run `syn .` to scaffold + inject the policy block."
|
|
5658
5911
|
});
|
|
5659
5912
|
} else {
|
|
5660
|
-
const md = await
|
|
5913
|
+
const md = await readFile17(paths.claudeMd, "utf8");
|
|
5661
5914
|
if (md.includes(`synthra-policy v${POLICY_VERSION} BEGIN`)) {
|
|
5662
5915
|
checks.push({
|
|
5663
5916
|
status: "ok",
|
|
@@ -5680,7 +5933,7 @@ async function runDoctorChecks(projectRoot) {
|
|
|
5680
5933
|
detail: "no .claude/settings.local.json \u2014 run `syn .` to install hooks."
|
|
5681
5934
|
});
|
|
5682
5935
|
} else {
|
|
5683
|
-
const s = await
|
|
5936
|
+
const s = await readFile17(paths.claudeSettings, "utf8");
|
|
5684
5937
|
checks.push(
|
|
5685
5938
|
s.includes("synthra-hook=true") ? { status: "ok", label: "Hooks", detail: "registered in .claude/settings.local.json" } : {
|
|
5686
5939
|
status: "warn",
|
|
@@ -5710,7 +5963,7 @@ async function doctorCommand(rawPath) {
|
|
|
5710
5963
|
}
|
|
5711
5964
|
|
|
5712
5965
|
// src/cli/self-update.ts
|
|
5713
|
-
import { mkdir as
|
|
5966
|
+
import { mkdir as mkdir14, readFile as readFile18, writeFile as writeFile12 } from "fs/promises";
|
|
5714
5967
|
import { homedir as homedir3 } from "os";
|
|
5715
5968
|
import { join as join11 } from "path";
|
|
5716
5969
|
import { createInterface } from "readline/promises";
|
|
@@ -5768,7 +6021,7 @@ async function checkForUpdate() {
|
|
|
5768
6021
|
}
|
|
5769
6022
|
async function readLastSeen() {
|
|
5770
6023
|
try {
|
|
5771
|
-
const raw = await
|
|
6024
|
+
const raw = await readFile18(LAST_SEEN_PATH, "utf8");
|
|
5772
6025
|
const parsed = JSON.parse(raw);
|
|
5773
6026
|
return parsed.version ?? null;
|
|
5774
6027
|
} catch {
|
|
@@ -5777,9 +6030,9 @@ async function readLastSeen() {
|
|
|
5777
6030
|
}
|
|
5778
6031
|
async function writeLastSeen(version) {
|
|
5779
6032
|
try {
|
|
5780
|
-
await
|
|
6033
|
+
await mkdir14(SYNTHRA_DIR, { recursive: true });
|
|
5781
6034
|
const data = { version, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5782
|
-
await
|
|
6035
|
+
await writeFile12(LAST_SEEN_PATH, JSON.stringify(data, null, 2), "utf8");
|
|
5783
6036
|
} catch {
|
|
5784
6037
|
}
|
|
5785
6038
|
}
|
|
@@ -5811,7 +6064,7 @@ async function readInstalledChangelog() {
|
|
|
5811
6064
|
const root = await npmGlobalRoot();
|
|
5812
6065
|
if (!root) return null;
|
|
5813
6066
|
try {
|
|
5814
|
-
return await
|
|
6067
|
+
return await readFile18(join11(root, "@jefuriiij", "synthra", "CHANGELOG.md"), "utf8");
|
|
5815
6068
|
} catch {
|
|
5816
6069
|
return null;
|
|
5817
6070
|
}
|
|
@@ -6018,7 +6271,7 @@ async function defaultFlow(rawPath, opts) {
|
|
|
6018
6271
|
await runStartupChangelogCheck();
|
|
6019
6272
|
await promptForUpdateOrLog();
|
|
6020
6273
|
await recordProject(projectRoot);
|
|
6021
|
-
const scan = await scanCommand(rawPath);
|
|
6274
|
+
const scan = await scanCommand(rawPath, { full: opts.full });
|
|
6022
6275
|
const mcpHandle = await startServer(paths);
|
|
6023
6276
|
let dashboardHandle = null;
|
|
6024
6277
|
try {
|
|
@@ -6065,11 +6318,11 @@ function buildProgram() {
|
|
|
6065
6318
|
{
|
|
6066
6319
|
default: true
|
|
6067
6320
|
}
|
|
6068
|
-
).option("--resume <id>", "Resume an existing Claude session (only with --launch-cli)").option("--launch-cli", "Also spawn `claude` CLI in this terminal (legacy M3 behavior)", false).action(async (path, opts) => {
|
|
6321
|
+
).option("--resume <id>", "Resume an existing Claude session (only with --launch-cli)").option("--launch-cli", "Also spawn `claude` CLI in this terminal (legacy M3 behavior)", false).option("--full", "Re-parse every file, ignoring the incremental parse cache", false).action(async (path, opts) => {
|
|
6069
6322
|
await defaultFlow(path ?? ".", opts);
|
|
6070
6323
|
});
|
|
6071
|
-
prog.command("scan [path]", "Scan only \u2014 walk + parse + write graph.").action(async (path) => {
|
|
6072
|
-
await scanCommand(path ?? ".");
|
|
6324
|
+
prog.command("scan [path]", "Scan only \u2014 walk + parse + write graph.").option("--full", "Re-parse every file, ignoring the incremental parse cache", false).action(async (path, opts) => {
|
|
6325
|
+
await scanCommand(path ?? ".", { full: opts.full });
|
|
6073
6326
|
});
|
|
6074
6327
|
prog.command("serve [path]", "Start the HTTP MCP server against a scanned project.").action(async (path) => {
|
|
6075
6328
|
await serveCommand(path ?? ".");
|