codecartographer-pi 0.9.1 → 0.10.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.
@@ -14,6 +14,7 @@ export function renderDashboard(inputs) {
14
14
  `<div class="cc-main">`,
15
15
  renderHeader(inputs),
16
16
  renderStalenessWarning(inputs),
17
+ renderHealthPanel(inputs),
17
18
  inputs.narration ? renderNarration(inputs.narration, completedPhaseCount(inputs.status)) : "",
18
19
  renderKeyResults(inputs),
19
20
  renderProgressBar(inputs.pipeline, inputs.status),
@@ -84,7 +85,7 @@ function renderHeader(inputs) {
84
85
  const { status, pipeline, packageVersion, generatedAt } = inputs;
85
86
  const projectName = status.project_name || "(unnamed project)";
86
87
  const pipelineLabel = pipeline.workflow_name || status.pipeline;
87
- const currentPhase = status.current_phase || "—";
88
+ const currentPhase = displayCurrentPhase(status);
88
89
  const totals = computeTotals(inputs.usage);
89
90
  const completed = completedPhaseCount(status);
90
91
  const total = pipeline.phase_order.length;
@@ -100,7 +101,7 @@ function renderHeader(inputs) {
100
101
  renderStat("Pipeline", pipelineLabel),
101
102
  renderStat("Current phase", currentPhase),
102
103
  renderStat("Progress", `${completed}/${total} phases`),
103
- renderStat("Recorded tokens", formatTokenCount(totals.tokens.input + totals.tokens.output)),
104
+ renderStat("Recorded tokens", formatTokensForDashboard(inputs.usage)),
104
105
  renderStat("Tool uses", String(totals.tool_uses)),
105
106
  renderStat("Package", `v${packageVersion}`),
106
107
  `</div>`,
@@ -125,6 +126,66 @@ function renderStalenessWarning(inputs) {
125
126
  return "";
126
127
  return `<section class="cc-warning" data-section><strong>Dashboard may be stale.</strong> Status was updated at ${escapeHtml(inputs.status.last_updated)} after this dashboard was generated at ${escapeHtml(inputs.generatedAt)}. Regenerate with <code>/codecarto-dashboard</code>.</section>`;
127
128
  }
129
+ function renderHealthPanel(inputs) {
130
+ const { status, pipeline, usage } = inputs;
131
+ const completed = completedPhaseCount(status);
132
+ const total = pipeline.phase_order.length;
133
+ const totals = computeTotals(usage);
134
+ const issues = collectDashboardIssues(inputs);
135
+ const openQuestionCount = countOpenQuestions(status);
136
+ const carryForwardCount = countCarryForward(status);
137
+ const health = issues.some((i) => i.severity === "blocker") ? "attention required" : issues.length ? "review recommended" : completed === total ? "complete" : "on track";
138
+ const healthClass = issues.some((i) => i.severity === "blocker") ? "bad" : issues.length ? "warn" : "ok";
139
+ const tokenText = usageHasTokenAccounting(usage) ? formatTokenCount(totals.tokens.input + totals.tokens.output) : usage.runs.length ? "unavailable" : "0";
140
+ const issueMarkup = issues.length
141
+ ? `<div class="cc-health-issues">${issues.map(renderDashboardIssue).join("\n")}</div>`
142
+ : `<p class="cc-health-ok">No blocking artifact gaps detected.</p>`;
143
+ return [
144
+ `<section class="cc-card cc-health cc-health-${healthClass}" aria-label="Dashboard health" data-section data-search-text="health status blockers missing artifacts open questions">`,
145
+ `<div class="cc-health-hero">`,
146
+ `<div><div class="cc-eyebrow">Pipeline health</div><h2>${escapeHtml(health)}</h2><p>${escapeHtml(status.current_phase && status.current_phase !== "complete" ? `Current phase: ${status.current_phase}` : "Pipeline complete — all phases have finished.")}</p></div>`,
147
+ `<div class="cc-health-ring" aria-label="${completed} of ${total} phases complete"><strong>${completed}/${total}</strong><span>phases</span></div>`,
148
+ `</div>`,
149
+ `<div class="cc-health-grid">`,
150
+ renderHealthMetric("Artifacts needing attention", String(issues.length), issues.length ? "bad" : "ok"),
151
+ renderHealthMetric("Open questions", String(openQuestionCount), openQuestionCount ? "warn" : "ok"),
152
+ renderHealthMetric("Carry-forward items", String(carryForwardCount), carryForwardCount ? "warn" : "ok"),
153
+ renderHealthMetric("Tool uses", String(totals.tool_uses), "neutral"),
154
+ renderHealthMetric("Runtime", formatMillis(totals.duration_ms), "neutral"),
155
+ renderHealthMetric("Tokens", tokenText, tokenText === "unavailable" ? "warn" : "neutral"),
156
+ `</div>`,
157
+ issueMarkup,
158
+ `</section>`,
159
+ ].join("\n");
160
+ }
161
+ function renderHealthMetric(label, value, tone) {
162
+ return `<div class="cc-health-metric cc-tone-${tone}"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`;
163
+ }
164
+ function renderDashboardIssue(issue) {
165
+ const path = issue.path ? `<code>${escapeHtml(issue.path)}</code>` : "";
166
+ return `<article class="cc-health-issue cc-issue-${issue.severity}"><div><span class="cc-pill ${issue.severity === "blocker" ? "cc-pill-bad" : ""}">${escapeHtml(issue.severity)}</span><a href="#${phaseAnchor(issue.phaseId)}">${escapeHtml(issue.phaseId)}</a></div><strong>${escapeHtml(issue.title)}</strong><p>${escapeHtml(issue.detail)} ${path}</p></article>`;
167
+ }
168
+ function collectDashboardIssues(inputs) {
169
+ const out = [];
170
+ for (const phaseId of inputs.pipeline.phase_order) {
171
+ const phase = getPhase(inputs.pipeline, phaseId);
172
+ if (!phase?.primary_output)
173
+ continue;
174
+ const state = phaseRenderState(inputs.status, phaseId);
175
+ const primary = inputs.outputsPresent.get(phaseId)?.primary;
176
+ const shouldExist = state === "complete" || state === "current" || state === "running";
177
+ if (shouldExist && primary?.exists === false) {
178
+ out.push({
179
+ severity: state === "complete" || state === "current" ? "blocker" : "warning",
180
+ phaseId,
181
+ title: "Required primary output is missing",
182
+ detail: state === "complete" ? "Phase is marked complete but the dashboard cannot find its primary artifact at" : "This phase is active or ready, but its required artifact is not present at",
183
+ path: phase.primary_output,
184
+ });
185
+ }
186
+ }
187
+ return out;
188
+ }
128
189
  function renderNarration(narration, currentCompletedCount) {
129
190
  const runsSince = Math.max(0, currentCompletedCount - narration.phaseCountAtGeneration);
130
191
  const staleness = runsSince === 0 ? "current" : `${runsSince} run${runsSince === 1 ? "" : "s"} since`;
@@ -305,38 +366,71 @@ function renderPhaseOwnerNotes(phaseState) {
305
366
  function renderPhaseLastRun(run) {
306
367
  if (!run)
307
368
  return `<div class="cc-phase-section"><h3>Last run</h3><p class="cc-muted">No usage record for this phase.</p></div>`;
308
- const tokensTotal = (run.tokens?.input ?? 0) + (run.tokens?.output ?? 0);
369
+ const tokensTotal = formatRunTokens(run);
309
370
  const sessionLink = run.session_file ? renderSafeLink(run.session_file, "transcript") : undefined;
310
371
  const session = sessionLink ? `<dt>Session</dt><dd>${sessionLink}</dd>` : "";
311
- return [`<div class="cc-phase-section">`, `<h3>Last run</h3>`, `<dl class="cc-run-meta">`, `<dt>Timestamp</dt><dd>${escapeHtml(run.timestamp)}</dd>`, `<dt>Status</dt><dd>${escapeHtml(run.status)}</dd>`, `<dt>Turns</dt><dd>${run.turn_count}</dd>`, `<dt>Tool uses</dt><dd>${run.tool_uses}</dd>`, `<dt>Tokens</dt><dd>${escapeHtml(formatTokenCount(tokensTotal))}</dd>`, `<dt>Duration</dt><dd>${escapeHtml(formatMillis(run.duration_ms))}</dd>`, session, `</dl>`, `</div>`].join("");
372
+ return [`<div class="cc-phase-section">`, `<h3>Last run</h3>`, `<dl class="cc-run-meta">`, `<dt>Timestamp</dt><dd>${escapeHtml(run.timestamp)}</dd>`, `<dt>Status</dt><dd>${escapeHtml(run.status)}</dd>`, `<dt>Turns</dt><dd>${run.turn_count}</dd>`, `<dt>Tool uses</dt><dd>${run.tool_uses}</dd>`, `<dt>Tokens</dt><dd>${escapeHtml(tokensTotal)}</dd>`, `<dt>Duration</dt><dd>${escapeHtml(formatMillis(run.duration_ms))}</dd>`, session, `</dl>`, `</div>`].join("");
312
373
  }
313
374
  function renderUsagePanel(inputs) {
314
375
  const { usage, pipeline, status } = inputs;
315
376
  const totals = computeTotals(usage);
316
377
  const perPhase = computePerPhaseTotals(usage);
378
+ const tokenAccounting = usageHasTokenAccounting(usage);
379
+ const maxTools = Math.max(1, ...[...perPhase.values()].map((t) => t.tool_uses));
380
+ const maxDuration = Math.max(1, ...[...perPhase.values()].map((t) => t.duration_ms));
317
381
  const rows = pipeline.phase_order.map((phaseId) => {
318
382
  const t = perPhase.get(phaseId);
319
383
  if (!t) {
320
384
  const complete = status.phases[phaseId]?.status === "complete";
321
- return `<tr class="${complete ? "cc-usage-missing" : ""}"><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>0</td><td>—</td><td>—</td><td>${complete ? "usage not recorded" : "not run"}</td></tr>`;
385
+ return `<tr class="${complete ? "cc-usage-missing" : ""}"><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>0</td><td>—</td><td>—</td><td>—</td><td>${complete ? "usage not recorded" : "not run"}</td></tr>`;
322
386
  }
323
387
  const tokensTotal = t.tokens.input + t.tokens.output;
324
- return `<tr><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>${t.runs}</td><td>${escapeHtml(formatTokenCount(tokensTotal))}</td><td>${t.tool_uses}</td><td>${escapeHtml(formatMillis(t.duration_ms))}</td></tr>`;
388
+ return `<tr><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>${t.runs}</td><td>${escapeHtml(tokenAccounting ? formatTokenCount(tokensTotal) : "unavailable")}</td><td>${renderUsageBar(t.tool_uses, maxTools, String(t.tool_uses))}</td><td>${renderUsageBar(t.duration_ms, maxDuration, formatMillis(t.duration_ms))}</td><td>${usagePhaseNote(phaseId, status)}</td></tr>`;
325
389
  }).join("");
326
390
  return [
327
391
  `<section class="cc-card cc-usage" id="usage" aria-label="Token usage" data-section data-search-text="usage tokens tool duration">`,
328
392
  `<div class="cc-section-head"><h2>Usage</h2><span>${totals.runs} runs</span></div>`,
329
- usage.runs.length === 0 ? `<p class="cc-empty">No phase runs recorded yet.</p>` : `<dl class="cc-usage-totals">${renderUsageTotalsList(totals)}</dl>`,
393
+ usage.runs.length === 0 ? `<p class="cc-empty">No phase runs recorded yet.</p>` : `<dl class="cc-usage-totals">${renderUsageTotalsList(totals, tokenAccounting)}</dl>`,
394
+ renderUsageInsights(perPhase, status),
330
395
  `<table class="cc-usage-table">`,
331
- `<thead><tr><th>Phase</th><th>Runs</th><th>Tokens</th><th>Tools</th><th>Duration</th></tr></thead>`,
396
+ `<thead><tr><th>Phase</th><th>Runs</th><th>Tokens</th><th>Tools</th><th>Duration</th><th>State</th></tr></thead>`,
332
397
  `<tbody>${rows}</tbody>`,
333
398
  `</table>`,
334
399
  `</section>`,
335
400
  ].join("\n");
336
401
  }
337
- function renderUsageTotalsList(totals) {
402
+ function renderUsageTotalsList(totals, tokenAccounting) {
338
403
  const tokensTotal = totals.tokens.input + totals.tokens.output;
339
- return [`<dt>Total runs</dt><dd>${totals.runs}</dd>`, `<dt>Tokens (in / out / cache)</dt><dd>${escapeHtml(formatTokenCount(totals.tokens.input))} / ${escapeHtml(formatTokenCount(totals.tokens.output))} / ${escapeHtml(formatTokenCount(totals.tokens.cache_write))}</dd>`, `<dt>Total tokens</dt><dd>${escapeHtml(formatTokenCount(tokensTotal))}</dd>`, `<dt>Tool uses</dt><dd>${totals.tool_uses}</dd>`, `<dt>Total duration</dt><dd>${escapeHtml(formatMillis(totals.duration_ms))}</dd>`].join("");
404
+ const tokenDetail = tokenAccounting
405
+ ? `${escapeHtml(formatTokenCount(totals.tokens.input))} / ${escapeHtml(formatTokenCount(totals.tokens.output))} / ${escapeHtml(formatTokenCount(totals.tokens.cache_write))}`
406
+ : `<span class="cc-muted">unavailable — host did not report token counts</span>`;
407
+ const tokenTotal = tokenAccounting ? escapeHtml(formatTokenCount(tokensTotal)) : `<span class="cc-muted">unavailable</span>`;
408
+ return [`<dt>Total runs</dt><dd>${totals.runs}</dd>`, `<dt>Tokens (in / out / cache)</dt><dd>${tokenDetail}</dd>`, `<dt>Total tokens</dt><dd>${tokenTotal}</dd>`, `<dt>Tool uses</dt><dd>${totals.tool_uses}</dd>`, `<dt>Total duration</dt><dd>${escapeHtml(formatMillis(totals.duration_ms))}</dd>`].join("");
409
+ }
410
+ function renderUsageInsights(perPhase, status) {
411
+ if (perPhase.size === 0)
412
+ return "";
413
+ const entries = [...perPhase.entries()];
414
+ const longest = entries.reduce((best, entry) => entry[1].duration_ms > best[1].duration_ms ? entry : best, entries[0]);
415
+ const mostTools = entries.reduce((best, entry) => entry[1].tool_uses > best[1].tool_uses ? entry : best, entries[0]);
416
+ const current = status.current_phase ? perPhase.get(status.current_phase) : undefined;
417
+ return [
418
+ `<div class="cc-usage-insights">`,
419
+ renderInsight("Longest phase", longest[0], formatMillis(longest[1].duration_ms)),
420
+ renderInsight("Most tool-heavy", mostTools[0], `${mostTools[1].tool_uses} tools`),
421
+ current && status.current_phase ? renderInsight("Current phase usage", status.current_phase, `${current.runs} run${current.runs === 1 ? "" : "s"} · ${formatMillis(current.duration_ms)}`) : "",
422
+ `</div>`,
423
+ ].filter(Boolean).join("\n");
424
+ }
425
+ function renderInsight(label, phaseId, value) {
426
+ return `<article class="cc-insight"><span>${escapeHtml(label)}</span><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a><strong>${escapeHtml(value)}</strong></article>`;
427
+ }
428
+ function renderUsageBar(value, max, label) {
429
+ const pct = Math.max(3, Math.min(100, Math.round((value / max) * 100)));
430
+ return `<span class="cc-bar-cell"><span class="cc-bar" style="--cc-bar:${pct}%"></span><span>${escapeHtml(label)}</span></span>`;
431
+ }
432
+ function usagePhaseNote(phaseId, status) {
433
+ return escapeHtml(phaseRenderState(status, phaseId));
340
434
  }
341
435
  function renderActivityTimeline(runs) {
342
436
  if (runs.length === 0)
@@ -344,17 +438,19 @@ function renderActivityTimeline(runs) {
344
438
  const sorted = [...runs].sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1));
345
439
  const visible = sorted.slice(0, TIMELINE_VISIBLE_COUNT);
346
440
  const overflow = sorted.slice(TIMELINE_VISIBLE_COUNT);
441
+ const hasSessionLinks = sorted.some((run) => Boolean(run.session_file && safeRelativeHref(run.session_file)));
347
442
  const rowOf = (run) => {
348
- const tokensTotal = (run.tokens?.input ?? 0) + (run.tokens?.output ?? 0);
443
+ const tokensTotal = formatRunTokens(run);
349
444
  const sessionCell = run.session_file ? (renderSafeLink(run.session_file, "session") ?? "—") : "—";
350
- return `<tr><td>${escapeHtml(run.timestamp)}</td><td><a href="#${phaseAnchor(run.phase)}">${escapeHtml(run.phase)}</a></td><td>${escapeHtml(run.status)}</td><td>${run.turn_count}</td><td>${run.tool_uses}</td><td>${escapeHtml(formatTokenCount(tokensTotal))}</td><td>${escapeHtml(formatMillis(run.duration_ms))}</td><td>${sessionCell}</td></tr>`;
445
+ return `<tr><td>${escapeHtml(run.timestamp)}</td><td><a href="#${phaseAnchor(run.phase)}">${escapeHtml(run.phase)}</a></td><td>${escapeHtml(run.status)}</td><td>${run.turn_count}</td><td>${run.tool_uses}</td><td>${escapeHtml(tokensTotal)}</td><td>${escapeHtml(formatMillis(run.duration_ms))}</td>${hasSessionLinks ? `<td>${sessionCell}</td>` : ""}</tr>`;
351
446
  };
352
447
  const overflowBlock = overflow.length === 0 ? "" : [`<details class="cc-timeline-older">`, `<summary>Older runs (${overflow.length})</summary>`, `<table class="cc-timeline-table">`, `<tbody>${overflow.map(rowOf).join("")}</tbody>`, `</table>`, `</details>`].join("\n");
353
- return [`<section class="cc-card cc-timeline" aria-label="Activity timeline" data-section data-search-text="activity timeline sessions">`, `<div class="cc-section-head"><h2>Activity timeline</h2><span>newest first</span></div>`, `<table class="cc-timeline-table">`, `<thead><tr><th>When</th><th>Phase</th><th>Status</th><th>⟳</th><th>Tools</th><th>Tokens</th><th>Duration</th><th>Session</th></tr></thead>`, `<tbody>${visible.map(rowOf).join("")}</tbody>`, `</table>`, overflowBlock, `</section>`].join("\n");
448
+ return [`<section class="cc-card cc-timeline" aria-label="Activity timeline" data-section data-search-text="activity timeline sessions">`, `<div class="cc-section-head"><h2>Activity timeline</h2><span>newest first</span></div>`, `<table class="cc-timeline-table">`, `<thead><tr><th>When</th><th>Phase</th><th>Status</th><th>Turns</th><th>Tools</th><th>Tokens</th><th>Duration</th>${hasSessionLinks ? "<th>Session</th>" : ""}</tr></thead>`, `<tbody>${visible.map(rowOf).join("")}</tbody>`, `</table>`, overflowBlock, `</section>`].join("\n");
354
449
  }
355
450
  function renderOpenQuestionsRollup(status) {
356
451
  const buckets = [];
357
452
  const seen = new Set();
453
+ const byKind = new Map();
358
454
  let total = 0;
359
455
  for (const [phaseId, phaseState] of Object.entries(status.phases)) {
360
456
  const questions = [];
@@ -363,6 +459,8 @@ function renderOpenQuestionsRollup(status) {
363
459
  if (seen.has(key))
364
460
  continue;
365
461
  seen.add(key);
462
+ const kind = String(q.kind ?? "unspecified");
463
+ byKind.set(kind, (byKind.get(kind) ?? 0) + 1);
366
464
  questions.push(renderOpenQuestion(q));
367
465
  }
368
466
  if (questions.length === 0)
@@ -372,7 +470,8 @@ function renderOpenQuestionsRollup(status) {
372
470
  }
373
471
  if (total === 0)
374
472
  return "";
375
- return [`<section class="cc-card cc-rollup" aria-label="Open questions roll-up" data-section data-search-text="open questions">`, `<div class="cc-section-head"><h2>Open questions</h2><span>${total} unique</span></div>`, buckets.join("\n"), `</section>`].join("\n");
473
+ const kindSummary = [...byKind.entries()].sort((a, b) => b[1] - a[1]).map(([kind, count]) => `<span class="cc-kind-chip"><strong>${count}</strong>${escapeHtml(kind)}</span>`).join("");
474
+ return [`<section class="cc-card cc-rollup" aria-label="Open questions roll-up" data-section data-search-text="open questions">`, `<div class="cc-section-head"><h2>Open questions</h2><span>${total} unique</span></div>`, `<div class="cc-kind-summary">${kindSummary}</div>`, buckets.join("\n"), `</section>`].join("\n");
376
475
  }
377
476
  function renderCloseoutsList(inputs) {
378
477
  const closeouts = inputs.closeouts;
@@ -478,9 +577,32 @@ function phaseRenderState(status, phaseId) {
478
577
  return "current";
479
578
  return "pending";
480
579
  }
580
+ function displayCurrentPhase(status) {
581
+ return status.current_phase === "complete" ? "Pipeline complete" : status.current_phase || "—";
582
+ }
481
583
  function completedPhaseCount(status) {
482
584
  return Object.values(status.phases).filter((p) => p.status === "complete").length;
483
585
  }
586
+ function countOpenQuestions(status) {
587
+ return Object.values(status.phases).reduce((sum, p) => sum + (p.open_questions?.length ?? 0), 0);
588
+ }
589
+ function countCarryForward(status) {
590
+ return Object.values(status.phases).reduce((sum, p) => sum + (p.carry_forward?.length ?? 0), 0);
591
+ }
592
+ function usageHasTokenAccounting(usage) {
593
+ return usage.runs.some((run) => (run.tokens?.input ?? 0) > 0 || (run.tokens?.output ?? 0) > 0 || (run.tokens?.cache_write ?? 0) > 0);
594
+ }
595
+ function formatTokensForDashboard(usage) {
596
+ const totals = computeTotals(usage);
597
+ if (usage.runs.length > 0 && !usageHasTokenAccounting(usage))
598
+ return "unavailable";
599
+ return formatTokenCount(totals.tokens.input + totals.tokens.output);
600
+ }
601
+ function formatRunTokens(run) {
602
+ const total = (run.tokens?.input ?? 0) + (run.tokens?.output ?? 0);
603
+ const hasAccounting = total > 0 || (run.tokens?.cache_write ?? 0) > 0;
604
+ return hasAccounting ? formatTokenCount(total) : "unavailable";
605
+ }
484
606
  function lastRunPerPhase(runs) {
485
607
  const out = new Map();
486
608
  for (const r of runs) {
@@ -586,6 +708,24 @@ h2 { margin: 0; font-size: 18px; } h3 { margin: var(--s-2) 0 var(--s-1); color:
586
708
  .cc-meta-details { margin-top: var(--s-3); } .cc-header-meta, .cc-run-meta, .cc-usage-totals { display: grid; grid-template-columns: max-content 1fr; gap: var(--s-1) var(--s-3); margin: var(--s-2) 0; } dt { color: var(--fg-dim); } dd { margin: 0; }
587
709
  .cc-card, .cc-warning { padding: var(--s-3); margin-bottom: var(--s-4); }
588
710
  .cc-warning { border-color: var(--status-running); background: color-mix(in srgb, var(--status-running) 9%, var(--bg-card)); }
711
+ .cc-health { border-width: 1px; position: relative; overflow: hidden; }
712
+ .cc-health::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 4px; background: var(--accent-2); }
713
+ .cc-health-bad::before { background: var(--status-error); } .cc-health-warn::before { background: var(--status-running); } .cc-health-ok::before { background: var(--status-complete); }
714
+ .cc-health-hero { display: flex; justify-content: space-between; align-items: center; gap: var(--s-3); margin-bottom: var(--s-3); }
715
+ .cc-health-hero h2 { font-size: clamp(24px, 3vw, 38px); text-transform: capitalize; margin: 2px 0; }
716
+ .cc-health-hero p { color: var(--fg-dim); margin: 0; }
717
+ .cc-health-ring { width: 112px; height: 112px; border-radius: 999px; display: grid; place-items: center; align-content: center; background: radial-gradient(circle at center, var(--bg-card) 58%, transparent 59%), conic-gradient(var(--accent-3), var(--accent-2)); border: 1px solid var(--border); flex: 0 0 auto; }
718
+ .cc-health-ring strong { font-size: 24px; line-height: 1; } .cc-health-ring span { color: var(--fg-dim); font-size: 12px; text-transform: uppercase; letter-spacing: .08em; }
719
+ .cc-health-grid { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: var(--s-2); margin-bottom: var(--s-3); }
720
+ .cc-health-metric { padding: var(--s-2); border: 1px solid var(--border); border-radius: 14px; background: var(--bg); min-width: 0; }
721
+ .cc-health-metric span { display: block; color: var(--fg-dim); font-size: 12px; } .cc-health-metric strong { display: block; margin-top: 2px; font-size: 18px; overflow-wrap: anywhere; }
722
+ .cc-tone-ok strong { color: var(--status-complete); } .cc-tone-warn strong { color: var(--status-running); } .cc-tone-bad strong { color: var(--status-error); }
723
+ .cc-health-issues { display: grid; gap: var(--s-2); }
724
+ .cc-health-issue { padding: var(--s-2); border: 1px solid var(--border); border-radius: 14px; background: var(--bg); }
725
+ .cc-health-issue div { display: flex; flex-wrap: wrap; gap: var(--s-1); align-items: center; margin-bottom: 2px; }
726
+ .cc-health-issue strong { display: block; font-size: 15px; } .cc-health-issue p { color: var(--fg-dim); margin: 2px 0 0; }
727
+ .cc-issue-blocker { border-color: color-mix(in srgb, var(--status-error) 55%, var(--border)); background: color-mix(in srgb, var(--status-error) 8%, var(--bg-card)); }
728
+ .cc-health-ok { color: var(--status-complete); margin: 0; }
589
729
  .cc-section-head { display: flex; justify-content: space-between; gap: var(--s-2); align-items: baseline; padding-bottom: var(--s-2); margin-bottom: var(--s-2); border-bottom: 1px solid var(--border); }
590
730
  .cc-section-head span, .cc-muted, .cc-empty { color: var(--fg-dim); }
591
731
  .cc-narration pre { white-space: pre-wrap; word-wrap: break-word; margin: 0; font-family: inherit; }
@@ -616,6 +756,15 @@ h2 { margin: 0; font-size: 18px; } h3 { margin: var(--s-2) 0 var(--s-1); color:
616
756
  .cc-usage-table th, .cc-usage-table td, .cc-timeline-table th, .cc-timeline-table td { padding: var(--s-1) var(--s-2); text-align: left; border-bottom: 1px solid var(--border); font-variant-numeric: tabular-nums; }
617
757
  .cc-usage-table th, .cc-timeline-table th { color: var(--fg-dim); font-weight: 700; text-transform: uppercase; font-size: 11px; letter-spacing: .04em; }
618
758
  .cc-usage-missing td:last-child { color: var(--status-running); }
759
+ .cc-usage-insights { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-2); margin: var(--s-3) 0; }
760
+ .cc-insight { padding: var(--s-2); border: 1px solid var(--border); border-radius: 12px; background: var(--bg); display: grid; gap: 2px; }
761
+ .cc-insight span { color: var(--fg-dim); font-size: 12px; } .cc-insight a { font-weight: 800; } .cc-insight strong { font-size: 15px; }
762
+ .cc-bar-cell { min-width: 120px; display: grid; grid-template-columns: minmax(42px, 1fr) auto; align-items: center; gap: var(--s-2); }
763
+ .cc-bar-cell > span:last-child { min-width: 48px; text-align: right; }
764
+ .cc-bar { height: 8px; border-radius: 999px; background: linear-gradient(90deg, var(--accent-2) var(--cc-bar), var(--bg-soft) var(--cc-bar)); border: 1px solid var(--border); }
765
+ .cc-kind-summary { display: flex; flex-wrap: wrap; gap: var(--s-1); margin: var(--s-2) 0 var(--s-3); }
766
+ .cc-kind-chip { display: inline-flex; align-items: center; gap: 6px; padding: 5px 9px; border: 1px solid var(--border); border-radius: 999px; background: var(--bg); color: var(--fg-dim); font-size: 12px; }
767
+ .cc-kind-chip strong { color: var(--fg); }
619
768
  .cc-timeline-older { margin-top: var(--s-2); }
620
769
  .cc-footer { margin-top: var(--s-5); padding-top: var(--s-3); border-top: 1px solid var(--border); color: var(--fg-dim); font-size: 13px; }
621
770
  @media (max-width: 900px) {
@@ -624,7 +773,8 @@ h2 { margin: 0; font-size: 18px; } h3 { margin: var(--s-2) 0 var(--s-1); color:
624
773
  body.cc-sidebar-open .cc-sidebar { transform: translateX(0); }
625
774
  .cc-menu-button { display: block; }
626
775
  .cc-main { padding: var(--s-3); padding-top: var(--s-5); }
627
- .cc-stat-grid, .cc-artifact-row, .cc-closeout-row { grid-template-columns: 1fr; }
776
+ .cc-stat-grid, .cc-artifact-row, .cc-closeout-row, .cc-health-grid, .cc-usage-insights { grid-template-columns: 1fr; }
777
+ .cc-health-hero { align-items: flex-start; } .cc-health-ring { width: 88px; height: 88px; }
628
778
  .cc-header-meta, .cc-run-meta, .cc-usage-totals { grid-template-columns: 1fr; }
629
779
  table { display: block; overflow-x: auto; white-space: nowrap; }
630
780
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecartographer-pi",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "CodeCartographer packaged for Pi as an extension-driven workflow wrapper.",
5
5
  "type": "module",
6
6
  "keywords": [