From a42ecf33ca704dee22f2e282db7210201dd0c426 Mon Sep 17 00:00:00 2001 From: Aegis Date: Fri, 24 Apr 2026 08:40:14 -0500 Subject: [PATCH 1/2] feat(observability): port entropy and drift routes --- web/src/routes/observability.ts | 122 ++++++++++++++++++++++++++++++++ web/tests/observability.test.ts | 98 ++++++++++++++++++++++++- 2 files changed, 218 insertions(+), 2 deletions(-) diff --git a/web/src/routes/observability.ts b/web/src/routes/observability.ts index 25bdd99..b73cf3f 100644 --- a/web/src/routes/observability.ts +++ b/web/src/routes/observability.ts @@ -3,9 +3,16 @@ import { Hono } from 'hono'; import type { Env } from '../types.js'; import { getAllProceduresWithDerivedStats, getActiveAgendaItems } from '../kernel/memory/index.js'; +import { detectEntropy } from '../kernel/scheduled/entropy.js'; +import { buildEdgeEnv } from '../edge-env.js'; const observability = new Hono<{ Bindings: Env }>(); +function boundedDays(value: string | undefined, fallback: number, max: number): number { + const days = parseInt(value ?? String(fallback), 10); + return Number.isNaN(days) || days < 1 || days > max ? fallback : days; +} + // ─── Shadow Write Stats ───────────────────────────────────── observability.get('/api/shadow-stats', async (c) => { @@ -46,6 +53,121 @@ observability.get('/api/shadow-read-stats', async (c) => { return c.json({ days, summary, by_site: bySite.results, recent: recent.results }); }); +// ─── Entropy ──────────────────────────────────────────────── + +observability.get('/api/entropy', async (c) => { + const env = buildEdgeEnv(c.env); + const report = await detectEntropy(env); + return c.json(report); +}); + +// ─── Shadow Read Drift ────────────────────────────────────── + +observability.get('/api/shadow-read-drift', async (c) => { + const days = boundedDays(c.req.query('days'), 7, 30); + const reader = c.req.query('reader'); + + const latestWhere = reader + ? "WHERE reader = ? AND sampled_at > datetime('now', '-' || ? || ' days')" + : "WHERE sampled_at > datetime('now', '-' || ? || ' days')"; + const latestBindings = reader ? [reader, days] : [days]; + const windowBindings = reader ? [days, reader] : [days]; + + const [distribution, readiness, topDrifters] = await Promise.all([ + c.env.DB.prepare(` + WITH ranked AS ( + SELECT reader, + ABS((cached_count - pre_tier_count) - derived_count) AS count_abs_drift, + ABS(cached_avg_latency_ms - derived_avg_latency_ms) AS latency_abs_drift, + ABS(cached_avg_cost - derived_avg_cost) AS cost_abs_drift, + ROW_NUMBER() OVER (PARTITION BY reader ORDER BY ABS((cached_count - pre_tier_count) - derived_count)) AS count_rank, + ROW_NUMBER() OVER (PARTITION BY reader ORDER BY ABS(cached_avg_latency_ms - derived_avg_latency_ms)) AS latency_rank, + ROW_NUMBER() OVER (PARTITION BY reader ORDER BY ABS(cached_avg_cost - derived_avg_cost)) AS cost_rank, + COUNT(*) OVER (PARTITION BY reader) AS n + FROM shadow_read_drift + WHERE sampled_at > datetime('now', '-' || ? || ' days') + ${reader ? 'AND reader = ?' : ''} + ) + SELECT reader, + MAX(n) AS samples, + ROUND(AVG(count_abs_drift), 2) AS avg_abs_count_drift, + ROUND(MAX(count_abs_drift), 2) AS max_abs_count_drift, + ROUND(MAX(CASE WHEN count_rank = MAX(1, (n + 1) / 2) THEN count_abs_drift END), 2) AS p50_count_drift, + ROUND(MAX(CASE WHEN count_rank = MAX(1, (n * 19 + 19) / 20) THEN count_abs_drift END), 2) AS p95_count_drift, + ROUND(MAX(CASE WHEN count_rank = MAX(1, (n * 99 + 99) / 100) THEN count_abs_drift END), 2) AS p99_count_drift, + ROUND(AVG(latency_abs_drift), 2) AS avg_latency_drift_ms, + ROUND(MAX(latency_abs_drift), 2) AS max_latency_drift_ms, + ROUND(MAX(CASE WHEN latency_rank = MAX(1, (n + 1) / 2) THEN latency_abs_drift END), 2) AS p50_latency_drift_ms, + ROUND(MAX(CASE WHEN latency_rank = MAX(1, (n * 19 + 19) / 20) THEN latency_abs_drift END), 2) AS p95_latency_drift_ms, + ROUND(MAX(CASE WHEN latency_rank = MAX(1, (n * 99 + 99) / 100) THEN latency_abs_drift END), 2) AS p99_latency_drift_ms, + ROUND(AVG(cost_abs_drift), 6) AS avg_cost_drift, + ROUND(MAX(cost_abs_drift), 6) AS max_cost_drift, + ROUND(MAX(CASE WHEN cost_rank = MAX(1, (n + 1) / 2) THEN cost_abs_drift END), 6) AS p50_cost_drift, + ROUND(MAX(CASE WHEN cost_rank = MAX(1, (n * 19 + 19) / 20) THEN cost_abs_drift END), 6) AS p95_cost_drift, + ROUND(MAX(CASE WHEN cost_rank = MAX(1, (n * 99 + 99) / 100) THEN cost_abs_drift END), 6) AS p99_cost_drift + FROM ranked + GROUP BY reader + `).bind(...windowBindings).all(), + + c.env.DB.prepare(` + WITH latest AS ( + SELECT reader, task_pattern, cached_count, cached_success_count, + cached_avg_latency_ms, cached_avg_cost, + derived_count, derived_success_count, + derived_avg_latency_ms, derived_avg_cost, + pre_tier_count, + ROW_NUMBER() OVER (PARTITION BY reader, task_pattern ORDER BY sampled_at DESC) as rn + FROM shadow_read_drift + ${latestWhere} + ) + SELECT + COUNT(*) as total_pairs, + COUNT(DISTINCT task_pattern) as distinct_procedures, + SUM(CASE WHEN pre_tier_count = 0 THEN 1 ELSE 0 END) as clean_pairs, + SUM(CASE + WHEN pre_tier_count = 0 + AND cached_count = derived_count + AND cached_success_count = derived_success_count + AND ABS(cached_avg_latency_ms - derived_avg_latency_ms) < 10 + AND ABS(cached_avg_cost - derived_avg_cost) < 0.0001 + THEN 1 ELSE 0 END) as ready_pairs + FROM latest WHERE rn = 1 + `).bind(...latestBindings).first(), + + c.env.DB.prepare(` + WITH latest_per_pattern AS ( + SELECT task_pattern, reader, + cached_count, derived_count, pre_tier_count, + cached_avg_latency_ms, derived_avg_latency_ms, + cached_avg_cost, derived_avg_cost, + sampled_at, + ROW_NUMBER() OVER (PARTITION BY task_pattern, reader ORDER BY sampled_at DESC) as rn + FROM shadow_read_drift + WHERE sampled_at > datetime('now', '-' || ? || ' days') + ${reader ? 'AND reader = ?' : ''} + ) + SELECT task_pattern, reader, + cached_count, derived_count, pre_tier_count, + ABS((cached_count - pre_tier_count) - derived_count) as count_drift, + ROUND(ABS(cached_avg_latency_ms - derived_avg_latency_ms), 1) as latency_drift, + ROUND(ABS(cached_avg_cost - derived_avg_cost), 6) as cost_drift, + sampled_at + FROM latest_per_pattern + WHERE rn = 1 + ORDER BY ABS((cached_count - pre_tier_count) - derived_count) DESC + LIMIT 15 + `).bind(...windowBindings).all(), + ]); + + return c.json({ + days, + reader_filter: reader ?? null, + distribution: distribution.results, + readiness, + top_drifters: topDrifters.results, + }); +}); + // ─── Agenda ───────────────────────────────────────────────── observability.get('/agenda', async (c) => { diff --git a/web/tests/observability.test.ts b/web/tests/observability.test.ts index e64f07c..64a6578 100755 --- a/web/tests/observability.test.ts +++ b/web/tests/observability.test.ts @@ -9,8 +9,19 @@ vi.mock('../src/kernel/memory/index.js', () => ({ getActiveAgendaItems: vi.fn().mockResolvedValue([]), })); -import { observability } from '../src/routes/observability.js'; -import { getAllProceduresWithDerivedStats, getActiveAgendaItems } from '../src/kernel/memory/index.js'; +vi.mock('../src/kernel/scheduled/entropy.js', () => ({ + detectEntropy: vi.fn().mockResolvedValue({ + score: 0, + ghostAgendaItems: [], + ghostTasks: [], + dormantGoals: [], + summary: 'Entropy 0% - all clear.', + }), +})); + +import { observability } from '../src/routes/observability.js'; +import { getAllProceduresWithDerivedStats, getActiveAgendaItems } from '../src/kernel/memory/index.js'; +import { detectEntropy } from '../src/kernel/scheduled/entropy.js'; import type { Env } from '../src/types.js'; // ─── D1 Mock ────────────────────────────────────────────────── @@ -134,6 +145,89 @@ describe('observability routes', () => { }); }); + describe('GET /api/entropy', () => { + it('returns entropy report from detector', async () => { + const report = { + score: 25, + ghostAgendaItems: [{ id: 1, item: 'Stale item', daysSinceCreated: 9 }], + ghostTasks: [], + dormantGoals: [], + summary: 'Entropy 25% - 1 agenda item stale >7d.', + }; + vi.mocked(detectEntropy).mockResolvedValueOnce(report as any); + + const db = createMockDb(); + const app = createApp(db); + + const res = await app.request('/api/entropy'); + expect(res.status).toBe(200); + expect(await res.json()).toEqual(report); + expect(detectEntropy).toHaveBeenCalledWith(expect.objectContaining({ db })); + }); + }); + + describe('GET /api/shadow-read-drift', () => { + it('returns drift distribution, readiness, and top drifters', async () => { + const distribution = [{ + reader: 'observability', + samples: 4, + p95_count_drift: 1, + p95_latency_drift_ms: 5, + p95_cost_drift: 0.00001, + }]; + const readiness = { + total_pairs: 2, + distinct_procedures: 2, + clean_pairs: 2, + ready_pairs: 1, + }; + const topDrifters = [{ + task_pattern: 'dispatch:mid', + reader: 'observability', + count_drift: 1, + latency_drift: 5, + cost_drift: 0.00001, + sampled_at: '2026-04-24 00:00:00', + }]; + const db = createMockDb({ + firstResults: [readiness], + allResults: [distribution, topDrifters], + }); + const app = createApp(db); + + const res = await app.request('/api/shadow-read-drift?days=14&reader=observability'); + expect(res.status).toBe(200); + const json = await res.json() as any; + expect(json).toEqual({ + days: 14, + reader_filter: 'observability', + distribution, + readiness, + top_drifters: topDrifters, + }); + expect(db._queries[0].bindings).toEqual([14, 'observability']); + expect(db._queries[1].bindings).toEqual(['observability', 14]); + expect(db._queries[2].bindings).toEqual([14, 'observability']); + }); + + it('defaults invalid days and omits reader filter', async () => { + const db = createMockDb({ + firstResults: [{ total_pairs: 0, distinct_procedures: 0, clean_pairs: 0, ready_pairs: 0 }], + allResults: [[], []], + }); + const app = createApp(db); + + const res = await app.request('/api/shadow-read-drift?days=999'); + expect(res.status).toBe(200); + const json = await res.json() as any; + expect(json.days).toBe(7); + expect(json.reader_filter).toBeNull(); + expect(db._queries[0].bindings).toEqual([7]); + expect(db._queries[1].bindings).toEqual([7]); + expect(db._queries[2].bindings).toEqual([7]); + }); + }); + describe('GET /agenda', () => { it('returns active agenda items', async () => { const items = [ From ec312c0d253e9f08f0da8fb62598986144653357 Mon Sep 17 00:00:00 2001 From: Aegis Date: Fri, 8 May 2026 07:39:55 -0500 Subject: [PATCH 2/2] review(observability): address PR #42 feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comment on entropy route explaining why buildEdgeEnv is needed (detectEntropy requires full EdgeEnv — API keys, model config — not just DB) - Add comment clarifying latestBindings vs windowBindings order difference - Add test for days=0 to complete boundedDays lower-bound coverage Co-Authored-By: Claude Sonnet 4.6 --- web/src/routes/observability.ts | 3 + web/tests/observability.test.ts | 205 +++++++++++++++++--------------- 2 files changed, 112 insertions(+), 96 deletions(-) diff --git a/web/src/routes/observability.ts b/web/src/routes/observability.ts index b73cf3f..c37f854 100644 --- a/web/src/routes/observability.ts +++ b/web/src/routes/observability.ts @@ -56,6 +56,7 @@ observability.get('/api/shadow-read-stats', async (c) => { // ─── Entropy ──────────────────────────────────────────────── observability.get('/api/entropy', async (c) => { + // detectEntropy needs the full EdgeEnv (API keys, model config) — not just c.env.DB const env = buildEdgeEnv(c.env); const report = await detectEntropy(env); return c.json(report); @@ -70,6 +71,8 @@ observability.get('/api/shadow-read-drift', async (c) => { const latestWhere = reader ? "WHERE reader = ? AND sampled_at > datetime('now', '-' || ? || ' days')" : "WHERE sampled_at > datetime('now', '-' || ? || ' days')"; + // latestBindings: reader-first to match latestWhere (WHERE reader = ? AND sampled_at...) + // windowBindings: days-first to match the WHERE sampled_at... AND reader = ? pattern used in distribution/topDrifters const latestBindings = reader ? [reader, days] : [days]; const windowBindings = reader ? [days, reader] : [days]; diff --git a/web/tests/observability.test.ts b/web/tests/observability.test.ts index 64a6578..df87eb8 100755 --- a/web/tests/observability.test.ts +++ b/web/tests/observability.test.ts @@ -9,19 +9,19 @@ vi.mock('../src/kernel/memory/index.js', () => ({ getActiveAgendaItems: vi.fn().mockResolvedValue([]), })); -vi.mock('../src/kernel/scheduled/entropy.js', () => ({ - detectEntropy: vi.fn().mockResolvedValue({ - score: 0, - ghostAgendaItems: [], - ghostTasks: [], - dormantGoals: [], - summary: 'Entropy 0% - all clear.', - }), -})); - -import { observability } from '../src/routes/observability.js'; -import { getAllProceduresWithDerivedStats, getActiveAgendaItems } from '../src/kernel/memory/index.js'; -import { detectEntropy } from '../src/kernel/scheduled/entropy.js'; +vi.mock('../src/kernel/scheduled/entropy.js', () => ({ + detectEntropy: vi.fn().mockResolvedValue({ + score: 0, + ghostAgendaItems: [], + ghostTasks: [], + dormantGoals: [], + summary: 'Entropy 0% - all clear.', + }), +})); + +import { observability } from '../src/routes/observability.js'; +import { getAllProceduresWithDerivedStats, getActiveAgendaItems } from '../src/kernel/memory/index.js'; +import { detectEntropy } from '../src/kernel/scheduled/entropy.js'; import type { Env } from '../src/types.js'; // ─── D1 Mock ────────────────────────────────────────────────── @@ -145,89 +145,102 @@ describe('observability routes', () => { }); }); - describe('GET /api/entropy', () => { - it('returns entropy report from detector', async () => { - const report = { - score: 25, - ghostAgendaItems: [{ id: 1, item: 'Stale item', daysSinceCreated: 9 }], - ghostTasks: [], - dormantGoals: [], - summary: 'Entropy 25% - 1 agenda item stale >7d.', - }; - vi.mocked(detectEntropy).mockResolvedValueOnce(report as any); - - const db = createMockDb(); - const app = createApp(db); - - const res = await app.request('/api/entropy'); - expect(res.status).toBe(200); - expect(await res.json()).toEqual(report); - expect(detectEntropy).toHaveBeenCalledWith(expect.objectContaining({ db })); - }); - }); - - describe('GET /api/shadow-read-drift', () => { - it('returns drift distribution, readiness, and top drifters', async () => { - const distribution = [{ - reader: 'observability', - samples: 4, - p95_count_drift: 1, - p95_latency_drift_ms: 5, - p95_cost_drift: 0.00001, - }]; - const readiness = { - total_pairs: 2, - distinct_procedures: 2, - clean_pairs: 2, - ready_pairs: 1, - }; - const topDrifters = [{ - task_pattern: 'dispatch:mid', - reader: 'observability', - count_drift: 1, - latency_drift: 5, - cost_drift: 0.00001, - sampled_at: '2026-04-24 00:00:00', - }]; - const db = createMockDb({ - firstResults: [readiness], - allResults: [distribution, topDrifters], - }); - const app = createApp(db); - - const res = await app.request('/api/shadow-read-drift?days=14&reader=observability'); - expect(res.status).toBe(200); - const json = await res.json() as any; - expect(json).toEqual({ - days: 14, - reader_filter: 'observability', - distribution, - readiness, - top_drifters: topDrifters, - }); - expect(db._queries[0].bindings).toEqual([14, 'observability']); - expect(db._queries[1].bindings).toEqual(['observability', 14]); - expect(db._queries[2].bindings).toEqual([14, 'observability']); - }); - - it('defaults invalid days and omits reader filter', async () => { - const db = createMockDb({ - firstResults: [{ total_pairs: 0, distinct_procedures: 0, clean_pairs: 0, ready_pairs: 0 }], - allResults: [[], []], - }); - const app = createApp(db); - - const res = await app.request('/api/shadow-read-drift?days=999'); - expect(res.status).toBe(200); - const json = await res.json() as any; - expect(json.days).toBe(7); - expect(json.reader_filter).toBeNull(); - expect(db._queries[0].bindings).toEqual([7]); - expect(db._queries[1].bindings).toEqual([7]); - expect(db._queries[2].bindings).toEqual([7]); - }); - }); - + describe('GET /api/entropy', () => { + it('returns entropy report from detector', async () => { + const report = { + score: 25, + ghostAgendaItems: [{ id: 1, item: 'Stale item', daysSinceCreated: 9 }], + ghostTasks: [], + dormantGoals: [], + summary: 'Entropy 25% - 1 agenda item stale >7d.', + }; + vi.mocked(detectEntropy).mockResolvedValueOnce(report as any); + + const db = createMockDb(); + const app = createApp(db); + + const res = await app.request('/api/entropy'); + expect(res.status).toBe(200); + expect(await res.json()).toEqual(report); + expect(detectEntropy).toHaveBeenCalledWith(expect.objectContaining({ db })); + }); + }); + + describe('GET /api/shadow-read-drift', () => { + it('returns drift distribution, readiness, and top drifters', async () => { + const distribution = [{ + reader: 'observability', + samples: 4, + p95_count_drift: 1, + p95_latency_drift_ms: 5, + p95_cost_drift: 0.00001, + }]; + const readiness = { + total_pairs: 2, + distinct_procedures: 2, + clean_pairs: 2, + ready_pairs: 1, + }; + const topDrifters = [{ + task_pattern: 'dispatch:mid', + reader: 'observability', + count_drift: 1, + latency_drift: 5, + cost_drift: 0.00001, + sampled_at: '2026-04-24 00:00:00', + }]; + const db = createMockDb({ + firstResults: [readiness], + allResults: [distribution, topDrifters], + }); + const app = createApp(db); + + const res = await app.request('/api/shadow-read-drift?days=14&reader=observability'); + expect(res.status).toBe(200); + const json = await res.json() as any; + expect(json).toEqual({ + days: 14, + reader_filter: 'observability', + distribution, + readiness, + top_drifters: topDrifters, + }); + expect(db._queries[0].bindings).toEqual([14, 'observability']); + expect(db._queries[1].bindings).toEqual(['observability', 14]); + expect(db._queries[2].bindings).toEqual([14, 'observability']); + }); + + it('defaults invalid days and omits reader filter', async () => { + const db = createMockDb({ + firstResults: [{ total_pairs: 0, distinct_procedures: 0, clean_pairs: 0, ready_pairs: 0 }], + allResults: [[], []], + }); + const app = createApp(db); + + const res = await app.request('/api/shadow-read-drift?days=999'); + expect(res.status).toBe(200); + const json = await res.json() as any; + expect(json.days).toBe(7); + expect(json.reader_filter).toBeNull(); + expect(db._queries[0].bindings).toEqual([7]); + expect(db._queries[1].bindings).toEqual([7]); + expect(db._queries[2].bindings).toEqual([7]); + }); + + it('defaults days=0 to fallback', async () => { + const db = createMockDb({ + firstResults: [{ total_pairs: 0, distinct_procedures: 0, clean_pairs: 0, ready_pairs: 0 }], + allResults: [[], []], + }); + const app = createApp(db); + + const res = await app.request('/api/shadow-read-drift?days=0'); + expect(res.status).toBe(200); + const json = await res.json() as any; + expect(json.days).toBe(7); + }); + }); + describe('GET /agenda', () => { it('returns active agenda items', async () => { const items = [