@jefuriiij/synthra 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/dist/cli/index.js +497 -249
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/index.js +128 -11
- 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.0",
|
|
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">
|
|
@@ -671,6 +799,16 @@ var public_default = `<!doctype html>
|
|
|
671
799
|
<div class="gate-mini" id="gate-mini"></div>
|
|
672
800
|
</div>
|
|
673
801
|
|
|
802
|
+
<!-- Hot files (usage-learning) -->
|
|
803
|
+
<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.">
|
|
804
|
+
<div class="card-head">
|
|
805
|
+
<div class="card-eyebrow">Hot <em>files</em></div>
|
|
806
|
+
<div class="card-meta" id="hot-files-project">active project</div>
|
|
807
|
+
</div>
|
|
808
|
+
<div class="moat-value"><span id="hot-files-total">0</span> <em>tracked</em></div>
|
|
809
|
+
<div class="cost-sub" id="hot-files-list"></div>
|
|
810
|
+
</div>
|
|
811
|
+
|
|
674
812
|
</aside>
|
|
675
813
|
</main>
|
|
676
814
|
|
|
@@ -1042,6 +1180,40 @@ var public_default = `<!doctype html>
|
|
|
1042
1180
|
el.appendChild(frag);
|
|
1043
1181
|
}
|
|
1044
1182
|
|
|
1183
|
+
function shortenPath(p) {
|
|
1184
|
+
const parts = String(p).split('/');
|
|
1185
|
+
return parts.length <= 2 ? p : '\u2026/' + parts.slice(-2).join('/');
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function renderHotFiles(active) {
|
|
1189
|
+
const stats = (active && active.stats) || {};
|
|
1190
|
+
const files = stats.hot_files || [];
|
|
1191
|
+
$('#hot-files-total').innerHTML = fmt(stats.hot_files_total || 0);
|
|
1192
|
+
const proj = $('#hot-files-project');
|
|
1193
|
+
if (proj) proj.textContent = (active && active.project_name) || 'active project';
|
|
1194
|
+
const el = $('#hot-files-list');
|
|
1195
|
+
el.innerHTML = '';
|
|
1196
|
+
if (!files.length) {
|
|
1197
|
+
el.innerHTML = '<div class="empty">No usage learned yet \u2014 Synthra learns as you read/edit files.</div>';
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
const frag = document.createDocumentFragment();
|
|
1201
|
+
for (const f of files) {
|
|
1202
|
+
const row = document.createElement('div');
|
|
1203
|
+
row.className = 'cs-row';
|
|
1204
|
+
row.title = f.path;
|
|
1205
|
+
const k = document.createElement('span');
|
|
1206
|
+
k.className = 'cs-k path';
|
|
1207
|
+
k.textContent = shortenPath(f.path);
|
|
1208
|
+
const v = document.createElement('span');
|
|
1209
|
+
v.className = 'cs-v';
|
|
1210
|
+
v.textContent = String(f.score);
|
|
1211
|
+
row.append(k, v);
|
|
1212
|
+
frag.appendChild(row);
|
|
1213
|
+
}
|
|
1214
|
+
el.appendChild(frag);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1045
1217
|
function renderTurns(turns) {
|
|
1046
1218
|
const tbody = $('#turns-body');
|
|
1047
1219
|
const empty = $('#turns-empty');
|
|
@@ -1308,6 +1480,7 @@ var public_default = `<!doctype html>
|
|
|
1308
1480
|
renderCostHero(data.global);
|
|
1309
1481
|
renderMoat(data.global);
|
|
1310
1482
|
renderToolUsage(data.global);
|
|
1483
|
+
renderHotFiles(data.active);
|
|
1311
1484
|
renderTurns(turns);
|
|
1312
1485
|
renderGateMini(gates);
|
|
1313
1486
|
|
|
@@ -1396,7 +1569,10 @@ var public_default = `<!doctype html>
|
|
|
1396
1569
|
`;
|
|
1397
1570
|
|
|
1398
1571
|
// 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';
|
|
1572
|
+
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/* 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.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 .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';
|
|
1573
|
+
|
|
1574
|
+
// src/dashboard/public/favicon.svg
|
|
1575
|
+
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
1576
|
|
|
1401
1577
|
// src/dashboard/server.ts
|
|
1402
1578
|
var FALLBACK_RANGE = 9;
|
|
@@ -1416,6 +1592,11 @@ async function startDashboard(paths, preferredPort = 8901) {
|
|
|
1416
1592
|
c.header("Cache-Control", "no-cache");
|
|
1417
1593
|
return c.body(style_default);
|
|
1418
1594
|
});
|
|
1595
|
+
app.get("/favicon.svg", (c) => {
|
|
1596
|
+
c.header("Content-Type", "image/svg+xml; charset=utf-8");
|
|
1597
|
+
c.header("Cache-Control", "public, max-age=86400");
|
|
1598
|
+
return c.body(favicon_default);
|
|
1599
|
+
});
|
|
1419
1600
|
app.get("/health", (c) => c.json({ ok: true }));
|
|
1420
1601
|
app.get("/data", async (c) => {
|
|
1421
1602
|
const data = await computeDashboardData(paths, RECENT_N);
|
|
@@ -1434,8 +1615,8 @@ async function startDashboard(paths, preferredPort = 8901) {
|
|
|
1434
1615
|
}
|
|
1435
1616
|
|
|
1436
1617
|
// src/hooks/installer.ts
|
|
1437
|
-
import { mkdir as
|
|
1438
|
-
import { dirname as
|
|
1618
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
1619
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
1439
1620
|
|
|
1440
1621
|
// src/hooks/scripts/pre-compact.ps1
|
|
1441
1622
|
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 +1920,7 @@ function chosenScriptExt() {
|
|
|
1739
1920
|
}
|
|
1740
1921
|
async function readSettings(path) {
|
|
1741
1922
|
try {
|
|
1742
|
-
const raw = await
|
|
1923
|
+
const raw = await readFile4(path, "utf8");
|
|
1743
1924
|
return JSON.parse(raw);
|
|
1744
1925
|
} catch {
|
|
1745
1926
|
return {};
|
|
@@ -1778,18 +1959,18 @@ function mergeOurHooks(config, paths) {
|
|
|
1778
1959
|
return config;
|
|
1779
1960
|
}
|
|
1780
1961
|
async function installHooks(paths) {
|
|
1781
|
-
await
|
|
1962
|
+
await mkdir3(paths.claudeHooksDir, { recursive: true });
|
|
1782
1963
|
const scriptsWritten = [];
|
|
1783
1964
|
for (const s of SCRIPTS) {
|
|
1784
1965
|
const target = join3(paths.claudeHooksDir, `${s.baseName}${chosenScriptExt()}`);
|
|
1785
|
-
await
|
|
1966
|
+
await writeFile3(target, chosenScriptBody(s), "utf8");
|
|
1786
1967
|
scriptsWritten.push(target);
|
|
1787
1968
|
}
|
|
1788
|
-
await
|
|
1969
|
+
await mkdir3(dirname3(paths.claudeSettings), { recursive: true });
|
|
1789
1970
|
const existing = await readSettings(paths.claudeSettings);
|
|
1790
1971
|
const stripped = stripOurHooks(existing);
|
|
1791
1972
|
const merged = mergeOurHooks(stripped, paths);
|
|
1792
|
-
await
|
|
1973
|
+
await writeFile3(paths.claudeSettings, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
1793
1974
|
log.debug(`installed ${scriptsWritten.length} hook script(s) into ${paths.claudeHooksDir}`);
|
|
1794
1975
|
return { scriptsWritten, settingsUpdated: true };
|
|
1795
1976
|
}
|
|
@@ -1797,11 +1978,11 @@ async function installHooks(paths) {
|
|
|
1797
1978
|
// src/server/http.ts
|
|
1798
1979
|
import { serve as serve2 } from "@hono/node-server";
|
|
1799
1980
|
import { Hono as Hono2 } from "hono";
|
|
1800
|
-
import { writeFile as
|
|
1981
|
+
import { writeFile as writeFile11 } from "fs/promises";
|
|
1801
1982
|
|
|
1802
1983
|
// src/activity/activity-log.ts
|
|
1803
|
-
import { appendFile, mkdir as
|
|
1804
|
-
import { dirname as
|
|
1984
|
+
import { appendFile as appendFile2, mkdir as mkdir4 } from "fs/promises";
|
|
1985
|
+
import { dirname as dirname4 } from "path";
|
|
1805
1986
|
var DEFAULT_RING_SIZE = 100;
|
|
1806
1987
|
var ActivityStore = class {
|
|
1807
1988
|
ring = [];
|
|
@@ -1838,8 +2019,8 @@ var ActivityStore = class {
|
|
|
1838
2019
|
}
|
|
1839
2020
|
async persist(event) {
|
|
1840
2021
|
try {
|
|
1841
|
-
await
|
|
1842
|
-
await
|
|
2022
|
+
await mkdir4(dirname4(this.persistPath), { recursive: true });
|
|
2023
|
+
await appendFile2(this.persistPath, JSON.stringify(event) + "\n", "utf8");
|
|
1843
2024
|
} catch {
|
|
1844
2025
|
}
|
|
1845
2026
|
}
|
|
@@ -1847,7 +2028,7 @@ var ActivityStore = class {
|
|
|
1847
2028
|
|
|
1848
2029
|
// src/activity/file-watcher.ts
|
|
1849
2030
|
import chokidar from "chokidar";
|
|
1850
|
-
import { readFile as
|
|
2031
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1851
2032
|
import { join as join4, relative, sep } from "path";
|
|
1852
2033
|
import ignore from "ignore";
|
|
1853
2034
|
var ALWAYS_IGNORE = [
|
|
@@ -1870,7 +2051,7 @@ var ALWAYS_IGNORE = [
|
|
|
1870
2051
|
];
|
|
1871
2052
|
async function readIgnoreFile(path) {
|
|
1872
2053
|
try {
|
|
1873
|
-
const text = await
|
|
2054
|
+
const text = await readFile5(path, "utf8");
|
|
1874
2055
|
return text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
1875
2056
|
} catch {
|
|
1876
2057
|
return [];
|
|
@@ -1935,14 +2116,14 @@ function createFileWatcher(root, onEvent) {
|
|
|
1935
2116
|
// src/activity/git-watcher.ts
|
|
1936
2117
|
import { execFile } from "child_process";
|
|
1937
2118
|
import { watch } from "fs";
|
|
1938
|
-
import { readFile as
|
|
2119
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1939
2120
|
import { join as join5 } from "path";
|
|
1940
2121
|
import { promisify } from "util";
|
|
1941
2122
|
var execFileAsync = promisify(execFile);
|
|
1942
2123
|
var POLL_MS = 2e3;
|
|
1943
2124
|
async function readHeadBranch(projectRoot) {
|
|
1944
2125
|
try {
|
|
1945
|
-
const head = await
|
|
2126
|
+
const head = await readFile6(join5(projectRoot, ".git", "HEAD"), "utf8");
|
|
1946
2127
|
const m = head.trim().match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
1947
2128
|
return m?.[1] ?? null;
|
|
1948
2129
|
} catch {
|
|
@@ -2041,10 +2222,10 @@ function parseStatusFiles(porcelain) {
|
|
|
2041
2222
|
import { resolve } from "path";
|
|
2042
2223
|
|
|
2043
2224
|
// src/scanner/extract.ts
|
|
2044
|
-
import { dirname as
|
|
2225
|
+
import { dirname as dirname5, join as join6, posix } from "path";
|
|
2045
2226
|
|
|
2046
2227
|
// src/graph/types.ts
|
|
2047
|
-
var SCHEMA_VERSION2 =
|
|
2228
|
+
var SCHEMA_VERSION2 = 2;
|
|
2048
2229
|
|
|
2049
2230
|
// src/scanner/hash.ts
|
|
2050
2231
|
import { createHash } from "crypto";
|
|
@@ -2362,14 +2543,20 @@ async function buildGraph(root, parsed) {
|
|
|
2362
2543
|
for (const p of parsed) filesByPath.set(p.file.relPath, true);
|
|
2363
2544
|
const nodes = [];
|
|
2364
2545
|
const edges = [];
|
|
2546
|
+
const symbolsByFile = /* @__PURE__ */ new Map();
|
|
2547
|
+
const callsByFile = /* @__PURE__ */ new Map();
|
|
2365
2548
|
for (const p of parsed) {
|
|
2366
2549
|
const fileNode = toFileNode(p);
|
|
2367
2550
|
nodes.push(fileNode);
|
|
2551
|
+
const fileSymNodes = [];
|
|
2368
2552
|
for (const sym of p.symbols) {
|
|
2369
2553
|
const symNode = toSymbolNode(p, sym);
|
|
2370
2554
|
nodes.push(symNode);
|
|
2555
|
+
fileSymNodes.push(symNode);
|
|
2371
2556
|
edges.push({ from: fileNode.id, to: symNode.id, kind: "defines" });
|
|
2372
2557
|
}
|
|
2558
|
+
symbolsByFile.set(p.file.relPath, fileSymNodes);
|
|
2559
|
+
callsByFile.set(p.file.relPath, p.calls);
|
|
2373
2560
|
const importEdges = /* @__PURE__ */ new Set();
|
|
2374
2561
|
for (const spec of p.imports) {
|
|
2375
2562
|
const target = resolveImport(p.file.relPath, spec, filesByPath);
|
|
@@ -2384,6 +2571,7 @@ async function buildGraph(root, parsed) {
|
|
|
2384
2571
|
edges.push({ from: fileNode.id, to: fileId(testTargetPath), kind: "tests" });
|
|
2385
2572
|
}
|
|
2386
2573
|
}
|
|
2574
|
+
edges.push(...buildCallEdges(symbolsByFile, callsByFile));
|
|
2387
2575
|
const symbolCount = nodes.filter((n) => n.kind === "symbol").length;
|
|
2388
2576
|
const fileCount = nodes.length - symbolCount;
|
|
2389
2577
|
return {
|
|
@@ -2407,9 +2595,52 @@ function buildSymbolIndex(graph) {
|
|
|
2407
2595
|
}
|
|
2408
2596
|
return out;
|
|
2409
2597
|
}
|
|
2598
|
+
function tightestContainer(syms, line) {
|
|
2599
|
+
let best = null;
|
|
2600
|
+
for (const s of syms) {
|
|
2601
|
+
if (line < s.start_line || line > s.end_line) continue;
|
|
2602
|
+
if (!best || s.end_line - s.start_line < best.end_line - best.start_line) best = s;
|
|
2603
|
+
}
|
|
2604
|
+
return best;
|
|
2605
|
+
}
|
|
2606
|
+
function buildCallEdges(symbolsByFile, callsByFile) {
|
|
2607
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2608
|
+
for (const syms of symbolsByFile.values()) {
|
|
2609
|
+
for (const s of syms) {
|
|
2610
|
+
const list = byName.get(s.name);
|
|
2611
|
+
if (list) list.push(s);
|
|
2612
|
+
else byName.set(s.name, [s]);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
const edges = [];
|
|
2616
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2617
|
+
for (const [relPath, sites] of callsByFile) {
|
|
2618
|
+
const fileSyms = symbolsByFile.get(relPath) ?? [];
|
|
2619
|
+
for (const site of sites) {
|
|
2620
|
+
const caller = tightestContainer(fileSyms, site.line);
|
|
2621
|
+
if (!caller) continue;
|
|
2622
|
+
let callee = fileSyms.find((s) => s.name === site.callee);
|
|
2623
|
+
if (!callee) {
|
|
2624
|
+
const cands = byName.get(site.callee) ?? [];
|
|
2625
|
+
if (cands.length !== 1) continue;
|
|
2626
|
+
callee = cands[0];
|
|
2627
|
+
}
|
|
2628
|
+
if (!callee || callee.id === caller.id) continue;
|
|
2629
|
+
const key = `${caller.id}->${callee.id}`;
|
|
2630
|
+
if (seen.has(key)) continue;
|
|
2631
|
+
seen.add(key);
|
|
2632
|
+
edges.push({ from: caller.id, to: callee.id, kind: "calls" });
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
return edges;
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
// src/scanner/parse-cache.ts
|
|
2639
|
+
import { mkdir as mkdir5, readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
|
|
2640
|
+
import { dirname as dirname6 } from "path";
|
|
2410
2641
|
|
|
2411
2642
|
// src/scanner/parser.ts
|
|
2412
|
-
import { readFile as
|
|
2643
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2413
2644
|
import { createRequire } from "module";
|
|
2414
2645
|
import { Language, Parser } from "web-tree-sitter";
|
|
2415
2646
|
|
|
@@ -2425,10 +2656,11 @@ function cleanImport(s) {
|
|
|
2425
2656
|
async function runGenericParser(config, f, source) {
|
|
2426
2657
|
let symbols = [];
|
|
2427
2658
|
let imports = [];
|
|
2659
|
+
const calls = [];
|
|
2428
2660
|
try {
|
|
2429
2661
|
const { parser, language } = await createParser(config.grammar);
|
|
2430
2662
|
const tree = parser.parse(source);
|
|
2431
|
-
if (!tree) return { file: f, source, symbols, imports, calls
|
|
2663
|
+
if (!tree) return { file: f, source, symbols, imports, calls };
|
|
2432
2664
|
const query = new Query(language, config.query);
|
|
2433
2665
|
const matches = query.matches(tree.rootNode);
|
|
2434
2666
|
for (const match of matches) {
|
|
@@ -2453,6 +2685,14 @@ async function runGenericParser(config, f, source) {
|
|
|
2453
2685
|
});
|
|
2454
2686
|
continue;
|
|
2455
2687
|
}
|
|
2688
|
+
if (config.callCapture && config.callCalleeCapture) {
|
|
2689
|
+
const callNode = byName.get(config.callCapture);
|
|
2690
|
+
const calleeNode = byName.get(config.callCalleeCapture);
|
|
2691
|
+
if (callNode && calleeNode) {
|
|
2692
|
+
calls.push({ callee: calleeNode.text, line: callNode.startPosition.row + 1 });
|
|
2693
|
+
continue;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2456
2696
|
if (config.importCapture) {
|
|
2457
2697
|
const imp = byName.get(config.importCapture);
|
|
2458
2698
|
if (imp) imports.push(cleanImport(imp.text));
|
|
@@ -2468,7 +2708,7 @@ async function runGenericParser(config, f, source) {
|
|
|
2468
2708
|
imports = Array.from(new Set(imports)).filter((s) => s.length > 0);
|
|
2469
2709
|
} catch {
|
|
2470
2710
|
}
|
|
2471
|
-
return { file: f, source, symbols, imports, calls
|
|
2711
|
+
return { file: f, source, symbols, imports, calls };
|
|
2472
2712
|
}
|
|
2473
2713
|
|
|
2474
2714
|
// src/scanner/parsers/c.ts
|
|
@@ -2479,6 +2719,7 @@ var QUERY = `
|
|
|
2479
2719
|
(type_definition declarator: (type_identifier) @type.name) @type
|
|
2480
2720
|
(preproc_include path: (string_literal) @import)
|
|
2481
2721
|
(preproc_include path: (system_lib_string) @import)
|
|
2722
|
+
(call_expression function: (identifier) @call.name) @call
|
|
2482
2723
|
`;
|
|
2483
2724
|
async function parseC(f, source) {
|
|
2484
2725
|
return runGenericParser(
|
|
@@ -2491,7 +2732,9 @@ async function parseC(f, source) {
|
|
|
2491
2732
|
{ declCapture: "enum", nameCapture: "enum.name", kind: "enum" },
|
|
2492
2733
|
{ declCapture: "type", nameCapture: "type.name", kind: "type" }
|
|
2493
2734
|
],
|
|
2494
|
-
importCapture: "import"
|
|
2735
|
+
importCapture: "import",
|
|
2736
|
+
callCapture: "call",
|
|
2737
|
+
callCalleeCapture: "call.name"
|
|
2495
2738
|
},
|
|
2496
2739
|
f,
|
|
2497
2740
|
source
|
|
@@ -2508,6 +2751,9 @@ var QUERY2 = `
|
|
|
2508
2751
|
(namespace_definition name: (namespace_identifier) @namespace.name) @namespace
|
|
2509
2752
|
(preproc_include path: (string_literal) @import)
|
|
2510
2753
|
(preproc_include path: (system_lib_string) @import)
|
|
2754
|
+
(call_expression function: (identifier) @call.name) @call
|
|
2755
|
+
(call_expression function: (field_expression field: (field_identifier) @call.name)) @call
|
|
2756
|
+
(call_expression function: (qualified_identifier name: (identifier) @call.name)) @call
|
|
2511
2757
|
`;
|
|
2512
2758
|
async function parseCpp(f, source) {
|
|
2513
2759
|
return runGenericParser(
|
|
@@ -2522,7 +2768,9 @@ async function parseCpp(f, source) {
|
|
|
2522
2768
|
{ declCapture: "enum", nameCapture: "enum.name", kind: "enum" },
|
|
2523
2769
|
{ declCapture: "namespace", nameCapture: "namespace.name", kind: "class" }
|
|
2524
2770
|
],
|
|
2525
|
-
importCapture: "import"
|
|
2771
|
+
importCapture: "import",
|
|
2772
|
+
callCapture: "call",
|
|
2773
|
+
callCalleeCapture: "call.name"
|
|
2526
2774
|
},
|
|
2527
2775
|
f,
|
|
2528
2776
|
source
|
|
@@ -2538,6 +2786,8 @@ var QUERY3 = `
|
|
|
2538
2786
|
(method_declaration name: (identifier) @method.name) @method
|
|
2539
2787
|
(namespace_declaration name: (_) @namespace.name) @namespace
|
|
2540
2788
|
(using_directive (_) @import)
|
|
2789
|
+
(invocation_expression function: (identifier) @call.name) @call
|
|
2790
|
+
(invocation_expression function: (member_access_expression name: (identifier) @call.name)) @call
|
|
2541
2791
|
`;
|
|
2542
2792
|
async function parseCSharp(f, source) {
|
|
2543
2793
|
return runGenericParser(
|
|
@@ -2552,7 +2802,9 @@ async function parseCSharp(f, source) {
|
|
|
2552
2802
|
{ declCapture: "method", nameCapture: "method.name", kind: "method" },
|
|
2553
2803
|
{ declCapture: "namespace", nameCapture: "namespace.name", kind: "class" }
|
|
2554
2804
|
],
|
|
2555
|
-
importCapture: "import"
|
|
2805
|
+
importCapture: "import",
|
|
2806
|
+
callCapture: "call",
|
|
2807
|
+
callCalleeCapture: "call.name"
|
|
2556
2808
|
},
|
|
2557
2809
|
f,
|
|
2558
2810
|
source
|
|
@@ -2657,6 +2909,8 @@ var QUERY5 = `
|
|
|
2657
2909
|
(method_declaration name: (field_identifier) @method.name) @method
|
|
2658
2910
|
(type_spec name: (type_identifier) @type.name) @type
|
|
2659
2911
|
(import_spec path: (interpreted_string_literal) @import)
|
|
2912
|
+
(call_expression function: (identifier) @call.name) @call
|
|
2913
|
+
(call_expression function: (selector_expression field: (field_identifier) @call.name)) @call
|
|
2660
2914
|
`;
|
|
2661
2915
|
async function parseGo(f, source) {
|
|
2662
2916
|
return runGenericParser(
|
|
@@ -2668,7 +2922,9 @@ async function parseGo(f, source) {
|
|
|
2668
2922
|
{ declCapture: "method", nameCapture: "method.name", kind: "method" },
|
|
2669
2923
|
{ declCapture: "type", nameCapture: "type.name", kind: "type" }
|
|
2670
2924
|
],
|
|
2671
|
-
importCapture: "import"
|
|
2925
|
+
importCapture: "import",
|
|
2926
|
+
callCapture: "call",
|
|
2927
|
+
callCalleeCapture: "call.name"
|
|
2672
2928
|
},
|
|
2673
2929
|
f,
|
|
2674
2930
|
source
|
|
@@ -2733,6 +2989,7 @@ var QUERY6 = `
|
|
|
2733
2989
|
(method_declaration name: (identifier) @method.name) @method
|
|
2734
2990
|
(enum_declaration name: (identifier) @enum.name) @enum
|
|
2735
2991
|
(import_declaration (scoped_identifier) @import)
|
|
2992
|
+
(method_invocation name: (identifier) @call.name) @call
|
|
2736
2993
|
`;
|
|
2737
2994
|
async function parseJava(f, source) {
|
|
2738
2995
|
return runGenericParser(
|
|
@@ -2745,7 +3002,9 @@ async function parseJava(f, source) {
|
|
|
2745
3002
|
{ declCapture: "method", nameCapture: "method.name", kind: "method" },
|
|
2746
3003
|
{ declCapture: "enum", nameCapture: "enum.name", kind: "enum" }
|
|
2747
3004
|
],
|
|
2748
|
-
importCapture: "import"
|
|
3005
|
+
importCapture: "import",
|
|
3006
|
+
callCapture: "call",
|
|
3007
|
+
callCalleeCapture: "call.name"
|
|
2749
3008
|
},
|
|
2750
3009
|
f,
|
|
2751
3010
|
source
|
|
@@ -2758,6 +3017,7 @@ var QUERY7 = `
|
|
|
2758
3017
|
(class_declaration (type_identifier) @class.name) @class
|
|
2759
3018
|
(object_declaration (type_identifier) @object.name) @object
|
|
2760
3019
|
(import_header (identifier) @import)
|
|
3020
|
+
(call_expression (simple_identifier) @call.name) @call
|
|
2761
3021
|
`;
|
|
2762
3022
|
async function parseKotlin(f, source) {
|
|
2763
3023
|
return runGenericParser(
|
|
@@ -2769,7 +3029,9 @@ async function parseKotlin(f, source) {
|
|
|
2769
3029
|
{ declCapture: "class", nameCapture: "class.name", kind: "class" },
|
|
2770
3030
|
{ declCapture: "object", nameCapture: "object.name", kind: "class" }
|
|
2771
3031
|
],
|
|
2772
|
-
importCapture: "import"
|
|
3032
|
+
importCapture: "import",
|
|
3033
|
+
callCapture: "call",
|
|
3034
|
+
callCalleeCapture: "call.name"
|
|
2773
3035
|
},
|
|
2774
3036
|
f,
|
|
2775
3037
|
source
|
|
@@ -2783,6 +3045,9 @@ var QUERY8 = `
|
|
|
2783
3045
|
(interface_declaration name: (name) @interface.name) @interface
|
|
2784
3046
|
(trait_declaration name: (name) @trait.name) @trait
|
|
2785
3047
|
(method_declaration name: (name) @method.name) @method
|
|
3048
|
+
(function_call_expression function: (name) @call.name) @call
|
|
3049
|
+
(member_call_expression name: (name) @call.name) @call
|
|
3050
|
+
(scoped_call_expression name: (name) @call.name) @call
|
|
2786
3051
|
`;
|
|
2787
3052
|
async function parsePhp(f, source) {
|
|
2788
3053
|
return runGenericParser(
|
|
@@ -2795,7 +3060,9 @@ async function parsePhp(f, source) {
|
|
|
2795
3060
|
{ declCapture: "interface", nameCapture: "interface.name", kind: "interface" },
|
|
2796
3061
|
{ declCapture: "trait", nameCapture: "trait.name", kind: "class" },
|
|
2797
3062
|
{ declCapture: "method", nameCapture: "method.name", kind: "method" }
|
|
2798
|
-
]
|
|
3063
|
+
],
|
|
3064
|
+
callCapture: "call",
|
|
3065
|
+
callCalleeCapture: "call.name"
|
|
2799
3066
|
},
|
|
2800
3067
|
f,
|
|
2801
3068
|
source
|
|
@@ -2810,6 +3077,8 @@ var QUERY9 = `
|
|
|
2810
3077
|
(import_statement name: (dotted_name) @import.module)
|
|
2811
3078
|
(import_from_statement module_name: (dotted_name) @import.from)
|
|
2812
3079
|
(import_from_statement module_name: (relative_import) @import.from)
|
|
3080
|
+
(call function: (identifier) @call.name) @call
|
|
3081
|
+
(call function: (attribute attribute: (identifier) @call.name)) @call
|
|
2813
3082
|
`;
|
|
2814
3083
|
function firstLine3(text, max = 200) {
|
|
2815
3084
|
const line = text.split(/\r?\n/, 1)[0] ?? "";
|
|
@@ -2818,10 +3087,11 @@ function firstLine3(text, max = 200) {
|
|
|
2818
3087
|
async function parsePython(f, source) {
|
|
2819
3088
|
let symbols = [];
|
|
2820
3089
|
let imports = [];
|
|
3090
|
+
const calls = [];
|
|
2821
3091
|
try {
|
|
2822
3092
|
const { parser, language } = await createParser("python");
|
|
2823
3093
|
const tree = parser.parse(source);
|
|
2824
|
-
if (!tree) return { file: f, source, symbols, imports, calls
|
|
3094
|
+
if (!tree) return { file: f, source, symbols, imports, calls };
|
|
2825
3095
|
const query = new Query3(language, QUERY9);
|
|
2826
3096
|
const matches = query.matches(tree.rootNode);
|
|
2827
3097
|
for (const match of matches) {
|
|
@@ -2854,7 +3124,15 @@ async function parsePython(f, source) {
|
|
|
2854
3124
|
continue;
|
|
2855
3125
|
}
|
|
2856
3126
|
const importNode = byName.get("import.module") ?? byName.get("import.from");
|
|
2857
|
-
if (importNode)
|
|
3127
|
+
if (importNode) {
|
|
3128
|
+
imports.push(importNode.text);
|
|
3129
|
+
continue;
|
|
3130
|
+
}
|
|
3131
|
+
const callName = byName.get("call.name");
|
|
3132
|
+
const callNode = byName.get("call");
|
|
3133
|
+
if (callName && callNode) {
|
|
3134
|
+
calls.push({ callee: callName.text, line: callNode.startPosition.row + 1 });
|
|
3135
|
+
}
|
|
2858
3136
|
}
|
|
2859
3137
|
const seen = /* @__PURE__ */ new Set();
|
|
2860
3138
|
symbols = symbols.filter((s) => {
|
|
@@ -2866,7 +3144,7 @@ async function parsePython(f, source) {
|
|
|
2866
3144
|
imports = Array.from(new Set(imports));
|
|
2867
3145
|
} catch {
|
|
2868
3146
|
}
|
|
2869
|
-
return { file: f, source, symbols, imports, calls
|
|
3147
|
+
return { file: f, source, symbols, imports, calls };
|
|
2870
3148
|
}
|
|
2871
3149
|
|
|
2872
3150
|
// src/scanner/parsers/ruby.ts
|
|
@@ -2875,6 +3153,7 @@ var QUERY10 = `
|
|
|
2875
3153
|
(singleton_method name: (identifier) @method.name) @method
|
|
2876
3154
|
(class name: (constant) @class.name) @class
|
|
2877
3155
|
(module name: (constant) @module.name) @module
|
|
3156
|
+
(call method: (identifier) @call.name) @call
|
|
2878
3157
|
`;
|
|
2879
3158
|
async function parseRuby(f, source) {
|
|
2880
3159
|
return runGenericParser(
|
|
@@ -2886,7 +3165,9 @@ async function parseRuby(f, source) {
|
|
|
2886
3165
|
{ declCapture: "method", nameCapture: "method.name", kind: "method" },
|
|
2887
3166
|
{ declCapture: "class", nameCapture: "class.name", kind: "class" },
|
|
2888
3167
|
{ declCapture: "module", nameCapture: "module.name", kind: "class" }
|
|
2889
|
-
]
|
|
3168
|
+
],
|
|
3169
|
+
callCapture: "call",
|
|
3170
|
+
callCalleeCapture: "call.name"
|
|
2890
3171
|
},
|
|
2891
3172
|
f,
|
|
2892
3173
|
source
|
|
@@ -2900,6 +3181,9 @@ var QUERY11 = `
|
|
|
2900
3181
|
(enum_item name: (type_identifier) @enum.name) @enum
|
|
2901
3182
|
(trait_item name: (type_identifier) @trait.name) @trait
|
|
2902
3183
|
(impl_item type: (type_identifier) @impl.name) @impl
|
|
3184
|
+
(call_expression function: (identifier) @call.name) @call
|
|
3185
|
+
(call_expression function: (scoped_identifier name: (identifier) @call.name)) @call
|
|
3186
|
+
(call_expression function: (field_expression field: (field_identifier) @call.name)) @call
|
|
2903
3187
|
`;
|
|
2904
3188
|
async function parseRust(f, source) {
|
|
2905
3189
|
return runGenericParser(
|
|
@@ -2912,7 +3196,9 @@ async function parseRust(f, source) {
|
|
|
2912
3196
|
{ declCapture: "enum", nameCapture: "enum.name", kind: "enum" },
|
|
2913
3197
|
{ declCapture: "trait", nameCapture: "trait.name", kind: "interface" },
|
|
2914
3198
|
{ declCapture: "impl", nameCapture: "impl.name", kind: "class" }
|
|
2915
|
-
]
|
|
3199
|
+
],
|
|
3200
|
+
callCapture: "call",
|
|
3201
|
+
callCalleeCapture: "call.name"
|
|
2916
3202
|
},
|
|
2917
3203
|
f,
|
|
2918
3204
|
source
|
|
@@ -2931,6 +3217,8 @@ var TS_QUERY = `
|
|
|
2931
3217
|
(lexical_declaration (variable_declarator name: (identifier) @const-fn.name value: [(arrow_function) (function_expression)])) @const-fn
|
|
2932
3218
|
(assignment_expression left: (member_expression property: (property_identifier) @member-fn.name) right: [(arrow_function) (function_expression)]) @member-fn
|
|
2933
3219
|
(import_statement source: (string) @import)
|
|
3220
|
+
(call_expression function: (identifier) @call.name) @call
|
|
3221
|
+
(call_expression function: (member_expression property: (property_identifier) @call.name)) @call
|
|
2934
3222
|
`;
|
|
2935
3223
|
var JS_QUERY = `
|
|
2936
3224
|
(function_declaration name: (identifier) @function.name) @function
|
|
@@ -2940,6 +3228,8 @@ var JS_QUERY = `
|
|
|
2940
3228
|
(assignment_expression left: (member_expression property: (property_identifier) @member-fn.name) right: [(arrow_function) (function_expression)]) @member-fn
|
|
2941
3229
|
(import_statement source: (string) @import)
|
|
2942
3230
|
(call_expression function: (identifier) @_require_fn arguments: (arguments . (string) @require_source))
|
|
3231
|
+
(call_expression function: (identifier) @call.name) @call
|
|
3232
|
+
(call_expression function: (member_expression property: (property_identifier) @call.name)) @call
|
|
2943
3233
|
`;
|
|
2944
3234
|
function grammarFor(ext) {
|
|
2945
3235
|
if (ext === ".tsx" || ext === ".jsx") return "tsx";
|
|
@@ -2968,10 +3258,11 @@ async function parseTypeScript(f, source) {
|
|
|
2968
3258
|
const grammar = grammarFor(f.ext);
|
|
2969
3259
|
let symbols = [];
|
|
2970
3260
|
let imports = [];
|
|
3261
|
+
const calls = [];
|
|
2971
3262
|
try {
|
|
2972
3263
|
const { parser, language } = await createParser(grammar);
|
|
2973
3264
|
const tree = parser.parse(source);
|
|
2974
|
-
if (!tree) return { file: f, source, symbols, imports, calls
|
|
3265
|
+
if (!tree) return { file: f, source, symbols, imports, calls };
|
|
2975
3266
|
const query = new Query4(language, queryFor(grammar));
|
|
2976
3267
|
const matches = query.matches(tree.rootNode);
|
|
2977
3268
|
for (const match of matches) {
|
|
@@ -2997,6 +3288,12 @@ async function parseTypeScript(f, source) {
|
|
|
2997
3288
|
const requireSource = byName.get("require_source");
|
|
2998
3289
|
if (requireFn && requireSource && requireFn.text === "require") {
|
|
2999
3290
|
imports.push(unquote(requireSource.text));
|
|
3291
|
+
continue;
|
|
3292
|
+
}
|
|
3293
|
+
const callName = byName.get("call.name");
|
|
3294
|
+
const callNode = byName.get("call");
|
|
3295
|
+
if (callName && callNode) {
|
|
3296
|
+
calls.push({ callee: callName.text, line: callNode.startPosition.row + 1 });
|
|
3000
3297
|
}
|
|
3001
3298
|
}
|
|
3002
3299
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3009,7 +3306,7 @@ async function parseTypeScript(f, source) {
|
|
|
3009
3306
|
imports = Array.from(new Set(imports));
|
|
3010
3307
|
} catch {
|
|
3011
3308
|
}
|
|
3012
|
-
return { file: f, source, symbols, imports, calls
|
|
3309
|
+
return { file: f, source, symbols, imports, calls };
|
|
3013
3310
|
}
|
|
3014
3311
|
|
|
3015
3312
|
// src/scanner/parsers/svelte.ts
|
|
@@ -3043,6 +3340,7 @@ async function parseSvelte(f, source) {
|
|
|
3043
3340
|
});
|
|
3044
3341
|
}
|
|
3045
3342
|
for (const imp of parsed.imports) out.imports.push(imp);
|
|
3343
|
+
for (const call of parsed.calls) out.calls.push({ ...call, line: call.line + offset });
|
|
3046
3344
|
}
|
|
3047
3345
|
out.symbols.push({
|
|
3048
3346
|
name: f.relPath.split("/").pop()?.replace(/\.svelte$/i, "") ?? f.relPath,
|
|
@@ -3086,6 +3384,7 @@ async function parseVue(f, source) {
|
|
|
3086
3384
|
});
|
|
3087
3385
|
}
|
|
3088
3386
|
for (const imp of parsed.imports) out.imports.push(imp);
|
|
3387
|
+
for (const call of parsed.calls) out.calls.push({ ...call, line: call.line + offset });
|
|
3089
3388
|
}
|
|
3090
3389
|
out.symbols.push({
|
|
3091
3390
|
name: f.relPath.split("/").pop()?.replace(/\.vue$/i, "") ?? f.relPath,
|
|
@@ -3142,13 +3441,7 @@ async function createParser(name) {
|
|
|
3142
3441
|
function emptyParsed(file, source) {
|
|
3143
3442
|
return { file, source, symbols: [], imports: [], calls: [] };
|
|
3144
3443
|
}
|
|
3145
|
-
async function
|
|
3146
|
-
let source;
|
|
3147
|
-
try {
|
|
3148
|
-
source = await readFile6(f.absPath, "utf8");
|
|
3149
|
-
} catch {
|
|
3150
|
-
return emptyParsed(f, "");
|
|
3151
|
-
}
|
|
3444
|
+
async function parseSource(f, source) {
|
|
3152
3445
|
switch (f.ext) {
|
|
3153
3446
|
case ".ts":
|
|
3154
3447
|
case ".tsx":
|
|
@@ -3201,8 +3494,72 @@ async function parseFile(f) {
|
|
|
3201
3494
|
}
|
|
3202
3495
|
}
|
|
3203
3496
|
|
|
3497
|
+
// src/scanner/parse-cache.ts
|
|
3498
|
+
var PARSE_CACHE_VERSION = 2;
|
|
3499
|
+
function emptyParseCache() {
|
|
3500
|
+
return { schema_version: PARSE_CACHE_VERSION, files: {} };
|
|
3501
|
+
}
|
|
3502
|
+
async function readParseCache(path) {
|
|
3503
|
+
try {
|
|
3504
|
+
const raw = await readFile8(path, "utf8");
|
|
3505
|
+
const parsed = JSON.parse(raw);
|
|
3506
|
+
if (parsed.schema_version !== PARSE_CACHE_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
|
|
3507
|
+
return emptyParseCache();
|
|
3508
|
+
}
|
|
3509
|
+
return { schema_version: PARSE_CACHE_VERSION, files: parsed.files };
|
|
3510
|
+
} catch {
|
|
3511
|
+
return emptyParseCache();
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
async function writeParseCache(path, cache) {
|
|
3515
|
+
try {
|
|
3516
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
3517
|
+
await writeFile4(path, `${JSON.stringify(cache)}
|
|
3518
|
+
`, "utf8");
|
|
3519
|
+
} catch {
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
async function incrementalParse(parsable, prev, opts = {}) {
|
|
3523
|
+
const cache = emptyParseCache();
|
|
3524
|
+
const parsed = [];
|
|
3525
|
+
let reused = 0;
|
|
3526
|
+
let reparsed = 0;
|
|
3527
|
+
let parseErrors = 0;
|
|
3528
|
+
for (const f of parsable) {
|
|
3529
|
+
let source;
|
|
3530
|
+
try {
|
|
3531
|
+
source = await readFile8(f.absPath, "utf8");
|
|
3532
|
+
} catch {
|
|
3533
|
+
continue;
|
|
3534
|
+
}
|
|
3535
|
+
const hash = fileHash(source);
|
|
3536
|
+
const cached = opts.full ? void 0 : prev.files[f.relPath];
|
|
3537
|
+
if (cached && cached.hash === hash) {
|
|
3538
|
+
parsed.push({
|
|
3539
|
+
file: f,
|
|
3540
|
+
source,
|
|
3541
|
+
symbols: cached.symbols,
|
|
3542
|
+
imports: cached.imports,
|
|
3543
|
+
calls: cached.calls
|
|
3544
|
+
});
|
|
3545
|
+
cache.files[f.relPath] = cached;
|
|
3546
|
+
reused += 1;
|
|
3547
|
+
continue;
|
|
3548
|
+
}
|
|
3549
|
+
try {
|
|
3550
|
+
const p = await parseSource(f, source);
|
|
3551
|
+
parsed.push(p);
|
|
3552
|
+
cache.files[f.relPath] = { hash, symbols: p.symbols, imports: p.imports, calls: p.calls };
|
|
3553
|
+
reparsed += 1;
|
|
3554
|
+
} catch {
|
|
3555
|
+
parseErrors += 1;
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
return { parsed, cache, reused, reparsed, parseErrors };
|
|
3559
|
+
}
|
|
3560
|
+
|
|
3204
3561
|
// src/scanner/walker.ts
|
|
3205
|
-
import { readFile as
|
|
3562
|
+
import { readFile as readFile9, readdir, stat } from "fs/promises";
|
|
3206
3563
|
import { extname, join as join7, relative as relative2, sep as sep2 } from "path";
|
|
3207
3564
|
import ignore2 from "ignore";
|
|
3208
3565
|
var DEFAULT_IGNORE = [
|
|
@@ -3283,7 +3640,7 @@ var BINARY_EXTS = /* @__PURE__ */ new Set([
|
|
|
3283
3640
|
]);
|
|
3284
3641
|
async function readIgnoreFile2(path) {
|
|
3285
3642
|
try {
|
|
3286
|
-
const text = await
|
|
3643
|
+
const text = await readFile9(path, "utf8");
|
|
3287
3644
|
return text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
3288
3645
|
} catch {
|
|
3289
3646
|
return [];
|
|
@@ -3338,15 +3695,15 @@ async function* walk(root, options = {}) {
|
|
|
3338
3695
|
}
|
|
3339
3696
|
|
|
3340
3697
|
// src/graph/store.ts
|
|
3341
|
-
import { mkdir as
|
|
3342
|
-
import { dirname as
|
|
3698
|
+
import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
|
|
3699
|
+
import { dirname as dirname7 } from "path";
|
|
3343
3700
|
async function writeJson(path, data, pretty) {
|
|
3344
|
-
await
|
|
3701
|
+
await mkdir6(dirname7(path), { recursive: true });
|
|
3345
3702
|
const text = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
|
|
3346
|
-
await
|
|
3703
|
+
await writeFile5(path, text + "\n", "utf8");
|
|
3347
3704
|
}
|
|
3348
3705
|
async function readJson(path) {
|
|
3349
|
-
const text = await
|
|
3706
|
+
const text = await readFile10(path, "utf8");
|
|
3350
3707
|
return JSON.parse(text);
|
|
3351
3708
|
}
|
|
3352
3709
|
async function writeGraph(path, graph) {
|
|
@@ -3364,12 +3721,12 @@ async function readSymbolIndex(path) {
|
|
|
3364
3721
|
}
|
|
3365
3722
|
|
|
3366
3723
|
// src/cli/bootstrap.ts
|
|
3367
|
-
import { mkdir as
|
|
3724
|
+
import { mkdir as mkdir7, readFile as readFile12, stat as stat2, writeFile as writeFile7 } from "fs/promises";
|
|
3368
3725
|
import { basename as basename4 } from "path";
|
|
3369
3726
|
|
|
3370
3727
|
// src/hooks/claude-md.ts
|
|
3371
|
-
import { readFile as
|
|
3372
|
-
import { basename as basename3, dirname as
|
|
3728
|
+
import { readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
3729
|
+
import { basename as basename3, dirname as dirname8 } from "path";
|
|
3373
3730
|
var POLICY_VERSION = 6;
|
|
3374
3731
|
var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
|
|
3375
3732
|
var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
|
|
@@ -3531,14 +3888,14 @@ function onboardingSkeleton(projectName) {
|
|
|
3531
3888
|
async function patchClaudeMd(path, projectName) {
|
|
3532
3889
|
let existing;
|
|
3533
3890
|
try {
|
|
3534
|
-
existing = await
|
|
3891
|
+
existing = await readFile11(path, "utf8");
|
|
3535
3892
|
} catch {
|
|
3536
3893
|
existing = null;
|
|
3537
3894
|
}
|
|
3538
3895
|
const block = policyBlock();
|
|
3539
3896
|
if (existing === null) {
|
|
3540
|
-
const name = projectName || basename3(
|
|
3541
|
-
await
|
|
3897
|
+
const name = projectName || basename3(dirname8(path)) || "this project";
|
|
3898
|
+
await writeFile6(path, onboardingSkeleton(name) + "\n" + block + "\n", "utf8");
|
|
3542
3899
|
return { created: true, updated: false, skipped: false };
|
|
3543
3900
|
}
|
|
3544
3901
|
const stripped = existing.replace(ANY_BLOCK_RE, "");
|
|
@@ -3547,7 +3904,7 @@ async function patchClaudeMd(path, projectName) {
|
|
|
3547
3904
|
if (hadBlock && desired === existing) {
|
|
3548
3905
|
return { created: false, updated: false, skipped: true };
|
|
3549
3906
|
}
|
|
3550
|
-
await
|
|
3907
|
+
await writeFile6(path, desired, "utf8");
|
|
3551
3908
|
return { created: false, updated: true, skipped: false };
|
|
3552
3909
|
}
|
|
3553
3910
|
|
|
@@ -3572,13 +3929,13 @@ async function exists(path) {
|
|
|
3572
3929
|
}
|
|
3573
3930
|
async function ensureDir(path) {
|
|
3574
3931
|
const had = await exists(path);
|
|
3575
|
-
await
|
|
3932
|
+
await mkdir7(path, { recursive: true });
|
|
3576
3933
|
return !had;
|
|
3577
3934
|
}
|
|
3578
3935
|
async function patchGitignore(path) {
|
|
3579
3936
|
let existing = "";
|
|
3580
3937
|
try {
|
|
3581
|
-
existing = await
|
|
3938
|
+
existing = await readFile12(path, "utf8");
|
|
3582
3939
|
} catch {
|
|
3583
3940
|
}
|
|
3584
3941
|
const trimmed = new Set(existing.split(/\r?\n/).map((l) => l.trim()));
|
|
@@ -3587,7 +3944,7 @@ async function patchGitignore(path) {
|
|
|
3587
3944
|
const block = missing.map((m) => `# ${m.comment}
|
|
3588
3945
|
${m.entry}`).join("\n") + "\n";
|
|
3589
3946
|
const appendix = (existing.length === 0 || existing.endsWith("\n") ? "" : "\n") + (existing.length ? "\n" : "") + block;
|
|
3590
|
-
await
|
|
3947
|
+
await writeFile7(path, existing + appendix, "utf8");
|
|
3591
3948
|
return true;
|
|
3592
3949
|
}
|
|
3593
3950
|
async function bootstrap(paths) {
|
|
@@ -3661,25 +4018,22 @@ async function scanProject(projectRootRaw, opts = {}) {
|
|
|
3661
4018
|
for await (const file of walk(projectRoot)) walked.push(file);
|
|
3662
4019
|
if (verbose) log.info(` walked ${walked.length} files`);
|
|
3663
4020
|
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
|
-
}
|
|
4021
|
+
const prevCache = await readParseCache(paths.parseCache);
|
|
4022
|
+
const { parsed, cache, reused, reparsed, parseErrors } = await incrementalParse(
|
|
4023
|
+
parsable,
|
|
4024
|
+
prevCache,
|
|
4025
|
+
{ full: opts.full }
|
|
4026
|
+
);
|
|
3674
4027
|
if (verbose) {
|
|
3675
4028
|
log.info(
|
|
3676
|
-
` parsed ${parsed.length} files (${
|
|
4029
|
+
` parsed ${parsed.length} files (${reused} reused \xB7 ${reparsed} reparsed` + (parseErrors ? `, ${parseErrors} errored` : "") + `; ${walked.length - parsable.length} non-code skipped)`
|
|
3677
4030
|
);
|
|
3678
4031
|
}
|
|
3679
4032
|
const graph = await buildGraph(projectRoot, parsed);
|
|
3680
4033
|
const symbolIndex = buildSymbolIndex(graph);
|
|
3681
4034
|
await writeGraph(paths.infoGraph, graph);
|
|
3682
4035
|
await writeSymbolIndex(paths.symbolIndex, symbolIndex);
|
|
4036
|
+
await writeParseCache(paths.parseCache, cache);
|
|
3683
4037
|
if (verbose) {
|
|
3684
4038
|
log.info(
|
|
3685
4039
|
` wrote ${paths.infoGraph} \u2014 ${graph.symbol_count} symbols, ${graph.edge_count} edges`
|
|
@@ -3696,126 +4050,8 @@ async function scanProject(projectRootRaw, opts = {}) {
|
|
|
3696
4050
|
durationMs
|
|
3697
4051
|
};
|
|
3698
4052
|
}
|
|
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
|
-
}
|
|
4053
|
+
async function scanCommand(rawPath, opts = {}) {
|
|
4054
|
+
return scanProject(rawPath, opts);
|
|
3819
4055
|
}
|
|
3820
4056
|
|
|
3821
4057
|
// src/learn/runtime.ts
|
|
@@ -3876,8 +4112,8 @@ var LearnRuntime = class _LearnRuntime {
|
|
|
3876
4112
|
};
|
|
3877
4113
|
|
|
3878
4114
|
// src/server/mcp.ts
|
|
3879
|
-
import { appendFile as appendFile3, mkdir as
|
|
3880
|
-
import { dirname as
|
|
4115
|
+
import { appendFile as appendFile3, mkdir as mkdir10 } from "fs/promises";
|
|
4116
|
+
import { dirname as dirname11 } from "path";
|
|
3881
4117
|
|
|
3882
4118
|
// src/graph/rank.ts
|
|
3883
4119
|
var KW_BASE_WEIGHT = 2;
|
|
@@ -4130,14 +4366,14 @@ async function retrieve(graph, query, options = {}) {
|
|
|
4130
4366
|
|
|
4131
4367
|
// src/memory/branches.ts
|
|
4132
4368
|
import { execFile as execFile2 } from "child_process";
|
|
4133
|
-
import { readFile as
|
|
4369
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
4134
4370
|
import { join as join8 } from "path";
|
|
4135
4371
|
import { promisify as promisify2 } from "util";
|
|
4136
4372
|
var execFileAsync2 = promisify2(execFile2);
|
|
4137
4373
|
async function currentBranch(projectRoot) {
|
|
4138
4374
|
try {
|
|
4139
4375
|
const headPath = join8(projectRoot, ".git", "HEAD");
|
|
4140
|
-
const head = await
|
|
4376
|
+
const head = await readFile13(headPath, "utf8");
|
|
4141
4377
|
const trimmed = head.trim();
|
|
4142
4378
|
const match = trimmed.match(/^ref:\s+refs\/heads\/(.+)$/);
|
|
4143
4379
|
if (match?.[1]) return match[1];
|
|
@@ -4187,8 +4423,8 @@ function resolveBranchPaths(contextDir, branch, isDefault) {
|
|
|
4187
4423
|
}
|
|
4188
4424
|
|
|
4189
4425
|
// src/memory/context-md.ts
|
|
4190
|
-
import { mkdir as
|
|
4191
|
-
import { dirname as
|
|
4426
|
+
import { mkdir as mkdir8, readFile as readFile14, writeFile as writeFile8 } from "fs/promises";
|
|
4427
|
+
import { dirname as dirname9 } from "path";
|
|
4192
4428
|
var MAX_BULLETS = 3;
|
|
4193
4429
|
function deriveContextMd(entries, branch) {
|
|
4194
4430
|
const tasks = entries.filter((e) => e.type === "task").reverse();
|
|
@@ -4231,17 +4467,17 @@ function formatContextMd(ctx) {
|
|
|
4231
4467
|
return lines.join("\n");
|
|
4232
4468
|
}
|
|
4233
4469
|
async function writeContextMd(path, ctx) {
|
|
4234
|
-
await
|
|
4235
|
-
await
|
|
4470
|
+
await mkdir8(dirname9(path), { recursive: true });
|
|
4471
|
+
await writeFile8(path, formatContextMd(ctx), "utf8");
|
|
4236
4472
|
}
|
|
4237
4473
|
|
|
4238
4474
|
// src/memory/context-store.ts
|
|
4239
|
-
import { mkdir as
|
|
4240
|
-
import { dirname as
|
|
4475
|
+
import { mkdir as mkdir9, readFile as readFile15, writeFile as writeFile9 } from "fs/promises";
|
|
4476
|
+
import { dirname as dirname10 } from "path";
|
|
4241
4477
|
var SCHEMA_VERSION3 = 1;
|
|
4242
4478
|
async function readEntries(path) {
|
|
4243
4479
|
try {
|
|
4244
|
-
const raw = await
|
|
4480
|
+
const raw = await readFile15(path, "utf8");
|
|
4245
4481
|
const parsed = JSON.parse(raw);
|
|
4246
4482
|
return Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
4247
4483
|
} catch {
|
|
@@ -4249,9 +4485,9 @@ async function readEntries(path) {
|
|
|
4249
4485
|
}
|
|
4250
4486
|
}
|
|
4251
4487
|
async function writeEntries(path, entries) {
|
|
4252
|
-
await
|
|
4488
|
+
await mkdir9(dirname10(path), { recursive: true });
|
|
4253
4489
|
const store = { schema_version: SCHEMA_VERSION3, entries };
|
|
4254
|
-
await
|
|
4490
|
+
await writeFile9(path, JSON.stringify(store, null, 2) + "\n", "utf8");
|
|
4255
4491
|
}
|
|
4256
4492
|
async function appendEntry(path, entry) {
|
|
4257
4493
|
const entries = await readEntries(path);
|
|
@@ -4628,7 +4864,7 @@ var TOOLS = [
|
|
|
4628
4864
|
},
|
|
4629
4865
|
{
|
|
4630
4866
|
name: "blast_radius",
|
|
4631
|
-
description: "Given a file (or 'file::symbol' target), return all files that depend on it transitively via imports
|
|
4867
|
+
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
4868
|
inputSchema: {
|
|
4633
4869
|
type: "object",
|
|
4634
4870
|
properties: {
|
|
@@ -4640,7 +4876,7 @@ var TOOLS = [
|
|
|
4640
4876
|
},
|
|
4641
4877
|
{
|
|
4642
4878
|
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 (
|
|
4879
|
+
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
4880
|
inputSchema: {
|
|
4645
4881
|
type: "object",
|
|
4646
4882
|
properties: {
|
|
@@ -4686,12 +4922,24 @@ function blastRadius(args, ctx) {
|
|
|
4686
4922
|
const filePath = targetRaw.split("::", 1)[0]?.trim() ?? targetRaw;
|
|
4687
4923
|
const root = ctx.graph.nodes.find((n) => n.kind === "file" && n.path === filePath);
|
|
4688
4924
|
if (!root) return errorContent(`blast_radius: file not in graph: ${filePath}`);
|
|
4925
|
+
const fileIdBySymbol = /* @__PURE__ */ new Map();
|
|
4926
|
+
for (const n of ctx.graph.nodes) {
|
|
4927
|
+
if (n.kind === "symbol") fileIdBySymbol.set(n.id, `file:${n.file}`);
|
|
4928
|
+
}
|
|
4689
4929
|
const incoming = /* @__PURE__ */ new Map();
|
|
4930
|
+
const addIncoming = (to, from, kind) => {
|
|
4931
|
+
const list = incoming.get(to) ?? [];
|
|
4932
|
+
list.push({ from, kind });
|
|
4933
|
+
incoming.set(to, list);
|
|
4934
|
+
};
|
|
4690
4935
|
for (const e of ctx.graph.edges) {
|
|
4691
|
-
if (e.kind
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4936
|
+
if (e.kind === "imports" || e.kind === "tests") {
|
|
4937
|
+
addIncoming(e.to, e.from, e.kind);
|
|
4938
|
+
} else if (e.kind === "calls") {
|
|
4939
|
+
const fromFile = fileIdBySymbol.get(e.from);
|
|
4940
|
+
const toFile = fileIdBySymbol.get(e.to);
|
|
4941
|
+
if (fromFile && toFile && fromFile !== toFile) addIncoming(toFile, fromFile, "calls");
|
|
4942
|
+
}
|
|
4695
4943
|
}
|
|
4696
4944
|
const visited = /* @__PURE__ */ new Set([root.id]);
|
|
4697
4945
|
const hits = [];
|
|
@@ -4769,7 +5017,7 @@ _(no file is unreferenced \u2014 every file is either imported by another, has a
|
|
|
4769
5017
|
}
|
|
4770
5018
|
lines.push("");
|
|
4771
5019
|
lines.push(
|
|
4772
|
-
`
|
|
5020
|
+
`_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
5021
|
);
|
|
4774
5022
|
return textContent(lines.join("\n"));
|
|
4775
5023
|
}
|
|
@@ -4921,7 +5169,7 @@ async function contextRecall(args, ctx) {
|
|
|
4921
5169
|
}
|
|
4922
5170
|
async function logToolCall(ctx, tool) {
|
|
4923
5171
|
try {
|
|
4924
|
-
await
|
|
5172
|
+
await mkdir10(dirname11(ctx.paths.toolLog), { recursive: true });
|
|
4925
5173
|
await appendFile3(
|
|
4926
5174
|
ctx.paths.toolLog,
|
|
4927
5175
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool }) + "\n",
|
|
@@ -5020,12 +5268,12 @@ async function getCommitsSince(projectRoot, sinceIso) {
|
|
|
5020
5268
|
}
|
|
5021
5269
|
|
|
5022
5270
|
// src/memory/session.ts
|
|
5023
|
-
import { mkdir as
|
|
5024
|
-
import { dirname as
|
|
5271
|
+
import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile10 } from "fs/promises";
|
|
5272
|
+
import { dirname as dirname12 } from "path";
|
|
5025
5273
|
var SESSION_SCHEMA_VERSION = 1;
|
|
5026
5274
|
async function readSession(path) {
|
|
5027
5275
|
try {
|
|
5028
|
-
const raw = await
|
|
5276
|
+
const raw = await readFile16(path, "utf8");
|
|
5029
5277
|
const parsed = JSON.parse(raw);
|
|
5030
5278
|
if (parsed.schema_version !== SESSION_SCHEMA_VERSION) return null;
|
|
5031
5279
|
return parsed;
|
|
@@ -5034,8 +5282,8 @@ async function readSession(path) {
|
|
|
5034
5282
|
}
|
|
5035
5283
|
}
|
|
5036
5284
|
async function writeSession(path, state) {
|
|
5037
|
-
await
|
|
5038
|
-
await
|
|
5285
|
+
await mkdir11(dirname12(path), { recursive: true });
|
|
5286
|
+
await writeFile10(path, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
5039
5287
|
}
|
|
5040
5288
|
|
|
5041
5289
|
// src/server/routes/context-update.ts
|
|
@@ -5080,8 +5328,8 @@ async function handleContextUpdate(req, ctx) {
|
|
|
5080
5328
|
}
|
|
5081
5329
|
|
|
5082
5330
|
// src/server/routes/gate.ts
|
|
5083
|
-
import { appendFile as appendFile4, mkdir as
|
|
5084
|
-
import { dirname as
|
|
5331
|
+
import { appendFile as appendFile4, mkdir as mkdir12 } from "fs/promises";
|
|
5332
|
+
import { dirname as dirname13 } from "path";
|
|
5085
5333
|
var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
|
|
5086
5334
|
var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
|
|
5087
5335
|
function extractQuery(toolName, input) {
|
|
@@ -5137,7 +5385,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
|
|
|
5137
5385
|
}
|
|
5138
5386
|
async function logDecision(ctx, toolName, query, decision, reason) {
|
|
5139
5387
|
try {
|
|
5140
|
-
await
|
|
5388
|
+
await mkdir12(dirname13(ctx.paths.gateLog), { recursive: true });
|
|
5141
5389
|
const entry = {
|
|
5142
5390
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5143
5391
|
tool: toolName,
|
|
@@ -5209,15 +5457,15 @@ async function handleGate(req, ctx) {
|
|
|
5209
5457
|
}
|
|
5210
5458
|
|
|
5211
5459
|
// src/server/routes/log.ts
|
|
5212
|
-
import { appendFile as appendFile5, mkdir as
|
|
5213
|
-
import { dirname as
|
|
5460
|
+
import { appendFile as appendFile5, mkdir as mkdir13 } from "fs/promises";
|
|
5461
|
+
import { dirname as dirname14 } from "path";
|
|
5214
5462
|
async function handleLog(entry, ctx) {
|
|
5215
5463
|
if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
|
|
5216
5464
|
throw new Error("log: input_tokens and output_tokens (number) are required");
|
|
5217
5465
|
}
|
|
5218
5466
|
const written_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
5219
5467
|
const record = { ...entry, written_at };
|
|
5220
|
-
await
|
|
5468
|
+
await mkdir13(dirname14(ctx.paths.tokenLog), { recursive: true });
|
|
5221
5469
|
await appendFile5(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
|
|
5222
5470
|
return { ok: true, written_at };
|
|
5223
5471
|
}
|
|
@@ -5403,7 +5651,7 @@ async function startServer(paths, options = {}) {
|
|
|
5403
5651
|
const port = options.port ?? await findFreePort();
|
|
5404
5652
|
const app = buildApp(ctx, port);
|
|
5405
5653
|
const nodeServer = serve2({ fetch: app.fetch, port, hostname: "127.0.0.1" });
|
|
5406
|
-
await
|
|
5654
|
+
await writeFile11(paths.mcpPort, String(port), "utf8");
|
|
5407
5655
|
const fileWatcher = createFileWatcher(paths.projectRoot, (e) => ctx.activity.add(e));
|
|
5408
5656
|
const gitWatcher = createGitWatcher(paths.projectRoot, async (e) => {
|
|
5409
5657
|
await ctx.activity.add(e);
|
|
@@ -5547,7 +5795,7 @@ async function dashboardCommand(rawPath) {
|
|
|
5547
5795
|
}
|
|
5548
5796
|
|
|
5549
5797
|
// src/cli/doctor-command.ts
|
|
5550
|
-
import { readFile as
|
|
5798
|
+
import { readFile as readFile17, stat as stat4 } from "fs/promises";
|
|
5551
5799
|
import { join as join10, resolve as resolve3 } from "path";
|
|
5552
5800
|
import spawn from "cross-spawn";
|
|
5553
5801
|
var ICON = { ok: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
|
|
@@ -5616,7 +5864,7 @@ async function runDoctorChecks(projectRoot) {
|
|
|
5616
5864
|
});
|
|
5617
5865
|
} else {
|
|
5618
5866
|
try {
|
|
5619
|
-
const graph = JSON.parse(await
|
|
5867
|
+
const graph = JSON.parse(await readFile17(paths.infoGraph, "utf8"));
|
|
5620
5868
|
const parts = [`${graph.symbol_count} symbols`, `${graph.file_count} files`];
|
|
5621
5869
|
let status = "ok";
|
|
5622
5870
|
const ageMs = Date.now() - Date.parse(graph.generated_at);
|
|
@@ -5657,7 +5905,7 @@ async function runDoctorChecks(projectRoot) {
|
|
|
5657
5905
|
detail: "no CLAUDE.md \u2014 run `syn .` to scaffold + inject the policy block."
|
|
5658
5906
|
});
|
|
5659
5907
|
} else {
|
|
5660
|
-
const md = await
|
|
5908
|
+
const md = await readFile17(paths.claudeMd, "utf8");
|
|
5661
5909
|
if (md.includes(`synthra-policy v${POLICY_VERSION} BEGIN`)) {
|
|
5662
5910
|
checks.push({
|
|
5663
5911
|
status: "ok",
|
|
@@ -5680,7 +5928,7 @@ async function runDoctorChecks(projectRoot) {
|
|
|
5680
5928
|
detail: "no .claude/settings.local.json \u2014 run `syn .` to install hooks."
|
|
5681
5929
|
});
|
|
5682
5930
|
} else {
|
|
5683
|
-
const s = await
|
|
5931
|
+
const s = await readFile17(paths.claudeSettings, "utf8");
|
|
5684
5932
|
checks.push(
|
|
5685
5933
|
s.includes("synthra-hook=true") ? { status: "ok", label: "Hooks", detail: "registered in .claude/settings.local.json" } : {
|
|
5686
5934
|
status: "warn",
|
|
@@ -5710,7 +5958,7 @@ async function doctorCommand(rawPath) {
|
|
|
5710
5958
|
}
|
|
5711
5959
|
|
|
5712
5960
|
// src/cli/self-update.ts
|
|
5713
|
-
import { mkdir as
|
|
5961
|
+
import { mkdir as mkdir14, readFile as readFile18, writeFile as writeFile12 } from "fs/promises";
|
|
5714
5962
|
import { homedir as homedir3 } from "os";
|
|
5715
5963
|
import { join as join11 } from "path";
|
|
5716
5964
|
import { createInterface } from "readline/promises";
|
|
@@ -5768,7 +6016,7 @@ async function checkForUpdate() {
|
|
|
5768
6016
|
}
|
|
5769
6017
|
async function readLastSeen() {
|
|
5770
6018
|
try {
|
|
5771
|
-
const raw = await
|
|
6019
|
+
const raw = await readFile18(LAST_SEEN_PATH, "utf8");
|
|
5772
6020
|
const parsed = JSON.parse(raw);
|
|
5773
6021
|
return parsed.version ?? null;
|
|
5774
6022
|
} catch {
|
|
@@ -5777,9 +6025,9 @@ async function readLastSeen() {
|
|
|
5777
6025
|
}
|
|
5778
6026
|
async function writeLastSeen(version) {
|
|
5779
6027
|
try {
|
|
5780
|
-
await
|
|
6028
|
+
await mkdir14(SYNTHRA_DIR, { recursive: true });
|
|
5781
6029
|
const data = { version, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5782
|
-
await
|
|
6030
|
+
await writeFile12(LAST_SEEN_PATH, JSON.stringify(data, null, 2), "utf8");
|
|
5783
6031
|
} catch {
|
|
5784
6032
|
}
|
|
5785
6033
|
}
|
|
@@ -5811,7 +6059,7 @@ async function readInstalledChangelog() {
|
|
|
5811
6059
|
const root = await npmGlobalRoot();
|
|
5812
6060
|
if (!root) return null;
|
|
5813
6061
|
try {
|
|
5814
|
-
return await
|
|
6062
|
+
return await readFile18(join11(root, "@jefuriiij", "synthra", "CHANGELOG.md"), "utf8");
|
|
5815
6063
|
} catch {
|
|
5816
6064
|
return null;
|
|
5817
6065
|
}
|
|
@@ -6018,7 +6266,7 @@ async function defaultFlow(rawPath, opts) {
|
|
|
6018
6266
|
await runStartupChangelogCheck();
|
|
6019
6267
|
await promptForUpdateOrLog();
|
|
6020
6268
|
await recordProject(projectRoot);
|
|
6021
|
-
const scan = await scanCommand(rawPath);
|
|
6269
|
+
const scan = await scanCommand(rawPath, { full: opts.full });
|
|
6022
6270
|
const mcpHandle = await startServer(paths);
|
|
6023
6271
|
let dashboardHandle = null;
|
|
6024
6272
|
try {
|
|
@@ -6065,11 +6313,11 @@ function buildProgram() {
|
|
|
6065
6313
|
{
|
|
6066
6314
|
default: true
|
|
6067
6315
|
}
|
|
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) => {
|
|
6316
|
+
).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
6317
|
await defaultFlow(path ?? ".", opts);
|
|
6070
6318
|
});
|
|
6071
|
-
prog.command("scan [path]", "Scan only \u2014 walk + parse + write graph.").action(async (path) => {
|
|
6072
|
-
await scanCommand(path ?? ".");
|
|
6319
|
+
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) => {
|
|
6320
|
+
await scanCommand(path ?? ".", { full: opts.full });
|
|
6073
6321
|
});
|
|
6074
6322
|
prog.command("serve [path]", "Start the HTTP MCP server against a scanned project.").action(async (path) => {
|
|
6075
6323
|
await serveCommand(path ?? ".");
|