claude-memory-layer 1.0.18 → 1.0.19

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.
Files changed (35) hide show
  1. package/config/kpi-thresholds.json +7 -0
  2. package/dist/cli/index.js +372 -74
  3. package/dist/cli/index.js.map +3 -3
  4. package/dist/hooks/post-tool-use.js +6 -0
  5. package/dist/hooks/post-tool-use.js.map +2 -2
  6. package/dist/hooks/session-end.js +6 -0
  7. package/dist/hooks/session-end.js.map +2 -2
  8. package/dist/hooks/session-start.js +6 -0
  9. package/dist/hooks/session-start.js.map +2 -2
  10. package/dist/hooks/stop.js +6 -0
  11. package/dist/hooks/stop.js.map +2 -2
  12. package/dist/hooks/user-prompt-submit.js +245 -31
  13. package/dist/hooks/user-prompt-submit.js.map +3 -3
  14. package/dist/server/api/index.js +329 -31
  15. package/dist/server/api/index.js.map +3 -3
  16. package/dist/server/index.js +336 -38
  17. package/dist/server/index.js.map +3 -3
  18. package/dist/services/memory-service.js +6 -0
  19. package/dist/services/memory-service.js.map +2 -2
  20. package/dist/ui/app.js +236 -4
  21. package/dist/ui/index.html +51 -0
  22. package/dist/ui/style.css +34 -0
  23. package/memory/_index.md +3 -0
  24. package/memory/agent_response/uncategorized/2026-03-03.md +14 -0
  25. package/memory/session_summary/uncategorized/2026-03-03.md +5 -0
  26. package/memory/tool_observation/uncategorized/2026-03-03.md +21 -0
  27. package/package.json +3 -2
  28. package/scripts/delete-unknown-projects.js +154 -0
  29. package/src/hooks/user-prompt-submit.ts +225 -29
  30. package/src/server/api/events.ts +1 -0
  31. package/src/server/api/stats.ts +346 -0
  32. package/src/services/memory-service.ts +7 -0
  33. package/src/ui/app.js +236 -4
  34. package/src/ui/index.html +51 -0
  35. package/src/ui/style.css +34 -0
package/src/ui/app.js CHANGED
@@ -12,6 +12,8 @@ const state = {
12
12
  mostAccessed: null,
13
13
  helpfulness: null,
14
14
  retrievalTraces: null,
15
+ adherenceSummary: null,
16
+ adherenceWindow: '24h',
15
17
  currentLevel: 'L0',
16
18
  currentSort: 'recent',
17
19
  currentView: 'overview',
@@ -20,6 +22,9 @@ const state = {
20
22
  events: [],
21
23
  isLoading: false,
22
24
  chartInstance: null,
25
+ kpiChartInstance: null,
26
+ kpiWindow: '7d',
27
+ kpi: null,
23
28
  chatMessages: [],
24
29
  isChatOpen: false,
25
30
  isChatStreaming: false,
@@ -102,13 +107,42 @@ function setupEventListeners() {
102
107
  });
103
108
 
104
109
  // Sort buttons
105
- document.querySelectorAll('.sort-btn').forEach(btn => {
110
+ document.querySelectorAll('.sort-btn[data-sort]').forEach(btn => {
106
111
  btn.addEventListener('click', (e) => {
107
112
  const sort = e.currentTarget.dataset.sort;
108
113
  if (sort) selectSort(sort);
109
114
  });
110
115
  });
111
116
 
117
+ // Adherence window controls
118
+ document.querySelectorAll('#adherence-window-controls .sort-btn').forEach(btn => {
119
+ btn.addEventListener('click', async (e) => {
120
+ const window = e.currentTarget.dataset.adhWindow;
121
+ if (!window || state.adherenceWindow === window) return;
122
+ state.adherenceWindow = window;
123
+ document.querySelectorAll('#adherence-window-controls .sort-btn').forEach(b => {
124
+ b.classList.toggle('active', b.dataset.adhWindow === window);
125
+ });
126
+ state.adherenceSummary = await fetchAdherenceSummary().catch(() => null);
127
+ updateAdherenceSummaryUI();
128
+ });
129
+ });
130
+
131
+ // KPI window controls
132
+ document.querySelectorAll('.sort-btn[data-kpi-window]').forEach(btn => {
133
+ btn.addEventListener('click', async (e) => {
134
+ const window = e.currentTarget.dataset.kpiWindow;
135
+ if (!window || state.kpiWindow === window) return;
136
+ state.kpiWindow = window;
137
+ document.querySelectorAll('.sort-btn[data-kpi-window]').forEach(b => {
138
+ b.classList.toggle('active', b.dataset.kpiWindow === window);
139
+ });
140
+ await loadKpiData();
141
+ updateKpiCardsUI();
142
+ renderKpiTrendChart();
143
+ });
144
+ });
145
+
112
146
  // Search
113
147
  const searchInput = document.getElementById('search-input');
114
148
  if (searchInput) {
@@ -125,6 +159,10 @@ function setupEventListeners() {
125
159
  state.chartInstance.destroy();
126
160
  state.chartInstance = null;
127
161
  }
162
+ if (state.kpiChartInstance) {
163
+ state.kpiChartInstance.destroy();
164
+ state.kpiChartInstance = null;
165
+ }
128
166
  await initActivityChart();
129
167
  // Reload current view if not overview
130
168
  if (state.currentView !== 'overview') {
@@ -231,17 +269,24 @@ function setupEventListeners() {
231
269
 
232
270
  // --- Data Fetching ---
233
271
 
272
+ async function loadKpiData() {
273
+ state.kpi = await fetch(apiUrl(`${API_BASE}/stats/kpi`, { window: state.kpiWindow }))
274
+ .then(r => r.json())
275
+ .catch(() => null);
276
+ }
277
+
234
278
  async function refreshData() {
235
279
  const btn = document.getElementById('refresh-btn');
236
280
  if(btn) btn.classList.add('loading');
237
281
 
238
282
  try {
239
- const [stats, shared, mostAccessed, helpfulness, retrievalTraces] = await Promise.all([
283
+ const [stats, shared, mostAccessed, helpfulness, retrievalTraces, adherenceSummary] = await Promise.all([
240
284
  fetch(apiUrl(`${API_BASE}/stats`)).then(r => r.json()).catch(() => null),
241
285
  fetch(apiUrl(`${API_BASE}/stats/shared`)).then(r => r.json()).catch(() => null),
242
286
  fetch(apiUrl(`${API_BASE}/stats/most-accessed`, { limit: 10 })).then(r => r.json()).catch(() => null),
243
287
  fetch(apiUrl(`${API_BASE}/stats/helpfulness`, { limit: 5 })).then(r => r.json()).catch(() => null),
244
- fetch(apiUrl(`${API_BASE}/stats/retrieval-traces`, { limit: 20 })).then(r => r.json()).catch(() => null)
288
+ fetch(apiUrl(`${API_BASE}/stats/retrieval-traces`, { limit: 20 })).then(r => r.json()).catch(() => null),
289
+ fetchAdherenceSummary().catch(() => null)
245
290
  ]);
246
291
 
247
292
  state.stats = stats;
@@ -249,10 +294,15 @@ async function refreshData() {
249
294
  state.mostAccessed = mostAccessed;
250
295
  state.helpfulness = helpfulness;
251
296
  state.retrievalTraces = retrievalTraces;
297
+ state.adherenceSummary = adherenceSummary;
298
+
299
+ await loadKpiData();
252
300
 
253
301
  updateStatsUI();
254
302
  updateSharedUI();
255
303
  updateMemoryUsageUI();
304
+ updateKpiCardsUI();
305
+ renderKpiTrendChart();
256
306
  await loadLevelEvents(state.currentLevel);
257
307
 
258
308
  checkEndlessStatus();
@@ -327,6 +377,98 @@ function updateSharedUI() {
327
377
  document.getElementById('shared-errors').textContent = formatNumber(state.sharedStats.commonErrors || 0);
328
378
  }
329
379
 
380
+ function percentText(v) {
381
+ return `${((v || 0) * 100).toFixed(1)}%`;
382
+ }
383
+
384
+ function renderDelta(id, value, lowerIsBetter = false, asPercent = true) {
385
+ const el = document.getElementById(id);
386
+ if (!el) return;
387
+ const v = Number(value || 0);
388
+ const sign = v > 0 ? '+' : '';
389
+ const text = asPercent ? `${sign}${(v * 100).toFixed(1)}%` : `${sign}${v.toFixed(2)}`;
390
+
391
+ let positive = v > 0;
392
+ if (lowerIsBetter) positive = v < 0;
393
+ const cls = v === 0 ? 'neutral' : (positive ? 'good' : 'bad');
394
+ const arrow = v === 0 ? '→' : (positive ? '▲' : '▼');
395
+
396
+ el.className = `kpi-delta ${cls}`;
397
+ el.textContent = `${arrow} ${text} vs prev`;
398
+ }
399
+
400
+ function updateKpiCardsUI() {
401
+ const m = state.kpi?.metrics;
402
+ const d = state.kpi?.deltas;
403
+ if (!m) return;
404
+ const set = (id, value) => {
405
+ const el = document.getElementById(id);
406
+ if (el) el.textContent = value;
407
+ };
408
+ set('kpi-useful-recall', percentText(m.usefulRecallRate));
409
+ set('kpi-completion-turns', Number(m.avgCompletionTurns || 0).toFixed(2));
410
+ set('kpi-rework-rate', percentText(m.reworkRate));
411
+ set('kpi-failure-rate', percentText(m.postChangeFailureRate));
412
+
413
+ if (d) {
414
+ renderDelta('kpi-useful-recall-delta', d.usefulRecallRate, false, true);
415
+ renderDelta('kpi-completion-turns-delta', d.avgCompletionTurns, true, false);
416
+ renderDelta('kpi-rework-rate-delta', d.reworkRate, true, true);
417
+ renderDelta('kpi-failure-rate-delta', d.postChangeFailureRate, true, true);
418
+ }
419
+
420
+ const alertsEl = document.getElementById('kpi-alerts');
421
+ if (alertsEl) {
422
+ const alerts = state.kpi?.alerts || [];
423
+ if (alerts.length === 0) {
424
+ alertsEl.innerHTML = '<span style="color:var(--success);">No KPI alerts in current window.</span>';
425
+ } else {
426
+ alertsEl.innerHTML = alerts.slice(0, 3).map(a => `⚠️ ${escapeHtml(a.message)} (${a.metric})`).join(' · ');
427
+ }
428
+ }
429
+ }
430
+
431
+ function renderKpiTrendChart() {
432
+ const chartEl = document.querySelector('#kpi-trend-chart');
433
+ if (!chartEl) return;
434
+
435
+ const daily = state.kpi?.trend?.daily || [];
436
+ const categories = daily.map(d => d.date);
437
+ const useful = daily.map(d => Number(d.usefulRecallRate || 0) * 100);
438
+ const rework = daily.map(d => Number(d.reworkRate || 0) * 100);
439
+ const fail = daily.map(d => Number(d.postChangeFailureRate || 0) * 100);
440
+
441
+ if (state.kpiChartInstance) {
442
+ state.kpiChartInstance.destroy();
443
+ state.kpiChartInstance = null;
444
+ }
445
+
446
+ const options = {
447
+ series: [
448
+ { name: 'Useful Recall %', data: useful },
449
+ { name: 'Rework %', data: rework },
450
+ { name: 'Failure %', data: fail }
451
+ ],
452
+ chart: {
453
+ type: 'line',
454
+ height: 240,
455
+ background: 'transparent',
456
+ toolbar: { show: false },
457
+ fontFamily: 'Outfit, sans-serif'
458
+ },
459
+ stroke: { curve: 'smooth', width: 2 },
460
+ dataLabels: { enabled: false },
461
+ xaxis: { categories, labels: { style: { colors: '#8B9BB4' } } },
462
+ yaxis: { labels: { formatter: (v) => `${v.toFixed(0)}%`, style: { colors: '#8B9BB4' } } },
463
+ theme: { mode: 'dark' },
464
+ grid: { borderColor: 'rgba(255,255,255,0.05)', strokeDashArray: 4 },
465
+ colors: ['#34D399', '#FEB019', '#FF4560']
466
+ };
467
+
468
+ state.kpiChartInstance = new ApexCharts(chartEl, options);
469
+ state.kpiChartInstance.render();
470
+ }
471
+
330
472
  function selectLevel(level) {
331
473
  state.currentLevel = level;
332
474
 
@@ -347,6 +489,23 @@ function selectSort(sort) {
347
489
  loadLevelEvents(state.currentLevel, sort);
348
490
  }
349
491
 
492
+ function getAdherenceInfo(event) {
493
+ const adherence = event?.metadata?.adherence || event?.meta?.adherence || null;
494
+ if (!adherence || typeof adherence !== 'object') return null;
495
+ const reason = adherence.reason || 'unknown';
496
+ const checked = Boolean(adherence.checked);
497
+ const turn = adherence.turn;
498
+ return { reason, checked, turn };
499
+ }
500
+
501
+ function renderAdherenceBadge(event) {
502
+ const info = getAdherenceInfo(event);
503
+ if (!info) return '';
504
+ const modeClass = info.checked ? 'adherence-checked' : 'adherence-skipped';
505
+ const turnText = Number.isFinite(info.turn) ? ` · T${info.turn}` : '';
506
+ return `<span class="adherence-badge ${modeClass}" title="adherence ${info.checked ? 'checked' : 'skipped'}${turnText}">adh:${escapeHtml(info.reason)}</span>`;
507
+ }
508
+
350
509
  function updateEventsListUI() {
351
510
  const container = document.getElementById('event-list-container');
352
511
  container.innerHTML = '';
@@ -374,13 +533,17 @@ function updateEventsListUI() {
374
533
  const accessBadge = event.accessCount > 0
375
534
  ? `<span class="access-badge"><i class="ri-eye-line"></i> ${event.accessCount}</span>`
376
535
  : '';
536
+ const adherenceBadge = renderAdherenceBadge(event);
377
537
  const lastUsed = (state.currentSort === 'accessed' || state.currentSort === 'most-accessed') && event.lastAccessedAt
378
538
  ? `<span class="event-time" style="color:var(--accent-secondary);">used ${new Date(event.lastAccessedAt).toLocaleString()}</span>`
379
539
  : '';
380
540
 
381
541
  el.innerHTML = `
382
542
  <div class="event-header">
383
- <span class="event-type-badge ${typeClass}">${eventType}</span>
543
+ <div style="display:flex; gap:8px; align-items:center;">
544
+ <span class="event-type-badge ${typeClass}">${eventType}</span>
545
+ ${adherenceBadge}
546
+ </div>
384
547
  <div style="display:flex; gap:8px; align-items:center;">
385
548
  ${accessBadge}
386
549
  ${lastUsed}
@@ -400,9 +563,72 @@ function updateMemoryUsageUI() {
400
563
  updateGraduationBars();
401
564
  updateHelpfulnessUI();
402
565
  updateMostHelpfulList();
566
+ updateAdherenceSummaryUI();
403
567
  updateRetrievalTraceUI();
404
568
  }
405
569
 
570
+ function adherenceWindowToMs(window) {
571
+ if (window === '24h') return 24 * 60 * 60 * 1000;
572
+ if (window === '7d') return 7 * 24 * 60 * 60 * 1000;
573
+ if (window === '30d') return 30 * 24 * 60 * 60 * 1000;
574
+ return 24 * 60 * 60 * 1000;
575
+ }
576
+
577
+ async function fetchAdherenceSummary() {
578
+ const res = await fetch(apiUrl(`${API_BASE}/events`, { level: 'L0', limit: 500, sort: 'recent' }));
579
+ if (!res.ok) return null;
580
+ const data = await res.json();
581
+ const events = data.events || [];
582
+
583
+ const counts = {};
584
+ let checked = 0;
585
+ let skipped = 0;
586
+ let total = 0;
587
+
588
+ const now = Date.now();
589
+ const windowMs = adherenceWindowToMs(state.adherenceWindow);
590
+
591
+ for (const e of events) {
592
+ const ts = e?.timestamp ? new Date(e.timestamp).getTime() : 0;
593
+ if (!ts || now - ts > windowMs) continue;
594
+
595
+ const adherence = e?.metadata?.adherence || e?.meta?.adherence;
596
+ if (!adherence) continue;
597
+ total++;
598
+ const reason = adherence.reason || 'unknown';
599
+ counts[reason] = (counts[reason] || 0) + 1;
600
+ if (adherence.checked) checked++; else skipped++;
601
+ }
602
+
603
+ return { total, checked, skipped, counts, window: state.adherenceWindow };
604
+ }
605
+
606
+ function updateAdherenceSummaryUI() {
607
+ const el = document.getElementById('adherence-summary');
608
+ if (!el) return;
609
+
610
+ const s = state.adherenceSummary;
611
+ if (!s || !s.total) {
612
+ el.innerHTML = '<span style="color:var(--text-muted);">No adherence metadata yet.</span>';
613
+ return;
614
+ }
615
+
616
+ const top = Object.entries(s.counts || {})
617
+ .sort((a, b) => b[1] - a[1])
618
+ .slice(0, 5)
619
+ .map(([reason, count]) => `<span class="adherence-badge adherence-checked" style="margin-right:6px;">${escapeHtml(reason)}: ${count}</span>`)
620
+ .join('');
621
+
622
+ el.innerHTML = `
623
+ <div style="display:flex; gap:12px; flex-wrap:wrap; margin-bottom:8px;">
624
+ <span><strong>${s.total}</strong> tagged prompts (${escapeHtml(s.window || state.adherenceWindow)})</span>
625
+ <span style="color:var(--success);"><strong>${s.checked}</strong> checked</span>
626
+ <span style="color:var(--text-muted);"><strong>${s.skipped}</strong> skipped</span>
627
+ </div>
628
+ <div>${top}</div>
629
+ `;
630
+ }
631
+
406
632
  function updateGraduationBars() {
407
633
  const container = document.getElementById('graduation-bars');
408
634
  if (!container || !state.stats?.levelStats) return;
@@ -691,6 +917,7 @@ async function openDetailModal(eventId) {
691
917
  const eventType = evt.eventType || 'unknown';
692
918
  const typeClass = `type-${eventType.toLowerCase().replace('_', '-')}`;
693
919
  const time = new Date(evt.timestamp).toLocaleString();
920
+ const adherenceBadge = renderAdherenceBadge(evt);
694
921
 
695
922
  let contextHtml = '';
696
923
  if (ctx.length > 0) {
@@ -716,6 +943,7 @@ async function openDetailModal(eventId) {
716
943
  <i class="ri-price-tag-3-line"></i>
717
944
  <span class="event-type-badge ${typeClass}">${eventType}</span>
718
945
  </div>
946
+ ${adherenceBadge ? `<div class="modal-meta-item">${adherenceBadge}</div>` : ''}
719
947
  <div class="modal-meta-item">
720
948
  <i class="ri-time-line"></i>
721
949
  ${time}
@@ -763,11 +991,13 @@ async function showEventsListModal() {
763
991
 
764
992
  body.innerHTML = events.map(e => {
765
993
  const typeClass = `type-${(e.eventType || '').toLowerCase().replace('_', '-')}`;
994
+ const adherenceBadge = renderAdherenceBadge(e);
766
995
  return `
767
996
  <div class="modal-list-item" onclick="openDetailModal('${e.id}')">
768
997
  <div class="modal-list-info">
769
998
  <div class="title">
770
999
  <span class="event-type-badge ${typeClass}" style="margin-right:8px;">${e.eventType}</span>
1000
+ ${adherenceBadge}
771
1001
  ${escapeHtml((e.preview || '').slice(0, 80))}
772
1002
  </div>
773
1003
  <div class="subtitle">${new Date(e.timestamp).toLocaleString()} | Session: ${(e.sessionId || '').slice(0, 12)}...</div>
@@ -847,11 +1077,13 @@ async function showSessionDetailInModal(sessionId) {
847
1077
  <div class="modal-section-title">Events</div>
848
1078
  ${events.map(e => {
849
1079
  const typeClass = `type-${(e.eventType || '').toLowerCase().replace('_', '-')}`;
1080
+ const adherenceBadge = renderAdherenceBadge(e);
850
1081
  return `
851
1082
  <div class="modal-list-item" onclick="closeAllModals(); openDetailModal('${e.id}')">
852
1083
  <div class="modal-list-info">
853
1084
  <div class="title">
854
1085
  <span class="event-type-badge ${typeClass}" style="margin-right:8px;">${e.eventType}</span>
1086
+ ${adherenceBadge}
855
1087
  ${escapeHtml((e.preview || '').slice(0, 80))}
856
1088
  </div>
857
1089
  <div class="subtitle">${new Date(e.timestamp).toLocaleString()}</div>
package/src/ui/index.html CHANGED
@@ -120,6 +120,45 @@
120
120
  </div>
121
121
  </div>
122
122
 
123
+ <!-- KPI Cards -->
124
+ <div class="stats-grid kpi-grid" style="margin-top:-4px;">
125
+ <div class="stat-card kpi-card">
126
+ <div class="stat-value" id="kpi-useful-recall">-</div>
127
+ <div class="kpi-delta" id="kpi-useful-recall-delta">-</div>
128
+ <div class="stat-label"><i class="ri-thumb-up-line"></i> Useful Recall Rate</div>
129
+ </div>
130
+ <div class="stat-card kpi-card">
131
+ <div class="stat-value" id="kpi-completion-turns">-</div>
132
+ <div class="kpi-delta" id="kpi-completion-turns-delta">-</div>
133
+ <div class="stat-label"><i class="ri-repeat-line"></i> Avg Completion Turns</div>
134
+ </div>
135
+ <div class="stat-card kpi-card">
136
+ <div class="stat-value" id="kpi-rework-rate">-</div>
137
+ <div class="kpi-delta" id="kpi-rework-rate-delta">-</div>
138
+ <div class="stat-label"><i class="ri-hammer-line"></i> Rework Rate</div>
139
+ </div>
140
+ <div class="stat-card kpi-card">
141
+ <div class="stat-value" id="kpi-failure-rate">-</div>
142
+ <div class="kpi-delta" id="kpi-failure-rate-delta">-</div>
143
+ <div class="stat-label"><i class="ri-error-warning-line"></i> Post-change Failure</div>
144
+ </div>
145
+ </div>
146
+
147
+ <div class="card" style="margin-bottom:24px;">
148
+ <div class="card-header">
149
+ <div class="card-title">
150
+ <i class="ri-line-chart-line"></i>
151
+ <span>KPI Trend</span>
152
+ </div>
153
+ <div style="display:flex; gap:8px;">
154
+ <button class="sort-btn active" data-kpi-window="7d">7d</button>
155
+ <button class="sort-btn" data-kpi-window="30d">30d</button>
156
+ </div>
157
+ </div>
158
+ <div id="kpi-alerts" style="font-size:12px; color:var(--text-muted); margin-bottom:10px;">Loading...</div>
159
+ <div id="kpi-trend-chart" style="height:240px;"></div>
160
+ </div>
161
+
123
162
  <!-- Main Grid -->
124
163
  <div class="dashboard-grid">
125
164
 
@@ -272,6 +311,18 @@
272
311
  </div>
273
312
  </div>
274
313
 
314
+ <div style="margin-top:20px;">
315
+ <div class="section-label" style="display:flex; justify-content:space-between; align-items:center;">
316
+ <span>Adherence Reasons</span>
317
+ <span id="adherence-window-controls" style="display:flex; gap:6px;">
318
+ <button class="sort-btn active" data-adh-window="24h">24h</button>
319
+ <button class="sort-btn" data-adh-window="7d">7d</button>
320
+ <button class="sort-btn" data-adh-window="30d">30d</button>
321
+ </span>
322
+ </div>
323
+ <div id="adherence-summary" style="padding:8px 0; font-size:13px; color:var(--text-muted);">Loading...</div>
324
+ </div>
325
+
275
326
  <div style="margin-top:20px;">
276
327
  <div class="section-label">Retrieval Trace (1:1)</div>
277
328
  <div id="retrieval-trace-summary" style="padding:8px 0; font-size:13px; color:var(--text-muted);">Loading...</div>
package/src/ui/style.css CHANGED
@@ -329,6 +329,23 @@ body {
329
329
  opacity: 0.5;
330
330
  }
331
331
 
332
+ .kpi-grid .stat-card .stat-value {
333
+ font-size: 28px;
334
+ }
335
+ .kpi-grid .stat-card .stat-label {
336
+ font-size: 12px;
337
+ }
338
+ .kpi-delta {
339
+ font-size: 11px;
340
+ margin-top: -4px;
341
+ margin-bottom: 8px;
342
+ color: var(--text-muted);
343
+ font-weight: 600;
344
+ }
345
+ .kpi-delta.good { color: var(--success); }
346
+ .kpi-delta.bad { color: var(--error); }
347
+ .kpi-delta.neutral { color: var(--text-muted); }
348
+
332
349
  .stat-value {
333
350
  font-size: 36px;
334
351
  font-weight: 700;
@@ -594,6 +611,23 @@ body {
594
611
  font-weight: 600;
595
612
  }
596
613
 
614
+ .adherence-badge {
615
+ font-size: 10px;
616
+ padding: 3px 7px;
617
+ border-radius: 6px;
618
+ font-weight: 700;
619
+ text-transform: uppercase;
620
+ letter-spacing: 0.2px;
621
+ }
622
+ .adherence-checked {
623
+ background: rgba(52, 211, 153, 0.14);
624
+ color: #34D399;
625
+ }
626
+ .adherence-skipped {
627
+ background: rgba(148, 163, 184, 0.14);
628
+ color: #94A3B8;
629
+ }
630
+
597
631
  /* Section Label */
598
632
  .section-label {
599
633
  font-size: 12px;