feat(viz): kind-specific choreography and a finished mission you can read
Security: the pawns already orbited their destination, so homing them at
the security station gave circling for free. This adds the radial
press-and-retreat — an agent closing on the target and backing off reads
as probing it, where a fixed radius reads as waiting — and holds the
stochastic target release while probing, or the circling breaks up into
stray trips that look like distraction rather than a scan.
Findings are `mission_tasks` rows, one orb each, popped once. There is
deliberately no severity anywhere in the path: the scanner keeps
severity, file and line as substrings inside `title`, so a severity
parsed out of prose and rendered as an orb's RADIUS would be the picture
asserting a measurement the data never contained. Count only.
Benchmarks annotate the station, as text. `delta` has no schema —
compute_delta emits `{kind:"opaque"}` whenever the before/after metrics
were not structurally comparable, which is most drivers. The server
formats the shape it can parse and COUNTS the rest; an unparseable driver
reports "3 sample(s)" rather than an invented improvement, and an opaque
delta says nothing at all.
The finished map: the live label rule gates service/event nodes on
`heat > 0.12`, which is exactly backwards once everything has cooled — a
static map would be unlabelled dots. Frozen, the 25 most-touched nodes
label regardless of heat, phase stations carry a second line counting
what they produced, and the camera is released ONCE so it frames the
result even if the user panned during the run.
Every count in a caption is read off the drawn scene rather than a
parallel tally, so the words and the picture cannot disagree.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9e61e3ba35
commit
f8438c32ea
@@ -283,6 +283,137 @@ async fn world_acts(pool: &PgPool, ws: WorkspaceId, only: Option<Uuid>, after: i
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One security finding, as the scanner recorded it.
|
||||||
|
struct FindingRow {
|
||||||
|
mission_id: String,
|
||||||
|
phase_id: String,
|
||||||
|
task_id: String,
|
||||||
|
title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Findings raised by a security phase.
|
||||||
|
///
|
||||||
|
/// They are `mission_tasks` rows — the scanner's own store — not a new table.
|
||||||
|
/// There is deliberately NO severity field here: severity, file and line are
|
||||||
|
/// substrings inside `title` (see `security_scan.rs`), and a severity parsed
|
||||||
|
/// out of prose and then encoded as an orb's RADIUS would be an invented fact
|
||||||
|
/// rendered as a measurement. The title is shown as written.
|
||||||
|
async fn world_findings(pool: &PgPool, ws: WorkspaceId, only: Option<Uuid>) -> Vec<FindingRow> {
|
||||||
|
let rows = sqlx::query(
|
||||||
|
"SELECT t.mission_id::text AS mission_id,
|
||||||
|
t.phase_id::text AS phase_id,
|
||||||
|
t.id::text AS task_id,
|
||||||
|
t.title
|
||||||
|
FROM mission_tasks t
|
||||||
|
JOIN mission_phases p ON p.id = t.phase_id
|
||||||
|
JOIN missions m ON m.id = t.mission_id
|
||||||
|
WHERE m.workspace_id = $1
|
||||||
|
AND p.kind = 'security_scan'
|
||||||
|
AND ( m.status = 'running'
|
||||||
|
OR ( m.status IN ('completed', 'failed')
|
||||||
|
AND m.completed_at > now() - interval '24 hours' ) )
|
||||||
|
AND ($2::uuid IS NULL OR m.id = $2)
|
||||||
|
LIMIT 300",
|
||||||
|
)
|
||||||
|
.bind(ws.as_uuid())
|
||||||
|
.bind(only)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
rows.into_iter()
|
||||||
|
.map(|r| FindingRow {
|
||||||
|
mission_id: r.get::<String, _>("mission_id"),
|
||||||
|
phase_id: r.get::<String, _>("phase_id"),
|
||||||
|
task_id: r.get::<String, _>("task_id"),
|
||||||
|
title: r.get::<String, _>("title"),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A benchmark phase's result, summarised.
|
||||||
|
struct BenchRow {
|
||||||
|
mission_id: String,
|
||||||
|
phase_id: String,
|
||||||
|
note: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Benchmark deltas, as an annotation on the phase — not as nodes.
|
||||||
|
///
|
||||||
|
/// `delta` is a `Record<string, unknown>` with no schema: `compute_delta`
|
||||||
|
/// produces `{kind:"bencher_diff", samples:[…]}` when the before/after shapes
|
||||||
|
/// are structurally comparable, and `{kind:"opaque"}` when they are not. Only
|
||||||
|
/// the shape that can be parsed is formatted; the rest is COUNTED, never
|
||||||
|
/// guessed at, so a driver whose output we do not understand reports "3
|
||||||
|
/// samples" rather than an invented improvement.
|
||||||
|
async fn world_benchmarks(pool: &PgPool, ws: WorkspaceId, only: Option<Uuid>) -> Vec<BenchRow> {
|
||||||
|
let rows = sqlx::query(
|
||||||
|
"SELECT DISTINCT ON (b.phase_id)
|
||||||
|
b.mission_id::text AS mission_id,
|
||||||
|
b.phase_id::text AS phase_id,
|
||||||
|
b.delta
|
||||||
|
FROM benchmark_snapshots b
|
||||||
|
JOIN missions m ON m.id = b.mission_id
|
||||||
|
WHERE m.workspace_id = $1
|
||||||
|
AND b.delta IS NOT NULL
|
||||||
|
AND ( m.status = 'running'
|
||||||
|
OR ( m.status IN ('completed', 'failed')
|
||||||
|
AND m.completed_at > now() - interval '24 hours' ) )
|
||||||
|
AND ($2::uuid IS NULL OR m.id = $2)
|
||||||
|
ORDER BY b.phase_id, b.iteration DESC",
|
||||||
|
)
|
||||||
|
.bind(ws.as_uuid())
|
||||||
|
.bind(only)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
rows.into_iter()
|
||||||
|
.filter_map(|r| {
|
||||||
|
let note = benchmark_note(&r.get::<serde_json::Value, _>("delta"))?;
|
||||||
|
Some(BenchRow {
|
||||||
|
mission_id: r.get::<String, _>("mission_id"),
|
||||||
|
phase_id: r.get::<String, _>("phase_id"),
|
||||||
|
note,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One line describing a benchmark delta, or `None` if there is nothing
|
||||||
|
/// truthful to say about it.
|
||||||
|
fn benchmark_note(delta: &serde_json::Value) -> Option<String> {
|
||||||
|
let samples = delta.get("samples").and_then(|s| s.as_array())?;
|
||||||
|
if samples.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut improved = 0usize;
|
||||||
|
let mut regressed = 0usize;
|
||||||
|
// Best (most negative) percent change, since that is the one claim the
|
||||||
|
// shape actually supports.
|
||||||
|
let mut best: Option<f64> = None;
|
||||||
|
for s in samples {
|
||||||
|
match s.get("direction").and_then(|d| d.as_str()) {
|
||||||
|
Some("improved") => improved += 1,
|
||||||
|
Some("regressed") => regressed += 1,
|
||||||
|
// Counted in the total but claimed for neither side.
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if let Some(pct) = s.get("delta_pct").and_then(serde_json::Value::as_f64) {
|
||||||
|
best = Some(best.map_or(pct, |b: f64| b.min(pct)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut parts = vec![format!("{} sample(s)", samples.len())];
|
||||||
|
if improved > 0 {
|
||||||
|
parts.push(format!("{improved} faster"));
|
||||||
|
}
|
||||||
|
if regressed > 0 {
|
||||||
|
parts.push(format!("{regressed} slower"));
|
||||||
|
}
|
||||||
|
if let Some(b) = best.filter(|b| *b < 0.0) {
|
||||||
|
parts.push(format!("best {:.1}%", b));
|
||||||
|
}
|
||||||
|
Some(parts.join(", "))
|
||||||
|
}
|
||||||
|
|
||||||
/// Agents that execute a phase of this kind, via the purposes the phase runner
|
/// Agents that execute a phase of this kind, via the purposes the phase runner
|
||||||
/// itself uses. Returns empty for a teamless (microVM) mission.
|
/// itself uses. Returns empty for a teamless (microVM) mission.
|
||||||
async fn phase_agents(
|
async fn phase_agents(
|
||||||
@@ -612,6 +743,11 @@ pub async fn world_live(
|
|||||||
// opening a finished mission would replay an hour of tool calls as a
|
// opening a finished mission would replay an hour of tool calls as a
|
||||||
// burst storm and read as a mission that just did all of it at once.
|
// burst storm and read as a mission that just did all of it at once.
|
||||||
let mut event_cursor: i64 = -1;
|
let mut event_cursor: i64 = -1;
|
||||||
|
// Findings already announced. A finding is raised once.
|
||||||
|
let mut last_finding: HashSet<String> = HashSet::new();
|
||||||
|
// Last benchmark summary per phase; a looping phase produces a new one.
|
||||||
|
let mut last_bench: std::collections::HashMap<String, String> =
|
||||||
|
std::collections::HashMap::new();
|
||||||
// Audit-log cursor for edge-initiated inter-agent events (delegation,
|
// Audit-log cursor for edge-initiated inter-agent events (delegation,
|
||||||
// A2A) that bypass the run loop. -1 until seeded on the first pass.
|
// A2A) that bypass the run loop. -1 until seeded on the first pass.
|
||||||
let mut audit_cursor: i64 = -1;
|
let mut audit_cursor: i64 = -1;
|
||||||
@@ -792,6 +928,42 @@ pub async fn world_live(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Benchmark results, as an annotation on the station. Re-emitted
|
||||||
|
// only when the summary changes: a phase that loops produces a new
|
||||||
|
// snapshot per iteration, and that IS news.
|
||||||
|
for b in world_benchmarks(&pool, ws, only_mission).await {
|
||||||
|
if last_bench.get(&b.phase_id).map(|n| n == &b.note).unwrap_or(false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
last_bench.insert(b.phase_id.clone(), b.note.clone());
|
||||||
|
yield sse(
|
||||||
|
"mission.benchmark",
|
||||||
|
json!({
|
||||||
|
"missionId": b.mission_id,
|
||||||
|
"phaseId": b.phase_id,
|
||||||
|
"note": b.note,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Findings, once each, hanging off the security station. Same rule
|
||||||
|
// as files: a finding is a fact that was raised once, and
|
||||||
|
// re-sending it would pop the orb again on every poll.
|
||||||
|
for f in world_findings(&pool, ws, only_mission).await {
|
||||||
|
if !last_finding.insert(f.task_id.clone()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
yield sse(
|
||||||
|
"mission.finding",
|
||||||
|
json!({
|
||||||
|
"missionId": f.mission_id,
|
||||||
|
"phaseId": f.phase_id,
|
||||||
|
"findingId": f.task_id,
|
||||||
|
"title": f.title,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Structured activity — the motion channel. Tool calls and file
|
// Structured activity — the motion channel. Tool calls and file
|
||||||
// touches recorded at the source by the container tap and the
|
// touches recorded at the source by the container tap and the
|
||||||
// microVM `PostToolUse` hook. Never parsed from prose: a tool name
|
// microVM `PostToolUse` hook. Never parsed from prose: a tool name
|
||||||
@@ -1110,6 +1282,48 @@ mod mission_feed_tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A benchmark delta is described only as far as its shape allows.
|
||||||
|
///
|
||||||
|
/// `compute_delta` emits `{kind:"opaque"}` whenever the before/after
|
||||||
|
/// metrics were not structurally comparable — which is most drivers. The
|
||||||
|
/// temptation is to say something anyway; the result would be a performance
|
||||||
|
/// claim the picture makes and the data does not support.
|
||||||
|
#[test]
|
||||||
|
fn a_benchmark_is_described_only_as_far_as_its_shape_allows() {
|
||||||
|
use serde_json::json;
|
||||||
|
assert_eq!(
|
||||||
|
super::benchmark_note(&json!({
|
||||||
|
"kind": "bencher_diff",
|
||||||
|
"samples": [
|
||||||
|
{"name": "a", "delta_pct": -12.5, "direction": "improved"},
|
||||||
|
{"name": "b", "delta_pct": 3.0, "direction": "regressed"},
|
||||||
|
// No direction and no percentage: counted, claimed for
|
||||||
|
// neither side.
|
||||||
|
{"name": "c"},
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
.as_deref(),
|
||||||
|
Some("3 sample(s), 1 faster, 1 slower, best -12.5%")
|
||||||
|
);
|
||||||
|
// Nothing comparable happened, so nothing is said.
|
||||||
|
assert_eq!(
|
||||||
|
super::benchmark_note(&json!({ "kind": "opaque", "note": "not comparable" })),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
super::benchmark_note(&json!({ "kind": "bencher_diff", "samples": [] })),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
// Everything got slower: no "best" claim at all.
|
||||||
|
assert_eq!(
|
||||||
|
super::benchmark_note(&json!({
|
||||||
|
"samples": [{"name": "a", "delta_pct": 8.0, "direction": "regressed"}],
|
||||||
|
}))
|
||||||
|
.as_deref(),
|
||||||
|
Some("1 sample(s), 1 slower")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Agent→phase attribution must come from the runner's own mapping.
|
/// Agent→phase attribution must come from the runner's own mapping.
|
||||||
#[test]
|
#[test]
|
||||||
fn phase_attribution_reuses_the_runners_mapping() {
|
fn phase_attribution_reuses_the_runners_mapping() {
|
||||||
|
|||||||
@@ -102,6 +102,41 @@ const mono = "'Geist Mono', ui-monospace, monospace";
|
|||||||
|
|
||||||
/// Phase kind → what a person calls it. Mirrors the mission list so the two
|
/// Phase kind → what a person calls it. Mirrors the mission list so the two
|
||||||
/// surfaces do not name the same phase differently.
|
/// surfaces do not name the same phase differently.
|
||||||
|
/// What a station produced, counted from the scene the viewer is looking at.
|
||||||
|
///
|
||||||
|
/// Derived from the engine's own nodes rather than from a parallel tally: a
|
||||||
|
/// second count is free to disagree with the orbs on screen, and the caption
|
||||||
|
/// would then be confidently wrong about a picture the reader can see.
|
||||||
|
function phaseSubLabel(engine: WorldEngine, phaseId: string): string {
|
||||||
|
let files = 0;
|
||||||
|
let findings = 0;
|
||||||
|
for (const n of engine.nodes.values()) {
|
||||||
|
if (n.id.startsWith("finding:")) {
|
||||||
|
// Findings hang directly off the station that raised them.
|
||||||
|
if (n.parentId === phaseId) findings += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!n.id.startsWith("file:")) continue;
|
||||||
|
// Files nest through a directory chain, so walk up to the station.
|
||||||
|
let cur: string | null = n.parentId;
|
||||||
|
for (let hop = 0; cur && hop < 12; hop++) {
|
||||||
|
if (cur === phaseId) {
|
||||||
|
files += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cur = engine.nodes.get(cur)?.parentId ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parts: string[] = [];
|
||||||
|
const note = engine.nodes.get(phaseId)?.note;
|
||||||
|
if (note) parts.push(note);
|
||||||
|
if (files) parts.push(`${files} file${files === 1 ? "" : "s"}`);
|
||||||
|
// Count only. Severity lives inside the finding's title as prose, so a
|
||||||
|
// breakdown here would be invented — see the `mission.finding` contract.
|
||||||
|
if (findings) parts.push(`${findings} finding${findings === 1 ? "" : "s"}`);
|
||||||
|
return parts.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
const PHASE_LABEL: Record<string, string> = {
|
const PHASE_LABEL: Record<string, string> = {
|
||||||
research: "Research",
|
research: "Research",
|
||||||
coding: "Coding",
|
coding: "Coding",
|
||||||
@@ -130,7 +165,16 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
|
|||||||
const [speedMult, setSpeedMult] = useState(1);
|
const [speedMult, setSpeedMult] = useState(1);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const replayHours = 24;
|
const replayHours = 24;
|
||||||
const [hud, setHud] = useState({ agents: 0, active: 0, phases: 0, done: 0, stale: 0 });
|
const [hud, setHud] = useState({
|
||||||
|
agents: 0,
|
||||||
|
active: 0,
|
||||||
|
phases: 0,
|
||||||
|
done: 0,
|
||||||
|
stale: 0,
|
||||||
|
files: 0,
|
||||||
|
findings: 0,
|
||||||
|
finished: false,
|
||||||
|
});
|
||||||
const telemetry = useLiveState<"telemetry", { tokensPerMin?: number; doorsPending?: number; loops?: number }>(
|
const telemetry = useLiveState<"telemetry", { tokensPerMin?: number; doorsPending?: number; loops?: number }>(
|
||||||
"telemetry",
|
"telemetry",
|
||||||
(d) => d,
|
(d) => d,
|
||||||
@@ -148,6 +192,8 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
|
|||||||
/// Accumulated mission plan. Kept in a ref rather than state: it is written
|
/// Accumulated mission plan. Kept in a ref rather than state: it is written
|
||||||
/// from SSE handlers many times a second and only ever read to push into the
|
/// from SSE handlers many times a second and only ever read to push into the
|
||||||
/// engine, so re-rendering on every phase update would be pure cost.
|
/// engine, so re-rendering on every phase update would be pure cost.
|
||||||
|
/// Whether the camera has already been released once for the finished map.
|
||||||
|
const reframedRef = useRef(false);
|
||||||
const planRef = useRef<{
|
const planRef = useRef<{
|
||||||
missionId: string | null;
|
missionId: string | null;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -210,7 +256,24 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
|
|||||||
// is working" and "this mission stopped and nobody noticed".
|
// is working" and "this mission stopped and nobody noticed".
|
||||||
if ((n.heatFloor ?? 0) > 0 && (n.heatFloor ?? 0) <= 0.16) stale += 1;
|
if ((n.heatFloor ?? 0) > 0 && (n.heatFloor ?? 0) <= 0.16) stale += 1;
|
||||||
}
|
}
|
||||||
setHud({ agents: e.pawns.size, active, phases, done, stale });
|
// The finished-map summary. Counted from the drawn scene so the caption
|
||||||
|
// and the picture cannot disagree.
|
||||||
|
let files = 0;
|
||||||
|
let findings = 0;
|
||||||
|
for (const n of e.nodes.values()) {
|
||||||
|
if (n.id.startsWith("file:")) files += 1;
|
||||||
|
else if (n.id.startsWith("finding:")) findings += 1;
|
||||||
|
}
|
||||||
|
setHud({
|
||||||
|
agents: e.pawns.size,
|
||||||
|
active,
|
||||||
|
phases,
|
||||||
|
done,
|
||||||
|
stale,
|
||||||
|
files,
|
||||||
|
findings,
|
||||||
|
finished: e.frozen,
|
||||||
|
});
|
||||||
}, 600);
|
}, 600);
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -302,6 +365,9 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
|
|||||||
// pawn homes to the mission centre and the scene is a clump no matter
|
// pawn homes to the mission centre and the scene is a clump no matter
|
||||||
// how many stations it has.
|
// how many stations it has.
|
||||||
for (const a of d.agentIds ?? []) e.setHome(a, `phase:${d.phaseId}`);
|
for (const a of d.agentIds ?? []) e.setHome(a, `phase:${d.phaseId}`);
|
||||||
|
// A security station is probed, not rested at — the pawns circle it and
|
||||||
|
// press in. Only while it is running: a finished scan is a map.
|
||||||
|
e.setProbe(`phase:${d.phaseId}`, d.kind === "security_scan" && d.status === "running");
|
||||||
pushPlan();
|
pushPlan();
|
||||||
}),
|
}),
|
||||||
// The files a phase actually touched. Scoped by mission id, not by
|
// The files a phase actually touched. Scoped by mission id, not by
|
||||||
@@ -311,6 +377,14 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
|
|||||||
if (focusMissionId && d.missionId !== focusMissionId) return;
|
if (focusMissionId && d.missionId !== focusMissionId) return;
|
||||||
e.onMissionFile(d);
|
e.onMissionFile(d);
|
||||||
}),
|
}),
|
||||||
|
live.on("mission.finding", (d) => {
|
||||||
|
if (focusMissionId && d.missionId !== focusMissionId) return;
|
||||||
|
e.onMissionFinding(d);
|
||||||
|
}),
|
||||||
|
live.on("mission.benchmark", (d) => {
|
||||||
|
if (focusMissionId && d.missionId !== focusMissionId) return;
|
||||||
|
e.onMissionBenchmark(d);
|
||||||
|
}),
|
||||||
live.on("room.message", (d) => {
|
live.on("room.message", (d) => {
|
||||||
if (!d.fromAgentId || !inFocus(d.fromAgentId)) return;
|
if (!d.fromAgentId || !inFocus(d.fromAgentId)) return;
|
||||||
for (const pid of d.participantIds ?? []) {
|
for (const pid of d.participantIds ?? []) {
|
||||||
@@ -1090,6 +1164,14 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
|
|||||||
|
|
||||||
updateParticles(dt);
|
updateParticles(dt);
|
||||||
|
|
||||||
|
// The moment the mission finishes, the scene stops moving and becomes a
|
||||||
|
// thing to look at — so frame it once, even if the user had panned during
|
||||||
|
// the run. Once only: re-releasing the camera every frame would fight
|
||||||
|
// them for control of a map they are trying to read.
|
||||||
|
if (engine.frozen && !reframedRef.current) {
|
||||||
|
reframedRef.current = true;
|
||||||
|
userInteracted = false;
|
||||||
|
}
|
||||||
// camera: auto-frame until the user grabs it (then OrbitControls owns it)
|
// camera: auto-frame until the user grabs it (then OrbitControls owns it)
|
||||||
if (!userInteracted && minX < maxX) {
|
if (!userInteracted && minX < maxX) {
|
||||||
const cx = (minX + maxX) / 2;
|
const cx = (minX + maxX) / 2;
|
||||||
@@ -1104,6 +1186,21 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
|
|||||||
// labels (project to screen). In repo focus mode we skip labels for
|
// labels (project to screen). In repo focus mode we skip labels for
|
||||||
// hidden nodes so they don't leak past the culling.
|
// hidden nodes so they don't leak past the culling.
|
||||||
const wantLabels = new Map<string, { x: number; y: number; text: string; color: string; big: boolean }>();
|
const wantLabels = new Map<string, { x: number; y: number; text: string; color: string; big: boolean }>();
|
||||||
|
// A finished mission is a map to READ. The live rule below labels a
|
||||||
|
// service/event node only while `heat > 0.12`, which is exactly backwards
|
||||||
|
// for a static map: every orb has cooled, so the map would be unlabelled
|
||||||
|
// dots. Frozen, the most-touched nodes are labelled regardless of heat —
|
||||||
|
// capped, because a coding phase can leave hundreds of file orbs and all
|
||||||
|
// of them labelled is not a map either.
|
||||||
|
const settledLabels = engine.frozen
|
||||||
|
? new Set(
|
||||||
|
[...engine.nodes.values()]
|
||||||
|
.filter((n) => n.tier === "service" || n.tier === "event")
|
||||||
|
.sort((a, b) => (b.touchCount ?? 0) - (a.touchCount ?? 0))
|
||||||
|
.slice(0, 25)
|
||||||
|
.map((n) => n.id),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
for (const n of engine.nodes.values()) {
|
for (const n of engine.nodes.values()) {
|
||||||
if (n.tier === "root") continue;
|
if (n.tier === "root") continue;
|
||||||
if (visibleNodes && !visibleNodes.has(n.id)) continue;
|
if (visibleNodes && !visibleNodes.has(n.id)) continue;
|
||||||
@@ -1117,9 +1214,22 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
|
|||||||
n.tier === "team" ||
|
n.tier === "team" ||
|
||||||
n.tier === "mission" ||
|
n.tier === "mission" ||
|
||||||
n.tier === "phase";
|
n.tier === "phase";
|
||||||
const hot = (n.tier === "service" || n.tier === "event") && (n.heat > 0.12 || n.id === selectedRef.current);
|
const hot =
|
||||||
if (struct || hot)
|
(n.tier === "service" || n.tier === "event") &&
|
||||||
wantLabels.set(n.id, { x: n.x, y: n.y - n.r - 6, text: n.label, color: n.color, big: n.tier === "org" });
|
(n.heat > 0.12 || n.id === selectedRef.current || !!settledLabels?.has(n.id));
|
||||||
|
if (struct || hot) {
|
||||||
|
// Phase stations carry a second line: what the station produced. Read
|
||||||
|
// off the scene itself rather than a parallel count, so it cannot
|
||||||
|
// disagree with what is drawn.
|
||||||
|
const sub = n.tier === "phase" ? phaseSubLabel(engine, n.id) : "";
|
||||||
|
wantLabels.set(n.id, {
|
||||||
|
x: n.x,
|
||||||
|
y: n.y - n.r - 6,
|
||||||
|
text: sub ? `${n.label} · ${sub}` : n.label,
|
||||||
|
color: n.color,
|
||||||
|
big: n.tier === "org",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (showPawns)
|
if (showPawns)
|
||||||
for (const p of engine.pawns.values())
|
for (const p of engine.pawns.values())
|
||||||
@@ -1372,6 +1482,19 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
|
|||||||
{hud.stale} quiet
|
{hud.stale} quiet
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
{hud.files ? (
|
||||||
|
<span title="files this mission touched">
|
||||||
|
{hud.files} file{hud.files === 1 ? "" : "s"}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{/* Count only — the scanner keeps severity as prose inside the
|
||||||
|
finding's title, so a severity breakdown here would be invented. */}
|
||||||
|
{hud.findings ? (
|
||||||
|
<span style={{ color: "#ff5f57" }}>
|
||||||
|
{hud.findings} finding{hud.findings === 1 ? "" : "s"}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{hud.finished ? <span style={{ color: "#5fd08a" }}>finished</span> : null}
|
||||||
{telemetry.tokensPerMin ? <span>{Math.round(telemetry.tokensPerMin / 1000)}k tok/m</span> : null}
|
{telemetry.tokensPerMin ? <span>{Math.round(telemetry.tokensPerMin / 1000)}k tok/m</span> : null}
|
||||||
{telemetry.doorsPending ? (
|
{telemetry.doorsPending ? (
|
||||||
<span style={{ color: "#e8b465" }}>
|
<span style={{ color: "#e8b465" }}>
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ export interface GNode {
|
|||||||
* persistent "heat map" tint so files touched many times stay visibly
|
* persistent "heat map" tint so files touched many times stay visibly
|
||||||
* warmer than virgin files even at heat=0. */
|
* warmer than virgin files even at heat=0. */
|
||||||
touchCount: number;
|
touchCount: number;
|
||||||
|
/** A one-line annotation drawn beside this node's label — currently a
|
||||||
|
* benchmark station's result. Text only, on purpose. */
|
||||||
|
note?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GPawn {
|
export interface GPawn {
|
||||||
@@ -429,6 +432,54 @@ export class WorldEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A security finding, hanging off the station that raised it.
|
||||||
|
///
|
||||||
|
/// Rendered as one orb per finding and NOTHING else. The scanner keeps
|
||||||
|
/// severity, file and line as substrings inside `title`, so there is no
|
||||||
|
/// severity to encode — and a radius scaled by a severity parsed out of prose
|
||||||
|
/// would be the picture asserting a measurement the data never contained.
|
||||||
|
onMissionFinding(e: TaxonomyEvents["mission.finding"]) {
|
||||||
|
const id = `finding:${e.findingId}`;
|
||||||
|
const existed = this.nodes.has(id);
|
||||||
|
const parent = this.nodes.has(`phase:${e.phaseId}`) ? `phase:${e.phaseId}` : ROOT;
|
||||||
|
const depth = (this.nodes.get(parent)?.depth ?? 0) + 1;
|
||||||
|
const n = this.ensureNode(id, "event", e.title, parent, depth);
|
||||||
|
n.label = e.title;
|
||||||
|
n.lastSeen = this.now;
|
||||||
|
if (!existed) {
|
||||||
|
// The explosive pop, once. A finding is raised once; re-popping it on a
|
||||||
|
// reconnect would read as the scanner finding it again.
|
||||||
|
n.burst = 1;
|
||||||
|
n.heat = 1;
|
||||||
|
n.lastActivityAt = this.now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A benchmark station's result, as an annotation on the orb.
|
||||||
|
///
|
||||||
|
/// Text, not geometry. Encoding a speedup as radius or colour would make the
|
||||||
|
/// picture assert a magnitude, and the underlying `delta` has no schema that
|
||||||
|
/// supports one — see the `mission.benchmark` contract.
|
||||||
|
onMissionBenchmark(e: TaxonomyEvents["mission.benchmark"]) {
|
||||||
|
const n = this.nodes.get(`phase:${e.phaseId}`);
|
||||||
|
if (n) n.note = e.note;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stations whose agents circle and press in rather than resting.
|
||||||
|
///
|
||||||
|
/// Security is the one kind whose work IS the choreography: probes at a
|
||||||
|
/// target, over and over. The pawns already orbit their destination, so
|
||||||
|
/// homing them here buys the circling for free; all this adds is the radial
|
||||||
|
/// press-and-retreat and a hold on the stochastic target release, so a
|
||||||
|
/// probing agent does not wander off mid-pass.
|
||||||
|
private probing = new Set<string>();
|
||||||
|
|
||||||
|
/// Mark a phase node as a probe target (or not).
|
||||||
|
setProbe(nodeId: string, on: boolean) {
|
||||||
|
if (on) this.probing.add(nodeId);
|
||||||
|
else this.probing.delete(nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
/// Where a pawn rests when it is not touching anything.
|
/// Where a pawn rests when it is not touching anything.
|
||||||
///
|
///
|
||||||
/// This is what dissolves the clump. `homes` was written only by `seed()`, so
|
/// This is what dissolves the clump. `homes` was written only by `seed()`, so
|
||||||
@@ -764,8 +815,16 @@ export class WorldEngine {
|
|||||||
// Frozen: pawns settle at fixed angles around their last station rather
|
// Frozen: pawns settle at fixed angles around their last station rather
|
||||||
// than orbiting forever, so a finished map is still rather than restless.
|
// than orbiting forever, so a finished map is still rather than restless.
|
||||||
const ang = (this.frozen ? 0 : this.now * 1.4) + (p.id.charCodeAt(0) || 0);
|
const ang = (this.frozen ? 0 : this.now * 1.4) + (p.id.charCodeAt(0) || 0);
|
||||||
const ox = dest.x + Math.cos(ang) * (dest.r + 22);
|
// Probing: press in and pull back. A security agent circling at a fixed
|
||||||
const oy = dest.y + Math.sin(ang) * (dest.r + 22);
|
// radius reads as waiting; the same agent closing on the target and
|
||||||
|
// backing off reads as probing it, which is what it is doing.
|
||||||
|
const probe =
|
||||||
|
!this.frozen && this.probing.has(dest.id)
|
||||||
|
? Math.sin(this.now * 2.2 + (p.id.charCodeAt(1) || 0)) * 16
|
||||||
|
: 0;
|
||||||
|
const radius = dest.r + 22 - probe;
|
||||||
|
const ox = dest.x + Math.cos(ang) * radius;
|
||||||
|
const oy = dest.y + Math.sin(ang) * radius;
|
||||||
p.vx += (ox - p.x) * dt * 3;
|
p.vx += (ox - p.x) * dt * 3;
|
||||||
p.vy += (oy - p.y) * dt * 3;
|
p.vy += (oy - p.y) * dt * 3;
|
||||||
for (const q of pawns) {
|
for (const q of pawns) {
|
||||||
@@ -790,7 +849,10 @@ export class WorldEngine {
|
|||||||
target.heat = Math.min(1, target.heat + dt * 1.5);
|
target.heat = Math.min(1, target.heat + dt * 1.5);
|
||||||
if (Math.random() < dt * 5)
|
if (Math.random() < dt * 5)
|
||||||
this.beams.push({ x1: p.x, y1: p.y, x2: target.x, y2: target.y, color: p.color, life: 1 });
|
this.beams.push({ x1: p.x, y1: p.y, x2: target.x, y2: target.y, color: p.color, life: 1 });
|
||||||
if (Math.random() < dt * 0.7) p.targetId = null; // arrived; release
|
// Arrived; release — UNLESS this is a probe. Releasing there would
|
||||||
|
// send the agent home mid-pass and the circling would break up into
|
||||||
|
// stray trips, which looks like distraction rather than a scan.
|
||||||
|
if (!this.probing.has(target.id) && Math.random() < dt * 0.7) p.targetId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.idle += dt;
|
p.idle += dt;
|
||||||
|
|||||||
@@ -107,6 +107,27 @@ export interface TaxonomyEvents {
|
|||||||
status?: string;
|
status?: string;
|
||||||
source?: "diff" | "tool";
|
source?: "diff" | "tool";
|
||||||
};
|
};
|
||||||
|
/** World → one orb per security finding, hanging off the security station.
|
||||||
|
*
|
||||||
|
* There is deliberately no `severity`. The scanner writes severity, file and
|
||||||
|
* line as substrings INSIDE `title`, so a severity here would be parsed out
|
||||||
|
* of prose and then rendered as a measurement — a number the picture asserts
|
||||||
|
* and the data never contained. Structured severity needs a column written
|
||||||
|
* at the source. Ephemeral: raised once, and the engine keeps the orb. */
|
||||||
|
"mission.finding": {
|
||||||
|
missionId: string;
|
||||||
|
phaseId: string;
|
||||||
|
findingId: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
/** World → a one-line summary annotated on a benchmark station.
|
||||||
|
*
|
||||||
|
* A summary, not the delta: `delta` is a `Record<string, unknown>` with no
|
||||||
|
* schema, and only the `bencher_diff` shape can be read at all. The server
|
||||||
|
* formats what it can parse and COUNTS the rest, so an unparseable driver
|
||||||
|
* reports "3 samples" rather than an invented improvement. Stateful per
|
||||||
|
* phase — a late subscriber must see the result the station already has. */
|
||||||
|
"mission.benchmark": { missionId: string; phaseId: string; note: string };
|
||||||
/** World → re-layout (add/remove org units, agents, projects). */
|
/** World → re-layout (add/remove org units, agents, projects). */
|
||||||
"topology.update": {
|
"topology.update": {
|
||||||
formation: "hierarchy" | "flat" | "live";
|
formation: "hierarchy" | "flat" | "live";
|
||||||
@@ -152,6 +173,8 @@ export const TAXONOMY_TYPES: TaxonomyType[] = [
|
|||||||
"mission.update",
|
"mission.update",
|
||||||
"mission.phase",
|
"mission.phase",
|
||||||
"mission.file",
|
"mission.file",
|
||||||
|
"mission.finding",
|
||||||
|
"mission.benchmark",
|
||||||
"topology.update",
|
"topology.update",
|
||||||
"telemetry",
|
"telemetry",
|
||||||
"routine.update",
|
"routine.update",
|
||||||
@@ -167,6 +190,7 @@ export const STATEFUL_TYPES = new Set<TaxonomyType>([
|
|||||||
"node.activity",
|
"node.activity",
|
||||||
"mission.update",
|
"mission.update",
|
||||||
"mission.phase",
|
"mission.phase",
|
||||||
|
"mission.benchmark",
|
||||||
"telemetry",
|
"telemetry",
|
||||||
"topology.update",
|
"topology.update",
|
||||||
"routine.update",
|
"routine.update",
|
||||||
@@ -183,6 +207,8 @@ export function stateKey<T extends TaxonomyType>(type: T, d: TaxonomyPayload<T>)
|
|||||||
// moment it is pinned rather than on the next phase transition.
|
// moment it is pinned rather than on the next phase transition.
|
||||||
if (type === "mission.update") return `${type}:${(d as TaxonomyEvents["mission.update"]).missionId}`;
|
if (type === "mission.update") return `${type}:${(d as TaxonomyEvents["mission.update"]).missionId}`;
|
||||||
if (type === "mission.phase") return `${type}:${(d as TaxonomyEvents["mission.phase"]).phaseId}`;
|
if (type === "mission.phase") return `${type}:${(d as TaxonomyEvents["mission.phase"]).phaseId}`;
|
||||||
|
if (type === "mission.benchmark")
|
||||||
|
return `${type}:${(d as TaxonomyEvents["mission.benchmark"]).phaseId}`;
|
||||||
if (type === "telemetry") {
|
if (type === "telemetry") {
|
||||||
const a = (d as TaxonomyEvents["telemetry"]).agentId;
|
const a = (d as TaxonomyEvents["telemetry"]).agentId;
|
||||||
return a ? `telemetry:${a}` : "telemetry"; // per-agent slice vs workspace-wide
|
return a ? `telemetry:${a}` : "telemetry"; // per-agent slice vs workspace-wide
|
||||||
|
|||||||
Reference in New Issue
Block a user