From 86fe759a7cd6ebffa8435c6d7964da01ef87374d Mon Sep 17 00:00:00 2001
From: arko33 <221298156+arko33@users.noreply.github.com>
Date: Thu, 22 Oct 2026 06:37:25 +0700
Subject: [PATCH] feat: real probe latency measurement + city client polish +
shadowgit hardening (author edits, as-is)
---
packages/ghost/lib/city-client.mjs | 180 ++++++++++++++++++++++++-----
packages/ghost/lib/city.css | 31 +++++
packages/ghost/lib/probes.mjs | 6 +-
packages/ghost/lib/shadowgit.mjs | 38 ++++--
4 files changed, 216 insertions(+), 39 deletions(-)
diff --git a/packages/ghost/lib/city-client.mjs b/packages/ghost/lib/city-client.mjs
index 632117a..8d8dbb8 100644
--- a/packages/ghost/lib/city-client.mjs
+++ b/packages/ghost/lib/city-client.mjs
@@ -31,6 +31,7 @@ const ICON = {
file: icon(''),
x: icon(''),
undo: icon(''),
+ copy: icon(''),
};
const state = {
@@ -105,9 +106,21 @@ function normalize(raw) {
const anomaly = detect(samples);
let blamedPath = null;
if (anomaly) {
- const cp = checkpoints.filter((c) => c.turnNumber === anomaly.turn);
+ // Probes sample every few seconds, so a spike is often first seen one turn
+ // after the write that caused it. Blame the window [turn-1, turn], weighted
+ // so the current turn still wins a tie — otherwise the fix prompt points at
+ // whichever stylesheet happened to be edited next.
let best = null;
- for (const c of cp) for (const [p, v] of Object.entries(c.churn || {})) if (!best || v > best.v) best = { p, v };
+ for (const turn of [anomaly.turn, anomaly.turn - 1]) {
+ const weight = turn === anomaly.turn ? 1 : 0.7;
+ for (const c of checkpoints) {
+ if (c.turnNumber !== turn) continue;
+ for (const [p, v] of Object.entries(c.churn || {})) {
+ const score = v * weight;
+ if (!best || score > best.score) best = { p, score };
+ }
+ }
+ }
blamedPath = best ? best.p : null;
}
@@ -254,39 +267,123 @@ function chartSvg(samples) {
if (!samples.length) {
return '
' + GHOST(24, "ghost-mark--glow") + "
0 samples yet — point the recorder at your app (--app-url) so health samples arrive.
";
}
- const w = 320, h = 92, pad = 8;
- const rss = samples.map((s) => (s.metrics || {}).rss || 0);
- const cpu = samples.map((s) => (s.metrics || {}).cpu || 0);
- const maxR = Math.max(...rss, 0);
- const maxC = Math.max(...cpu, 0);
- if (maxR <= 0 && maxC <= 0) {
- return '' + GHOST(24, "ghost-mark--glow") + "
the probe answered, but returned no rss/cpu numbers yet — check /api/health on your app.
";
- }
- const scaleR = Math.max(maxR, 1);
- const scaleC = Math.max(maxC, 1);
- const x = (i) => pad + (i / Math.max(1, samples.length - 1)) * (w - pad * 2);
- const yR = (v) => h - pad - (v / scaleR) * (h - pad * 2);
- const yC = (v) => h - pad - (v / scaleC) * (h - pad * 2);
- const line = (arr, y) => arr.map((v, i) => (i ? "L" : "M") + x(i).toFixed(1) + " " + y(v).toFixed(1)).join(" ");
- const threshold = yR(scaleR * 0.82);
+ const W = 560, H = 250, padL = 44, padR = 46, padT = 30, padB = 30;
+ const mem = samples.map((s) => Math.round(((s.metrics || {}).rss || 0) / 1048576));
+ const lat = samples.map((s) => Math.round((s.metrics || {}).latencyMs || 0));
+ const maxMem = Math.max(400, Math.max.apply(null, mem)) * 1.02;
+ const maxLat = Math.max(300, Math.max.apply(null, lat)) * 1.08;
+ const threshold = 200;
+ const x = (i) => padL + (i / Math.max(1, samples.length - 1)) * (W - padL - padR);
+ const yMem = (v) => H - padB - (v / maxMem) * (H - padT - padB);
+ const yLat = (v) => H - padB - (v / maxLat) * (H - padT - padB);
+ const smooth = (pts) => {
+ if (!pts.length) return "";
+ let d = "M " + pts[0][0].toFixed(1) + " " + pts[0][1].toFixed(1);
+ for (let i = 0; i < pts.length - 1; i += 1) {
+ const mid = (pts[i][0] + pts[i + 1][0]) / 2;
+ d += " C " + mid.toFixed(1) + " " + pts[i][1].toFixed(1) + ", " + mid.toFixed(1) + " " + pts[i + 1][1].toFixed(1) + ", " + pts[i + 1][0].toFixed(1) + " " + pts[i + 1][1].toFixed(1);
+ }
+ return d;
+ };
+ const memPath = smooth(mem.map((v, i) => [x(i), yMem(v)]));
+ const latPath = smooth(lat.map((v, i) => [x(i), yLat(v)]));
+ const areaPath = memPath + " L " + x(samples.length - 1).toFixed(1) + " " + (H - padB) + " L " + x(0).toFixed(1) + " " + (H - padB) + " Z";
+
+ const anomalyIdx = state.anomaly ? Math.min(samples.length - 1, state.anomaly.index) : -1;
+ const anomalyEnd = anomalyIdx >= 0 ? Math.min(samples.length - 1, anomalyIdx + 4) : -1;
+ const delta = anomalyIdx > 0 ? mem[anomalyIdx] - mem[anomalyIdx - 1] : 0;
const activeN = state.turns[state.active] ? state.turns[state.active].n : 0;
- let activeIdx = -1;
+ let activeIdx = 0;
samples.forEach((s, i) => { if ((s.turnNumber || 0) <= activeN) activeIdx = i; });
+
+ const grid = [0, 0.25, 0.5, 0.75, 1].map((f) => {
+ const value = Math.round(maxMem * f);
+ const y = yMem(value);
+ return '' +
+ '' + value + "";
+ }).join("");
+ const right = [0, 0.5, 1].map((f) => {
+ const value = Math.round(maxLat * f);
+ return '' + value + "";
+ }).join("");
+
+ const region = anomalyIdx >= 0
+ ? '' +
+ ''
+ : "";
+
+ const leader = anomalyIdx > 0
+ ? '' +
+ '+' + delta + " MB / sample"
+ : "";
+
return '' +
- 'rss' +
- 'cpu' +
+ 'rss (MB)' +
+ 'p95 (ms)' +
'threshold' +
- '' + samples.length + " samples
" +
- '