selftune 0.2.22 → 0.2.24

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 (270) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +95 -15
  3. package/apps/local-dashboard/dist/assets/index-DgY2KGP-.css +1 -0
  4. package/apps/local-dashboard/dist/assets/index-Dmx7LPVX.js +15 -0
  5. package/apps/local-dashboard/dist/assets/vendor-react-C5oyHiV1.js +11 -0
  6. package/apps/local-dashboard/dist/assets/{vendor-table-BIiI3YhS.js → vendor-table-Bc_bbKd8.js} +1 -1
  7. package/apps/local-dashboard/dist/assets/vendor-ui-B3BPIYy7.js +1 -0
  8. package/apps/local-dashboard/dist/index.html +5 -5
  9. package/cli/selftune/adapters/codex/install.ts +310 -78
  10. package/cli/selftune/adapters/opencode/install.ts +3 -4
  11. package/cli/selftune/adapters/pi/hook.ts +273 -0
  12. package/cli/selftune/adapters/pi/install.ts +207 -0
  13. package/cli/selftune/alpha-upload/build-payloads.ts +3 -3
  14. package/cli/selftune/alpha-upload/stage-canonical.ts +17 -11
  15. package/cli/selftune/auto-update.ts +200 -8
  16. package/cli/selftune/canonical-export.ts +55 -25
  17. package/cli/selftune/command-surface.ts +397 -0
  18. package/cli/selftune/constants.ts +10 -1
  19. package/cli/selftune/contribute/contribute.ts +64 -13
  20. package/cli/selftune/contribution-config.ts +57 -3
  21. package/cli/selftune/contribution-preferences.ts +117 -0
  22. package/cli/selftune/contribution-signals.ts +8 -4
  23. package/cli/selftune/contribution-staging.ts +13 -2
  24. package/cli/selftune/contributions.ts +55 -121
  25. package/cli/selftune/creator-contributions.ts +29 -10
  26. package/cli/selftune/cron/setup.ts +7 -3
  27. package/cli/selftune/dashboard-contract.ts +87 -0
  28. package/cli/selftune/dashboard-server.ts +168 -17
  29. package/cli/selftune/dashboard.ts +350 -17
  30. package/cli/selftune/eval/baseline.ts +21 -5
  31. package/cli/selftune/eval/execution-eval.ts +170 -0
  32. package/cli/selftune/eval/family-overlap.ts +2 -2
  33. package/cli/selftune/eval/hooks-to-evals.ts +228 -82
  34. package/cli/selftune/eval/import-skillsbench.ts +2 -2
  35. package/cli/selftune/eval/invocation-classifier.ts +56 -0
  36. package/cli/selftune/eval/synthetic-evals.ts +5 -3
  37. package/cli/selftune/eval/unit-test-cli.ts +7 -4
  38. package/cli/selftune/evolution/apply-proposal.ts +295 -0
  39. package/cli/selftune/evolution/engines/judge-engine.ts +96 -0
  40. package/cli/selftune/evolution/engines/replay-engine.ts +180 -0
  41. package/cli/selftune/evolution/evidence.ts +2 -6
  42. package/cli/selftune/evolution/evolve-body.ts +152 -38
  43. package/cli/selftune/evolution/evolve.ts +244 -52
  44. package/cli/selftune/evolution/rollback.ts +0 -1
  45. package/cli/selftune/evolution/validate-body.ts +111 -49
  46. package/cli/selftune/evolution/validate-host-replay.ts +510 -60
  47. package/cli/selftune/evolution/validate-proposal.ts +11 -150
  48. package/cli/selftune/evolution/validate-routing.ts +51 -108
  49. package/cli/selftune/evolution/validation-contract.ts +91 -0
  50. package/cli/selftune/grading/auto-grade.ts +11 -7
  51. package/cli/selftune/grading/grade-session.ts +10 -16
  52. package/cli/selftune/hooks/skill-eval.ts +2 -1
  53. package/cli/selftune/hooks-shared/types.ts +1 -0
  54. package/cli/selftune/index.ts +58 -15
  55. package/cli/selftune/ingestors/claude-replay.ts +15 -10
  56. package/cli/selftune/ingestors/codex-wrapper.ts +3 -3
  57. package/cli/selftune/ingestors/opencode-ingest.ts +2 -2
  58. package/cli/selftune/ingestors/pi-ingest.ts +727 -0
  59. package/cli/selftune/init.ts +38 -4
  60. package/cli/selftune/localdb/direct-write.ts +120 -1
  61. package/cli/selftune/localdb/materialize.ts +6 -7
  62. package/cli/selftune/localdb/queries/cron.ts +34 -0
  63. package/cli/selftune/localdb/queries/dashboard.ts +834 -0
  64. package/cli/selftune/localdb/queries/evolution.ts +158 -0
  65. package/cli/selftune/localdb/queries/execution.ts +133 -0
  66. package/cli/selftune/localdb/queries/json.ts +18 -0
  67. package/cli/selftune/localdb/queries/monitoring.ts +263 -0
  68. package/cli/selftune/localdb/queries/raw.ts +95 -0
  69. package/cli/selftune/localdb/queries/staging.ts +270 -0
  70. package/cli/selftune/localdb/queries/trust.ts +392 -0
  71. package/cli/selftune/localdb/queries.ts +60 -2162
  72. package/cli/selftune/localdb/schema.ts +59 -0
  73. package/cli/selftune/monitoring/watch.ts +96 -29
  74. package/cli/selftune/normalization.ts +3 -0
  75. package/cli/selftune/observability.ts +12 -3
  76. package/cli/selftune/orchestrate/cli.ts +161 -0
  77. package/cli/selftune/orchestrate/execute.ts +295 -0
  78. package/cli/selftune/orchestrate/finalize.ts +157 -0
  79. package/cli/selftune/orchestrate/locks.ts +40 -0
  80. package/cli/selftune/orchestrate/plan.ts +131 -0
  81. package/cli/selftune/orchestrate/post-run.ts +59 -0
  82. package/cli/selftune/orchestrate/prepare.ts +334 -0
  83. package/cli/selftune/orchestrate/report.ts +182 -0
  84. package/cli/selftune/orchestrate/runtime.ts +120 -0
  85. package/cli/selftune/orchestrate/signals.ts +48 -0
  86. package/cli/selftune/orchestrate.ts +162 -1142
  87. package/cli/selftune/registry/client.ts +74 -0
  88. package/cli/selftune/registry/history.ts +54 -0
  89. package/cli/selftune/registry/index.ts +90 -0
  90. package/cli/selftune/registry/install.ts +141 -0
  91. package/cli/selftune/registry/list.ts +44 -0
  92. package/cli/selftune/registry/push.ts +171 -0
  93. package/cli/selftune/registry/rollback.ts +49 -0
  94. package/cli/selftune/registry/status.ts +62 -0
  95. package/cli/selftune/registry/sync.ts +125 -0
  96. package/cli/selftune/repair/skill-usage.ts +9 -3
  97. package/cli/selftune/routes/overview.ts +5 -2
  98. package/cli/selftune/routes/skill-report.ts +15 -2
  99. package/cli/selftune/schedule.ts +5 -5
  100. package/cli/selftune/status.ts +70 -2
  101. package/cli/selftune/sync.ts +127 -23
  102. package/cli/selftune/testing-readiness.ts +597 -0
  103. package/cli/selftune/types.ts +46 -5
  104. package/cli/selftune/uninstall.ts +2 -1
  105. package/cli/selftune/utils/canonical-log.ts +1 -9
  106. package/cli/selftune/utils/cli-error.ts +9 -0
  107. package/cli/selftune/utils/jsonl.ts +1 -30
  108. package/cli/selftune/utils/llm-call.ts +126 -6
  109. package/cli/selftune/utils/skill-discovery.ts +24 -0
  110. package/cli/selftune/workflows/proposals.ts +184 -0
  111. package/cli/selftune/workflows/skill-scaffold.ts +241 -0
  112. package/cli/selftune/workflows/workflows.ts +100 -26
  113. package/node_modules/@selftune/telemetry-contract/fixtures/complete-push.ts +1 -1
  114. package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +2 -2
  115. package/node_modules/@selftune/telemetry-contract/fixtures/golden.test.ts +0 -1
  116. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
  117. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +2 -2
  118. package/node_modules/@selftune/telemetry-contract/package.json +1 -1
  119. package/node_modules/@selftune/telemetry-contract/src/index.ts +1 -0
  120. package/node_modules/@selftune/telemetry-contract/src/schemas.ts +63 -5
  121. package/node_modules/@selftune/telemetry-contract/src/types.ts +97 -7
  122. package/node_modules/@selftune/telemetry-contract/tests/compatibility.test.ts +0 -1
  123. package/package.json +25 -9
  124. package/packages/dashboard-core/AGENTS.md +18 -0
  125. package/packages/dashboard-core/README.md +30 -0
  126. package/packages/dashboard-core/index.ts +3 -0
  127. package/packages/dashboard-core/package.json +39 -0
  128. package/packages/dashboard-core/src/chrome/DashboardChrome.tsx +74 -0
  129. package/packages/dashboard-core/src/chrome/DashboardHeader.tsx +200 -0
  130. package/packages/dashboard-core/src/chrome/DashboardSidebar.tsx +219 -0
  131. package/packages/dashboard-core/src/chrome/RuntimeBadge.tsx +46 -0
  132. package/packages/dashboard-core/src/chrome/index.ts +14 -0
  133. package/packages/dashboard-core/src/chrome/types.ts +81 -0
  134. package/packages/dashboard-core/src/chrome/utils.ts +23 -0
  135. package/packages/dashboard-core/src/gates/FeatureGate.tsx +11 -0
  136. package/packages/dashboard-core/src/gates/LockedRoute.tsx +29 -0
  137. package/packages/dashboard-core/src/gates/UpgradeCard.tsx +89 -0
  138. package/packages/dashboard-core/src/gates/index.ts +3 -0
  139. package/packages/dashboard-core/src/host/DashboardHostProvider.tsx +62 -0
  140. package/packages/dashboard-core/src/host/adapter.ts +47 -0
  141. package/packages/dashboard-core/src/host/capabilities.ts +55 -0
  142. package/packages/dashboard-core/src/host/index.ts +3 -0
  143. package/packages/dashboard-core/src/models/analytics.ts +39 -0
  144. package/packages/dashboard-core/src/models/index.ts +4 -0
  145. package/packages/dashboard-core/src/models/overview.ts +98 -0
  146. package/packages/dashboard-core/src/models/runtime.ts +7 -0
  147. package/packages/dashboard-core/src/models/skills.ts +34 -0
  148. package/packages/dashboard-core/src/routes/index.ts +2 -0
  149. package/packages/dashboard-core/src/routes/manifest.test.ts +70 -0
  150. package/packages/dashboard-core/src/routes/manifest.ts +451 -0
  151. package/packages/dashboard-core/src/routes/types.ts +39 -0
  152. package/packages/dashboard-core/src/screens/analytics/AnalyticsScreen.tsx +278 -0
  153. package/packages/dashboard-core/src/screens/analytics/index.ts +1 -0
  154. package/packages/dashboard-core/src/screens/index.ts +37 -0
  155. package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.test.ts +101 -0
  156. package/packages/dashboard-core/src/screens/overview/OverviewComparisonSurface.tsx +393 -0
  157. package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.test.tsx +113 -0
  158. package/packages/dashboard-core/src/screens/overview/OverviewCompositionSurface.tsx +72 -0
  159. package/packages/dashboard-core/src/screens/overview/OverviewCoreSurface.tsx +71 -0
  160. package/packages/dashboard-core/src/screens/overview/OverviewOnboardingBanner.tsx +90 -0
  161. package/packages/dashboard-core/src/screens/overview/OverviewRunSummary.tsx +40 -0
  162. package/packages/dashboard-core/src/screens/overview/index.ts +16 -0
  163. package/packages/dashboard-core/src/screens/overview/types.ts +13 -0
  164. package/packages/dashboard-core/src/screens/skill-report/SkillReportDailyBreakdownSection.tsx +99 -0
  165. package/packages/dashboard-core/src/screens/skill-report/SkillReportDataQualityTabContent.tsx +35 -0
  166. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceRail.tsx +71 -0
  167. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceSection.tsx +63 -0
  168. package/packages/dashboard-core/src/screens/skill-report/SkillReportEvidenceTabContent.tsx +25 -0
  169. package/packages/dashboard-core/src/screens/skill-report/SkillReportInvocationsSection.tsx +24 -0
  170. package/packages/dashboard-core/src/screens/skill-report/SkillReportMissedQueriesSection.tsx +79 -0
  171. package/packages/dashboard-core/src/screens/skill-report/SkillReportScaffold.tsx +150 -0
  172. package/packages/dashboard-core/src/screens/skill-report/SkillReportSections.test.tsx +224 -0
  173. package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.test.tsx +76 -0
  174. package/packages/dashboard-core/src/screens/skill-report/SkillReportTabs.tsx +88 -0
  175. package/packages/dashboard-core/src/screens/skill-report/SkillReportTrendSection.tsx +33 -0
  176. package/packages/dashboard-core/src/screens/skill-report/SkillReportTrustBadge.tsx +67 -0
  177. package/packages/dashboard-core/src/screens/skill-report/index.ts +45 -0
  178. package/packages/dashboard-core/src/screens/skills/SkillsLibraryScreen.tsx +162 -0
  179. package/packages/dashboard-core/src/screens/skills/index.ts +6 -0
  180. package/packages/telemetry-contract/fixtures/complete-push.ts +1 -1
  181. package/packages/telemetry-contract/fixtures/evidence-only-push.ts +2 -2
  182. package/packages/telemetry-contract/fixtures/golden.test.ts +0 -1
  183. package/packages/telemetry-contract/fixtures/partial-push-no-sessions.ts +1 -1
  184. package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +2 -2
  185. package/packages/telemetry-contract/package.json +1 -1
  186. package/packages/telemetry-contract/src/index.ts +1 -0
  187. package/packages/telemetry-contract/src/schemas.ts +63 -5
  188. package/packages/telemetry-contract/src/types.ts +97 -7
  189. package/packages/telemetry-contract/tests/compatibility.test.ts +0 -1
  190. package/packages/ui/AGENTS.md +16 -0
  191. package/packages/ui/README.md +1 -1
  192. package/packages/ui/package.json +1 -1
  193. package/packages/ui/src/components/ActivityTimeline.tsx +152 -168
  194. package/packages/ui/src/components/AnalyticsCharts.tsx +344 -0
  195. package/packages/ui/src/components/EvidenceViewer.tsx +229 -464
  196. package/packages/ui/src/components/EvolutionTimeline.tsx +34 -87
  197. package/packages/ui/src/components/InfoTip.tsx +1 -2
  198. package/packages/ui/src/components/InvocationsPanel.tsx +413 -0
  199. package/packages/ui/src/components/JobHistoryTimeline.tsx +156 -0
  200. package/packages/ui/src/components/OrchestrateRunsPanel.tsx +18 -36
  201. package/packages/ui/src/components/OverviewPanels.tsx +693 -0
  202. package/packages/ui/src/components/PipelineStatusBar.tsx +65 -0
  203. package/packages/ui/src/components/SkillReportGuide.tsx +215 -0
  204. package/packages/ui/src/components/SkillReportPanels.tsx +919 -0
  205. package/packages/ui/src/components/SkillsLibrary.tsx +437 -0
  206. package/packages/ui/src/components/index.ts +56 -1
  207. package/packages/ui/src/components/section-cards.tsx +18 -35
  208. package/packages/ui/src/components/skill-health-grid.tsx +47 -37
  209. package/packages/ui/src/lib/constants.tsx +0 -1
  210. package/packages/ui/src/primitives/card.tsx +1 -1
  211. package/packages/ui/src/primitives/checkbox.tsx +1 -1
  212. package/packages/ui/src/primitives/dropdown-menu.tsx +2 -2
  213. package/packages/ui/src/primitives/select.tsx +2 -2
  214. package/packages/ui/src/primitives/tabs.tsx +7 -6
  215. package/packages/ui/src/types.ts +182 -4
  216. package/skill/SKILL.md +130 -318
  217. package/skill/agents/diagnosis-analyst.md +3 -3
  218. package/skill/agents/evolution-reviewer.md +3 -3
  219. package/skill/agents/integration-guide.md +3 -3
  220. package/skill/agents/pattern-analyst.md +2 -2
  221. package/skill/references/cli-quick-reference.md +89 -0
  222. package/skill/references/creator-playbook.md +131 -0
  223. package/skill/references/examples.md +48 -0
  224. package/skill/references/troubleshooting.md +47 -0
  225. package/skill/references/version-history.md +1 -1
  226. package/skill/selftune.contribute.json +11 -0
  227. package/skill/{Workflows → workflows}/Baseline.md +20 -1
  228. package/skill/{Workflows → workflows}/Contribute.md +23 -10
  229. package/skill/{Workflows → workflows}/Contributions.md +13 -5
  230. package/skill/workflows/CreateTestDeploy.md +170 -0
  231. package/skill/{Workflows → workflows}/CreatorContributions.md +18 -6
  232. package/skill/{Workflows → workflows}/Cron.md +1 -1
  233. package/skill/{Workflows → workflows}/Dashboard.md +20 -0
  234. package/skill/{Workflows → workflows}/Doctor.md +1 -1
  235. package/skill/{Workflows → workflows}/Evals.md +67 -2
  236. package/skill/{Workflows → workflows}/Evolve.md +119 -30
  237. package/skill/{Workflows → workflows}/EvolveBody.md +41 -1
  238. package/skill/{Workflows → workflows}/Grade.md +1 -1
  239. package/skill/{Workflows → workflows}/Ingest.md +60 -2
  240. package/skill/{Workflows → workflows}/Initialize.md +16 -9
  241. package/skill/{Workflows → workflows}/Orchestrate.md +13 -3
  242. package/skill/{Workflows → workflows}/PlatformHooks.md +19 -3
  243. package/skill/workflows/Registry.md +99 -0
  244. package/skill/{Workflows → workflows}/Schedule.md +3 -3
  245. package/skill/workflows/SignalsDashboard.md +87 -0
  246. package/skill/{Workflows → workflows}/Sync.md +3 -1
  247. package/skill/{Workflows → workflows}/UnitTest.md +19 -0
  248. package/skill/{Workflows → workflows}/Watch.md +42 -2
  249. package/skill/{Workflows → workflows}/Workflows.md +39 -2
  250. package/apps/local-dashboard/dist/assets/index-D8O-RG1I.js +0 -60
  251. package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +0 -1
  252. package/apps/local-dashboard/dist/assets/vendor-react-CKkiCskZ.js +0 -11
  253. package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +0 -12
  254. package/cli/selftune/utils/html.ts +0 -27
  255. package/packages/ui/src/components/RecentActivityFeed.tsx +0 -117
  256. /package/skill/{Workflows → workflows}/AlphaUpload.md +0 -0
  257. /package/skill/{Workflows → workflows}/AutoActivation.md +0 -0
  258. /package/skill/{Workflows → workflows}/Badge.md +0 -0
  259. /package/skill/{Workflows → workflows}/Composability.md +0 -0
  260. /package/skill/{Workflows → workflows}/EvolutionMemory.md +0 -0
  261. /package/skill/{Workflows → workflows}/ExportCanonical.md +0 -0
  262. /package/skill/{Workflows → workflows}/Hook.md +0 -0
  263. /package/skill/{Workflows → workflows}/ImportSkillsBench.md +0 -0
  264. /package/skill/{Workflows → workflows}/Quickstart.md +0 -0
  265. /package/skill/{Workflows → workflows}/Recover.md +0 -0
  266. /package/skill/{Workflows → workflows}/RepairSkillUsage.md +0 -0
  267. /package/skill/{Workflows → workflows}/Replay.md +0 -0
  268. /package/skill/{Workflows → workflows}/Rollback.md +0 -0
  269. /package/skill/{Workflows → workflows}/Telemetry.md +0 -0
  270. /package/skill/{Workflows → workflows}/Uninstall.md +0 -0
@@ -1,2162 +1,60 @@
1
- /**
2
- * Query helpers for the selftune local SQLite materialized view store.
3
- *
4
- * These return payload shapes that match what the dashboard and report
5
- * pages need, so the HTTP layer can serve them directly.
6
- */
7
-
8
- import type { Database } from "bun:sqlite";
9
-
10
- import type {
11
- AnalyticsResponse,
12
- AttentionItem,
13
- AutonomousDecision,
14
- CommitRecord,
15
- CommitSummary,
16
- DecisionKind,
17
- ExecutionMetrics,
18
- OrchestrateRunReport,
19
- OverviewPaginatedPayload,
20
- OverviewPayload,
21
- PaginatedResult,
22
- PaginationCursor,
23
- PendingProposal,
24
- RecentActivityItem,
25
- SkillReportPaginatedPayload,
26
- SkillReportPayload,
27
- SkillSummary,
28
- SkillUsageRecord,
29
- TelemetryRecord,
30
- } from "../dashboard-contract.js";
31
-
32
- /**
33
- * Build the overview payload from SQLite, suitable for the dashboard main page.
34
- */
35
- export function getOverviewPayload(db: Database): OverviewPayload {
36
- // Telemetry summary (bounded to most recent 1000)
37
- const telemetryRows = db
38
- .query(
39
- `SELECT timestamp, session_id, skills_triggered_json, errors_encountered, total_tool_calls
40
- FROM session_telemetry
41
- ORDER BY timestamp DESC
42
- LIMIT 1000`,
43
- )
44
- .all() as Array<{
45
- timestamp: string;
46
- session_id: string;
47
- skills_triggered_json: string | null;
48
- errors_encountered: number;
49
- total_tool_calls: number;
50
- }>;
51
-
52
- const telemetry = telemetryRows.map((row) => ({
53
- timestamp: row.timestamp,
54
- session_id: row.session_id,
55
- skills_triggered: safeParseJsonArray<string>(row.skills_triggered_json),
56
- errors_encountered: row.errors_encountered,
57
- total_tool_calls: row.total_tool_calls,
58
- }));
59
-
60
- // Skill usage (bounded to most recent 2000)
61
- const skillRows = db
62
- .query(
63
- `SELECT occurred_at, session_id, skill_name, skill_path, query, triggered, source
64
- FROM skill_invocations
65
- ORDER BY occurred_at DESC
66
- LIMIT 2000`,
67
- )
68
- .all() as Array<{
69
- occurred_at: string;
70
- session_id: string;
71
- skill_name: string;
72
- skill_path: string;
73
- query: string;
74
- triggered: number;
75
- source: string | null;
76
- }>;
77
-
78
- const skills = skillRows.map((row) => ({
79
- timestamp: row.occurred_at,
80
- session_id: row.session_id,
81
- skill_name: row.skill_name,
82
- skill_path: row.skill_path,
83
- query: row.query,
84
- triggered: row.triggered === 1,
85
- source: row.source,
86
- }));
87
-
88
- // Evolution audit (bounded to most recent 500)
89
- const evolution = db
90
- .query(
91
- `SELECT timestamp, proposal_id, skill_name, action, details
92
- FROM evolution_audit
93
- ORDER BY timestamp DESC
94
- LIMIT 500`,
95
- )
96
- .all() as Array<{
97
- timestamp: string;
98
- proposal_id: string;
99
- skill_name: string | null;
100
- action: string;
101
- details: string;
102
- }>;
103
-
104
- // Counts (single query instead of 6 separate ones)
105
- const counts = db
106
- .query(
107
- `SELECT
108
- (SELECT COUNT(*) FROM session_telemetry) as telemetry,
109
- (SELECT COUNT(*) FROM skill_invocations) as skills,
110
- (SELECT COUNT(*) FROM evolution_audit) as evolution,
111
- (SELECT COUNT(*) FROM evolution_evidence) as evidence,
112
- (SELECT COUNT(*) FROM sessions) as sessions,
113
- (SELECT COUNT(*) FROM prompts) as prompts`,
114
- )
115
- .get() as {
116
- telemetry: number;
117
- skills: number;
118
- evolution: number;
119
- evidence: number;
120
- sessions: number;
121
- prompts: number;
122
- };
123
-
124
- // Unmatched queries: skill_invocations entries where triggered = 0 and no other
125
- // record for the same query text triggered
126
- const unmatchedRows = db
127
- .query(
128
- `SELECT si.occurred_at AS timestamp, si.session_id, si.query
129
- FROM skill_invocations si
130
- WHERE si.triggered = 0
131
- AND NOT EXISTS (
132
- SELECT 1 FROM skill_invocations si2
133
- WHERE si2.query = si.query AND si2.triggered = 1
134
- )
135
- ORDER BY si.occurred_at DESC
136
- LIMIT 500`,
137
- )
138
- .all() as Array<{ timestamp: string; session_id: string; query: string }>;
139
-
140
- // Pending proposals: created/validated but no terminal action (deduped in SQL)
141
- const pending_proposals = getPendingProposals(db);
142
-
143
- // Active sessions and recent activity
144
- const active_sessions = getActiveSessionCount(db);
145
- const recent_activity = getRecentActivity(db);
146
-
147
- return {
148
- telemetry,
149
- skills,
150
- evolution,
151
- counts,
152
- unmatched_queries: unmatchedRows,
153
- pending_proposals,
154
- active_sessions,
155
- recent_activity,
156
- };
157
- }
158
-
159
- /**
160
- * Build the skill report payload for a specific skill.
161
- */
162
- export function getSkillReportPayload(db: Database, skillName: string): SkillReportPayload {
163
- // Usage stats
164
- const usageRow = db
165
- .query(
166
- `SELECT
167
- COUNT(*) as total_checks,
168
- SUM(CASE WHEN triggered = 1 THEN 1 ELSE 0 END) as triggered_count
169
- FROM skill_invocations
170
- WHERE skill_name = ?`,
171
- )
172
- .get(skillName) as { total_checks: number; triggered_count: number };
173
-
174
- const total = usageRow.total_checks;
175
- const triggered = usageRow.triggered_count;
176
- const passRate = total > 0 ? triggered / total : 0;
177
-
178
- // Recent invocations (last 100)
179
- const invocationRows = db
180
- .query(
181
- `SELECT occurred_at, session_id, query, triggered, source
182
- FROM skill_invocations
183
- WHERE skill_name = ?
184
- ORDER BY occurred_at DESC
185
- LIMIT 100`,
186
- )
187
- .all(skillName) as Array<{
188
- occurred_at: string;
189
- session_id: string;
190
- query: string;
191
- triggered: number;
192
- source: string | null;
193
- }>;
194
-
195
- const recent_invocations = invocationRows.map((row) => ({
196
- timestamp: row.occurred_at,
197
- session_id: row.session_id,
198
- query: row.query,
199
- triggered: row.triggered === 1,
200
- source: row.source,
201
- }));
202
-
203
- // Evolution evidence (bounded to most recent 200)
204
- const evidenceRows = db
205
- .query(
206
- `SELECT proposal_id, target, stage, timestamp, rationale, confidence,
207
- original_text, proposed_text, validation_json, details, eval_set_json
208
- FROM evolution_evidence
209
- WHERE skill_name = ?
210
- ORDER BY timestamp DESC
211
- LIMIT 200`,
212
- )
213
- .all(skillName) as Array<{
214
- proposal_id: string;
215
- target: string;
216
- stage: string;
217
- timestamp: string;
218
- rationale: string | null;
219
- confidence: number | null;
220
- original_text: string | null;
221
- proposed_text: string | null;
222
- validation_json: string | null;
223
- details: string | null;
224
- eval_set_json: string | null;
225
- }>;
226
-
227
- const evidence = evidenceRows.map((row) => ({
228
- proposal_id: row.proposal_id,
229
- target: row.target,
230
- stage: row.stage,
231
- timestamp: row.timestamp,
232
- rationale: row.rationale,
233
- confidence: row.confidence,
234
- original_text: row.original_text,
235
- proposed_text: row.proposed_text,
236
- validation: safeParseJson(row.validation_json),
237
- details: row.details,
238
- eval_set: safeParseJsonArray<Record<string, unknown>>(row.eval_set_json),
239
- }));
240
-
241
- // Unique sessions count
242
- const sessionsRow = db
243
- .query(`SELECT COUNT(DISTINCT session_id) as c FROM skill_invocations WHERE skill_name = ?`)
244
- .get(skillName) as { c: number };
245
-
246
- return {
247
- skill_name: skillName,
248
- usage: {
249
- total_checks: total,
250
- triggered_count: triggered,
251
- pass_rate: passRate,
252
- },
253
- recent_invocations,
254
- evidence,
255
- sessions_with_skill: sessionsRow.c,
256
- };
257
- }
258
-
259
- // -- Cursor-based paginated queries -------------------------------------------
260
-
261
- export interface OverviewPaginationOptions {
262
- telemetry_cursor?: PaginationCursor | null;
263
- telemetry_limit?: number;
264
- skills_cursor?: PaginationCursor | null;
265
- skills_limit?: number;
266
- }
267
-
268
- export interface SkillReportPaginationOptions {
269
- invocations_cursor?: PaginationCursor | null;
270
- invocations_limit?: number;
271
- }
272
-
273
- /**
274
- * Build a paginated overview payload from SQLite.
275
- *
276
- * Uses (timestamp, session_id) composite cursors for stable backward pagination.
277
- * When no cursor is provided, returns the first page starting from most recent.
278
- */
279
- export function getOverviewPayloadPaginated(
280
- db: Database,
281
- opts: OverviewPaginationOptions = {},
282
- ): OverviewPaginatedPayload {
283
- const telemetryLimit = opts.telemetry_limit ?? 1000;
284
- const skillsLimit = opts.skills_limit ?? 2000;
285
-
286
- // Paginated telemetry
287
- const telemetry_page = paginateTelemetry(db, telemetryLimit, opts.telemetry_cursor ?? null);
288
-
289
- // Paginated skill invocations
290
- const skills_page = paginateSkillInvocations(db, skillsLimit, opts.skills_cursor ?? null);
291
-
292
- // Non-paginated parts reuse existing logic
293
- const evolution = db
294
- .query(
295
- `SELECT timestamp, proposal_id, skill_name, action, details
296
- FROM evolution_audit
297
- ORDER BY timestamp DESC
298
- LIMIT 500`,
299
- )
300
- .all() as Array<{
301
- timestamp: string;
302
- proposal_id: string;
303
- skill_name: string | null;
304
- action: string;
305
- details: string;
306
- }>;
307
-
308
- const counts = db
309
- .query(
310
- `SELECT
311
- (SELECT COUNT(*) FROM session_telemetry) as telemetry,
312
- (SELECT COUNT(*) FROM skill_invocations) as skills,
313
- (SELECT COUNT(*) FROM evolution_audit) as evolution,
314
- (SELECT COUNT(*) FROM evolution_evidence) as evidence,
315
- (SELECT COUNT(*) FROM sessions) as sessions,
316
- (SELECT COUNT(*) FROM prompts) as prompts`,
317
- )
318
- .get() as {
319
- telemetry: number;
320
- skills: number;
321
- evolution: number;
322
- evidence: number;
323
- sessions: number;
324
- prompts: number;
325
- };
326
-
327
- const unmatchedRows = db
328
- .query(
329
- `SELECT si.occurred_at AS timestamp, si.session_id, si.query
330
- FROM skill_invocations si
331
- WHERE si.triggered = 0
332
- AND NOT EXISTS (
333
- SELECT 1 FROM skill_invocations si2
334
- WHERE si2.query = si.query AND si2.triggered = 1
335
- )
336
- ORDER BY si.occurred_at DESC
337
- LIMIT 500`,
338
- )
339
- .all() as Array<{ timestamp: string; session_id: string; query: string }>;
340
-
341
- const pending_proposals = getPendingProposals(db);
342
- const active_sessions = getActiveSessionCount(db);
343
- const recent_activity = getRecentActivity(db);
344
-
345
- return {
346
- telemetry_page,
347
- skills_page,
348
- evolution,
349
- counts,
350
- unmatched_queries: unmatchedRows,
351
- pending_proposals,
352
- active_sessions,
353
- recent_activity,
354
- };
355
- }
356
-
357
- /**
358
- * Build a paginated skill report payload for a specific skill.
359
- *
360
- * Uses (occurred_at, skill_invocation_id) composite cursor for the recent
361
- * invocations sub-query. Non-paginated fields (usage stats, evidence, sessions)
362
- * are returned in full.
363
- */
364
- export function getSkillReportPayloadPaginated(
365
- db: Database,
366
- skillName: string,
367
- opts: SkillReportPaginationOptions = {},
368
- ): SkillReportPaginatedPayload {
369
- const invocationsLimit = opts.invocations_limit ?? 100;
370
-
371
- // Usage stats (unchanged)
372
- const usageRow = db
373
- .query(
374
- `SELECT
375
- COUNT(*) as total_checks,
376
- SUM(CASE WHEN triggered = 1 THEN 1 ELSE 0 END) as triggered_count
377
- FROM skill_invocations
378
- WHERE skill_name = ?`,
379
- )
380
- .get(skillName) as { total_checks: number; triggered_count: number };
381
-
382
- const total = usageRow.total_checks;
383
- const triggered = usageRow.triggered_count;
384
- const passRate = total > 0 ? triggered / total : 0;
385
-
386
- // Paginated invocations
387
- const invocations_page = paginateSkillReportInvocations(
388
- db,
389
- skillName,
390
- invocationsLimit,
391
- opts.invocations_cursor ?? null,
392
- );
393
-
394
- // Evidence (unchanged)
395
- const evidenceRows = db
396
- .query(
397
- `SELECT proposal_id, target, stage, timestamp, rationale, confidence,
398
- original_text, proposed_text, validation_json, details, eval_set_json
399
- FROM evolution_evidence
400
- WHERE skill_name = ?
401
- ORDER BY timestamp DESC
402
- LIMIT 200`,
403
- )
404
- .all(skillName) as Array<{
405
- proposal_id: string;
406
- target: string;
407
- stage: string;
408
- timestamp: string;
409
- rationale: string | null;
410
- confidence: number | null;
411
- original_text: string | null;
412
- proposed_text: string | null;
413
- validation_json: string | null;
414
- details: string | null;
415
- eval_set_json: string | null;
416
- }>;
417
-
418
- const evidence = evidenceRows.map((row) => ({
419
- proposal_id: row.proposal_id,
420
- target: row.target,
421
- stage: row.stage,
422
- timestamp: row.timestamp,
423
- rationale: row.rationale,
424
- confidence: row.confidence,
425
- original_text: row.original_text,
426
- proposed_text: row.proposed_text,
427
- validation: safeParseJson(row.validation_json),
428
- details: row.details,
429
- eval_set: safeParseJsonArray<Record<string, unknown>>(row.eval_set_json),
430
- }));
431
-
432
- const sessionsRow = db
433
- .query(`SELECT COUNT(DISTINCT session_id) as c FROM skill_invocations WHERE skill_name = ?`)
434
- .get(skillName) as { c: number };
435
-
436
- return {
437
- skill_name: skillName,
438
- usage: {
439
- total_checks: total,
440
- triggered_count: triggered,
441
- pass_rate: passRate,
442
- },
443
- invocations_page,
444
- evidence,
445
- sessions_with_skill: sessionsRow.c,
446
- };
447
- }
448
-
449
- // -- Internal pagination helpers ------------------------------------------------
450
-
451
- function paginateTelemetry(
452
- db: Database,
453
- limit: number,
454
- cursor: PaginationCursor | null,
455
- ): PaginatedResult<TelemetryRecord> {
456
- // Fetch one extra to detect has_more
457
- const fetchLimit = limit + 1;
458
-
459
- let rows: Array<{
460
- timestamp: string;
461
- session_id: string;
462
- skills_triggered_json: string | null;
463
- errors_encountered: number;
464
- total_tool_calls: number;
465
- }>;
466
-
467
- if (cursor) {
468
- rows = db
469
- .query(
470
- `SELECT timestamp, session_id, skills_triggered_json, errors_encountered, total_tool_calls
471
- FROM session_telemetry
472
- WHERE (timestamp < ? OR (timestamp = ? AND session_id < ?))
473
- ORDER BY timestamp DESC, session_id DESC
474
- LIMIT ?`,
475
- )
476
- .all(cursor.timestamp, cursor.timestamp, String(cursor.id), fetchLimit) as typeof rows;
477
- } else {
478
- rows = db
479
- .query(
480
- `SELECT timestamp, session_id, skills_triggered_json, errors_encountered, total_tool_calls
481
- FROM session_telemetry
482
- ORDER BY timestamp DESC, session_id DESC
483
- LIMIT ?`,
484
- )
485
- .all(fetchLimit) as typeof rows;
486
- }
487
-
488
- const hasMore = rows.length > limit;
489
- const pageRows = hasMore ? rows.slice(0, limit) : rows;
490
-
491
- const items: TelemetryRecord[] = pageRows.map((row) => ({
492
- timestamp: row.timestamp,
493
- session_id: row.session_id,
494
- skills_triggered: safeParseJsonArray<string>(row.skills_triggered_json),
495
- errors_encountered: row.errors_encountered,
496
- total_tool_calls: row.total_tool_calls,
497
- }));
498
-
499
- const lastItem = pageRows[pageRows.length - 1];
500
- const next_cursor: PaginationCursor | null =
501
- hasMore && lastItem ? { timestamp: lastItem.timestamp, id: lastItem.session_id } : null;
502
-
503
- return { items, next_cursor, has_more: hasMore };
504
- }
505
-
506
- function paginateSkillInvocations(
507
- db: Database,
508
- limit: number,
509
- cursor: PaginationCursor | null,
510
- ): PaginatedResult<SkillUsageRecord> {
511
- const fetchLimit = limit + 1;
512
-
513
- let rows: Array<{
514
- occurred_at: string;
515
- session_id: string;
516
- skill_name: string;
517
- skill_path: string;
518
- query: string;
519
- triggered: number;
520
- source: string | null;
521
- skill_invocation_id: string;
522
- }>;
523
-
524
- if (cursor) {
525
- rows = db
526
- .query(
527
- `SELECT occurred_at, session_id, skill_name, skill_path, query, triggered, source, skill_invocation_id
528
- FROM skill_invocations
529
- WHERE (occurred_at < ? OR (occurred_at = ? AND skill_invocation_id < ?))
530
- ORDER BY occurred_at DESC, skill_invocation_id DESC
531
- LIMIT ?`,
532
- )
533
- .all(cursor.timestamp, cursor.timestamp, String(cursor.id), fetchLimit) as typeof rows;
534
- } else {
535
- rows = db
536
- .query(
537
- `SELECT occurred_at, session_id, skill_name, skill_path, query, triggered, source, skill_invocation_id
538
- FROM skill_invocations
539
- ORDER BY occurred_at DESC, skill_invocation_id DESC
540
- LIMIT ?`,
541
- )
542
- .all(fetchLimit) as typeof rows;
543
- }
544
-
545
- const hasMore = rows.length > limit;
546
- const pageRows = hasMore ? rows.slice(0, limit) : rows;
547
-
548
- const items: SkillUsageRecord[] = pageRows.map((row) => ({
549
- timestamp: row.occurred_at,
550
- session_id: row.session_id,
551
- skill_name: row.skill_name,
552
- skill_path: row.skill_path,
553
- query: row.query,
554
- triggered: row.triggered === 1,
555
- source: row.source,
556
- }));
557
-
558
- const lastRow = pageRows[pageRows.length - 1];
559
- const next_cursor: PaginationCursor | null =
560
- hasMore && lastRow ? { timestamp: lastRow.occurred_at, id: lastRow.skill_invocation_id } : null;
561
-
562
- return { items, next_cursor, has_more: hasMore };
563
- }
564
-
565
- function paginateSkillReportInvocations(
566
- db: Database,
567
- skillName: string,
568
- limit: number,
569
- cursor: PaginationCursor | null,
570
- ): PaginatedResult<{
571
- timestamp: string;
572
- session_id: string;
573
- query: string;
574
- triggered: boolean;
575
- source: string | null;
576
- }> {
577
- const fetchLimit = limit + 1;
578
-
579
- let rows: Array<{
580
- occurred_at: string;
581
- session_id: string;
582
- query: string;
583
- triggered: number;
584
- source: string | null;
585
- skill_invocation_id: string;
586
- }>;
587
-
588
- if (cursor) {
589
- rows = db
590
- .query(
591
- `SELECT si.occurred_at, si.session_id, COALESCE(si.query, p.prompt_text) as query,
592
- si.triggered, si.source, si.skill_invocation_id
593
- FROM skill_invocations si
594
- LEFT JOIN prompts p ON si.matched_prompt_id = p.prompt_id
595
- WHERE si.skill_name = ?
596
- AND (si.occurred_at < ? OR (si.occurred_at = ? AND si.skill_invocation_id < ?))
597
- ORDER BY si.occurred_at DESC, si.skill_invocation_id DESC
598
- LIMIT ?`,
599
- )
600
- .all(
601
- skillName,
602
- cursor.timestamp,
603
- cursor.timestamp,
604
- String(cursor.id),
605
- fetchLimit,
606
- ) as typeof rows;
607
- } else {
608
- rows = db
609
- .query(
610
- `SELECT si.occurred_at, si.session_id, COALESCE(si.query, p.prompt_text) as query,
611
- si.triggered, si.source, si.skill_invocation_id
612
- FROM skill_invocations si
613
- LEFT JOIN prompts p ON si.matched_prompt_id = p.prompt_id
614
- WHERE si.skill_name = ?
615
- ORDER BY si.occurred_at DESC, si.skill_invocation_id DESC
616
- LIMIT ?`,
617
- )
618
- .all(skillName, fetchLimit) as typeof rows;
619
- }
620
-
621
- const hasMore = rows.length > limit;
622
- const pageRows = hasMore ? rows.slice(0, limit) : rows;
623
-
624
- const items = pageRows.map((row) => ({
625
- timestamp: row.occurred_at,
626
- session_id: row.session_id,
627
- query: row.query ?? "",
628
- triggered: row.triggered === 1,
629
- source: row.source,
630
- }));
631
-
632
- const lastRow = pageRows[pageRows.length - 1];
633
- const next_cursor: PaginationCursor | null =
634
- hasMore && lastRow ? { timestamp: lastRow.occurred_at, id: lastRow.skill_invocation_id } : null;
635
-
636
- return { items, next_cursor, has_more: hasMore };
637
- }
638
-
639
- /**
640
- * Get a summary list of all skills with aggregated stats.
641
- */
642
- export function getSkillsList(db: Database): SkillSummary[] {
643
- const trustedRows = queryTrustedSkillObservationRows(db);
644
- const bySkill = new Map<
645
- string,
646
- Array<{
647
- skill_name: string;
648
- session_id: string;
649
- occurred_at: string | null;
650
- triggered: number;
651
- matched_prompt_id: string | null;
652
- confidence: number | null;
653
- }>
654
- >();
655
-
656
- for (const row of trustedRows) {
657
- const arr = bySkill.get(row.skill_name);
658
- const base = {
659
- skill_name: row.skill_name,
660
- session_id: row.session_id,
661
- occurred_at: row.occurred_at,
662
- triggered: row.triggered,
663
- matched_prompt_id: row.matched_prompt_id,
664
- confidence: row.confidence,
665
- };
666
- if (arr) arr.push(base);
667
- else bySkill.set(row.skill_name, [base]);
668
- }
669
-
670
- // Get set of skill names with evidence
671
- const evidenceSkills = new Set(
672
- (
673
- db.query(`SELECT DISTINCT skill_name FROM evolution_evidence`).all() as Array<{
674
- skill_name: string;
675
- }>
676
- ).map((r) => r.skill_name),
677
- );
678
-
679
- const skillScopeRows = db
680
- .query(
681
- `SELECT
682
- si.skill_name,
683
- COALESCE(
684
- (SELECT s2.skill_scope FROM skill_invocations s2
685
- WHERE s2.skill_name = si.skill_name AND s2.skill_scope IS NOT NULL
686
- ORDER BY s2.occurred_at DESC LIMIT 1),
687
- (SELECT su.skill_scope FROM skill_usage su
688
- WHERE su.skill_name = si.skill_name AND su.skill_scope IS NOT NULL
689
- ORDER BY su.timestamp DESC LIMIT 1)
690
- ) as skill_scope
691
- FROM skill_invocations si
692
- GROUP BY si.skill_name`,
693
- )
694
- .all() as Array<{ skill_name: string; skill_scope: string | null }>;
695
- const scopeBySkill = new Map(skillScopeRows.map((row) => [row.skill_name, row.skill_scope]));
696
-
697
- return [...bySkill.entries()]
698
- .map(([skillName, rows]) => {
699
- const totalChecks = rows.length;
700
- const triggeredCount = rows.filter((row) => row.triggered === 1).length;
701
- const uniqueSessions = new Set(rows.map((row) => row.session_id)).size;
702
- const lastSeen =
703
- rows
704
- .map((row) => row.occurred_at)
705
- .filter((value): value is string => value != null)
706
- .sort((a, b) => b.localeCompare(a))[0] ?? null;
707
- const withConfidence = rows.filter((row) => row.confidence != null);
708
- const routingConfidence =
709
- withConfidence.length > 0
710
- ? withConfidence.reduce((sum, row) => sum + (row.confidence ?? 0), 0) /
711
- withConfidence.length
712
- : null;
713
-
714
- return {
715
- skill_name: skillName,
716
- skill_scope: scopeBySkill.get(skillName) ?? null,
717
- total_checks: totalChecks,
718
- triggered_count: triggeredCount,
719
- pass_rate: totalChecks > 0 ? triggeredCount / totalChecks : 0,
720
- unique_sessions: uniqueSessions,
721
- last_seen: lastSeen,
722
- has_evidence: evidenceSkills.has(skillName),
723
- routing_confidence: routingConfidence,
724
- confidence_coverage: totalChecks > 0 ? withConfidence.length / totalChecks : 0,
725
- };
726
- })
727
- .sort((a, b) => b.total_checks - a.total_checks);
728
- }
729
-
730
- /**
731
- * Build the performance analytics payload from SQLite.
732
- * Powers the GET /api/v2/analytics endpoint.
733
- */
734
- export function getAnalyticsPayload(db: Database): AnalyticsResponse {
735
- const trustedRows = queryTrustedSkillObservationRows(db);
736
- const today = new Date();
737
- const dateKey = (value: string | null): string | null => {
738
- if (!value) return null;
739
- const parsed = new Date(value);
740
- return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString().slice(0, 10);
741
- };
742
- const cutoffDate = (days: number): string => {
743
- const cutoff = new Date(today);
744
- cutoff.setUTCDate(cutoff.getUTCDate() - days);
745
- return cutoff.toISOString().slice(0, 10);
746
- };
747
-
748
- // 1. Pass rate trend — last 90 days, bucketed by day
749
- const passRateTrendByDate = new Map<string, { triggered: number; total: number }>();
750
- for (const row of trustedRows) {
751
- const occurredDate = dateKey(row.occurred_at);
752
- if (!occurredDate || occurredDate < cutoffDate(90)) continue;
753
- const counts = passRateTrendByDate.get(occurredDate) ?? { triggered: 0, total: 0 };
754
- counts.total += 1;
755
- if (row.triggered === 1) counts.triggered += 1;
756
- passRateTrendByDate.set(occurredDate, counts);
757
- }
758
- const passRateTrendRows = [...passRateTrendByDate.entries()]
759
- .map(([date, counts]) => ({
760
- date,
761
- pass_rate: counts.total > 0 ? counts.triggered / counts.total : 0,
762
- total_checks: counts.total,
763
- }))
764
- .sort((a, b) => a.date.localeCompare(b.date));
765
-
766
- const pass_rate_trend = passRateTrendRows.map((row) => ({
767
- date: row.date,
768
- pass_rate: row.pass_rate,
769
- total_checks: row.total_checks,
770
- }));
771
-
772
- // 2. Skill rankings — all skills with at least 1 check, ordered by pass rate
773
- const skillRankingMap = new Map<string, { triggered_count: number; total_checks: number }>();
774
- for (const row of trustedRows) {
775
- const counts = skillRankingMap.get(row.skill_name) ?? { triggered_count: 0, total_checks: 0 };
776
- counts.total_checks += 1;
777
- if (row.triggered === 1) counts.triggered_count += 1;
778
- skillRankingMap.set(row.skill_name, counts);
779
- }
780
- const skillRankingRows = [...skillRankingMap.entries()]
781
- .map(([skill_name, counts]) => ({
782
- skill_name,
783
- pass_rate: counts.total_checks > 0 ? counts.triggered_count / counts.total_checks : 0,
784
- total_checks: counts.total_checks,
785
- triggered_count: counts.triggered_count,
786
- }))
787
- .sort(
788
- (a, b) =>
789
- b.pass_rate - a.pass_rate ||
790
- b.total_checks - a.total_checks ||
791
- a.skill_name.localeCompare(b.skill_name),
792
- );
793
-
794
- const skill_rankings = skillRankingRows.map((row) => ({
795
- skill_name: row.skill_name,
796
- pass_rate: row.pass_rate,
797
- total_checks: row.total_checks,
798
- triggered_count: row.triggered_count,
799
- }));
800
-
801
- // 3. Daily activity — last 84 days (12 weeks) for heatmap
802
- const dailyActivityByDate = new Map<string, number>();
803
- for (const row of trustedRows) {
804
- const occurredDate = dateKey(row.occurred_at);
805
- if (!occurredDate || occurredDate < cutoffDate(84)) continue;
806
- dailyActivityByDate.set(occurredDate, (dailyActivityByDate.get(occurredDate) ?? 0) + 1);
807
- }
808
- const dailyActivityRows = [...dailyActivityByDate.entries()]
809
- .map(([date, checks]) => ({ date, checks }))
810
- .sort((a, b) => a.date.localeCompare(b.date));
811
-
812
- const daily_activity = dailyActivityRows.map((row) => ({
813
- date: row.date,
814
- checks: row.checks,
815
- }));
816
-
817
- // 4. Evolution impact — before/after pass rates for deployed evolutions
818
- const deployedRows = db
819
- .query(
820
- `SELECT ea.skill_name, ea.proposal_id, ea.timestamp as deployed_at
821
- FROM evolution_audit ea
822
- WHERE ea.action = 'deployed' AND ea.skill_name IS NOT NULL
823
- ORDER BY ea.timestamp DESC`,
824
- )
825
- .all() as Array<{ skill_name: string; proposal_id: string; deployed_at: string }>;
826
-
827
- const evolution_impact: AnalyticsResponse["evolution_impact"] = [];
828
- for (const deploy of deployedRows) {
829
- const beforeRows = trustedRows.filter(
830
- (row) => row.skill_name === deploy.skill_name && (row.occurred_at ?? "") < deploy.deployed_at,
831
- );
832
- const afterRows = trustedRows.filter(
833
- (row) =>
834
- row.skill_name === deploy.skill_name && (row.occurred_at ?? "") >= deploy.deployed_at,
835
- );
836
-
837
- evolution_impact.push({
838
- skill_name: deploy.skill_name,
839
- proposal_id: deploy.proposal_id,
840
- deployed_at: deploy.deployed_at,
841
- pass_rate_before:
842
- beforeRows.length > 0
843
- ? beforeRows.filter((row) => row.triggered === 1).length / beforeRows.length
844
- : 0,
845
- pass_rate_after:
846
- afterRows.length > 0
847
- ? afterRows.filter((row) => row.triggered === 1).length / afterRows.length
848
- : 0,
849
- });
850
- }
851
-
852
- // 5. Summary aggregates
853
- const totalEvolutionsRow = db
854
- .query(`SELECT COUNT(*) as c FROM evolution_audit WHERE action = 'deployed'`)
855
- .get() as { c: number } | null;
856
-
857
- const checks30dRows = trustedRows.filter((row) => {
858
- const occurredDate = dateKey(row.occurred_at);
859
- return occurredDate != null && occurredDate >= cutoffDate(30);
860
- });
861
- const activeSkills30d = new Set(checks30dRows.map((row) => row.skill_name));
862
-
863
- // Average improvement across all deployed evolutions
864
- let avgImprovement = 0;
865
- if (evolution_impact.length > 0) {
866
- const totalImprovement = evolution_impact.reduce(
867
- (sum, e) => sum + (e.pass_rate_after - e.pass_rate_before),
868
- 0,
869
- );
870
- avgImprovement = totalImprovement / evolution_impact.length;
871
- }
872
-
873
- const summary: AnalyticsResponse["summary"] = {
874
- total_evolutions: totalEvolutionsRow?.c ?? 0,
875
- avg_improvement: avgImprovement,
876
- total_checks_30d: checks30dRows.length,
877
- active_skills: activeSkills30d.size,
878
- };
879
-
880
- return {
881
- pass_rate_trend,
882
- skill_rankings,
883
- daily_activity,
884
- evolution_impact,
885
- summary,
886
- };
887
- }
888
-
889
- /**
890
- * Get pending proposals (created/validated with no terminal action).
891
- * Optionally filtered by skill_name.
892
- */
893
- export function getPendingProposals(db: Database, skillName?: string): PendingProposal[] {
894
- const whereClause = skillName ? "WHERE ea.skill_name = ? AND" : "WHERE";
895
- const params = skillName ? [skillName] : [];
896
- return db
897
- .query(
898
- `WITH latest AS (
899
- SELECT ea.proposal_id, ea.action, ea.timestamp, ea.details, ea.skill_name,
900
- ROW_NUMBER() OVER (PARTITION BY ea.proposal_id ORDER BY ea.timestamp DESC, ea.id DESC) AS rn
901
- FROM evolution_audit ea
902
- LEFT JOIN evolution_audit ea2
903
- ON ea2.proposal_id = ea.proposal_id
904
- AND ea2.action IN ('deployed', 'rejected', 'rolled_back')
905
- ${whereClause} ea.action IN ('created', 'validated')
906
- AND ea2.id IS NULL
907
- )
908
- SELECT proposal_id, action, timestamp, details, skill_name
909
- FROM latest
910
- WHERE rn = 1
911
- ORDER BY timestamp DESC`,
912
- )
913
- .all(...params) as PendingProposal[];
914
- }
915
-
916
- /**
917
- * Get recent orchestrate run reports (most recent first).
918
- */
919
- export function getOrchestrateRuns(db: Database, limit = 20): OrchestrateRunReport[] {
920
- const rows = db
921
- .query(
922
- `SELECT run_id, timestamp, elapsed_ms, dry_run, approval_mode,
923
- total_skills, evaluated, evolved, deployed, watched, skipped,
924
- skill_actions_json
925
- FROM orchestrate_runs
926
- ORDER BY timestamp DESC
927
- LIMIT ?`,
928
- )
929
- .all(limit) as Array<{
930
- run_id: string;
931
- timestamp: string;
932
- elapsed_ms: number;
933
- dry_run: number;
934
- approval_mode: string;
935
- total_skills: number;
936
- evaluated: number;
937
- evolved: number;
938
- deployed: number;
939
- watched: number;
940
- skipped: number;
941
- skill_actions_json: string;
942
- }>;
943
-
944
- return rows.map((r) => ({
945
- run_id: r.run_id,
946
- timestamp: r.timestamp,
947
- elapsed_ms: r.elapsed_ms,
948
- dry_run: r.dry_run === 1,
949
- approval_mode: r.approval_mode as "auto" | "review",
950
- total_skills: r.total_skills,
951
- evaluated: r.evaluated,
952
- evolved: r.evolved,
953
- deployed: r.deployed,
954
- watched: r.watched,
955
- skipped: r.skipped,
956
- skill_actions: safeParseJsonArray(r.skill_actions_json),
957
- }));
958
- }
959
-
960
- /**
961
- * Count sessions that have queries recorded but no session_telemetry yet
962
- * (i.e., the session is still in progress).
963
- */
964
- export function getActiveSessionCount(db: Database): number {
965
- const row = db
966
- .query(
967
- `SELECT COUNT(DISTINCT q.session_id) as count
968
- FROM queries q
969
- WHERE NOT EXISTS (
970
- SELECT 1 FROM session_telemetry st WHERE st.session_id = q.session_id
971
- )`,
972
- )
973
- .get() as { count: number };
974
- return row.count;
975
- }
976
-
977
- /**
978
- * Get the most recent skill invocations with a flag indicating whether the
979
- * session is still in progress (no session_telemetry row yet).
980
- */
981
- export function getRecentActivity(db: Database, limit = 20): RecentActivityItem[] {
982
- // Step 1: Fetch recent invocations without JOIN (avoids materializing full JOIN before LIMIT)
983
- const rows = db
984
- .query(
985
- `SELECT occurred_at, session_id, skill_name, query, triggered
986
- FROM skill_invocations
987
- ORDER BY occurred_at DESC
988
- LIMIT ?`,
989
- )
990
- .all(limit) as Array<{
991
- occurred_at: string;
992
- session_id: string;
993
- skill_name: string;
994
- query: string;
995
- triggered: number;
996
- }>;
997
-
998
- if (rows.length === 0) return [];
999
-
1000
- // Step 2: Batch lookup which sessions have completed (have a telemetry row)
1001
- const uniqueSessionIds = [...new Set(rows.map((r) => r.session_id))];
1002
- const placeholders = uniqueSessionIds.map(() => "?").join(",");
1003
- const completedRows = db
1004
- .query(
1005
- `SELECT DISTINCT session_id FROM session_telemetry WHERE session_id IN (${placeholders})`,
1006
- )
1007
- .all(...uniqueSessionIds) as Array<{ session_id: string }>;
1008
- const completedSessions = new Set(completedRows.map((r) => r.session_id));
1009
-
1010
- return rows.map((row) => ({
1011
- timestamp: row.occurred_at,
1012
- session_id: row.session_id,
1013
- skill_name: row.skill_name,
1014
- query: row.query ?? "",
1015
- triggered: row.triggered === 1,
1016
- is_live: !completedSessions.has(row.session_id),
1017
- }));
1018
- }
1019
-
1020
- // -- Generic read queries (Phase 3: replace readJsonl calls) ------------------
1021
-
1022
- /**
1023
- * Read all session telemetry records from SQLite.
1024
- * Replaces: readJsonl<SessionTelemetryRecord>(TELEMETRY_LOG)
1025
- */
1026
- export function querySessionTelemetry(db: Database): Array<{
1027
- timestamp: string;
1028
- session_id: string;
1029
- cwd: string;
1030
- transcript_path: string;
1031
- tool_calls: Record<string, number>;
1032
- total_tool_calls: number;
1033
- bash_commands: string[];
1034
- skills_triggered: string[];
1035
- skills_invoked?: string[];
1036
- assistant_turns: number;
1037
- errors_encountered: number;
1038
- transcript_chars: number;
1039
- last_user_query: string;
1040
- source?: string;
1041
- input_tokens?: number;
1042
- output_tokens?: number;
1043
- }> {
1044
- const rows = db.query(`SELECT * FROM session_telemetry ORDER BY timestamp DESC`).all() as Array<
1045
- Record<string, unknown>
1046
- >;
1047
- return rows.map((r) => ({
1048
- timestamp: r.timestamp as string,
1049
- session_id: r.session_id as string,
1050
- cwd: r.cwd as string,
1051
- transcript_path: r.transcript_path as string,
1052
- tool_calls: (safeParseJson(r.tool_calls_json as string) as Record<string, number>) ?? {},
1053
- total_tool_calls: r.total_tool_calls as number,
1054
- bash_commands: safeParseJsonArray<string>(r.bash_commands_json as string),
1055
- skills_triggered: safeParseJsonArray<string>(r.skills_triggered_json as string),
1056
- skills_invoked: r.skills_invoked_json
1057
- ? safeParseJsonArray<string>(r.skills_invoked_json as string)
1058
- : undefined,
1059
- assistant_turns: r.assistant_turns as number,
1060
- errors_encountered: r.errors_encountered as number,
1061
- transcript_chars: (r.transcript_chars as number) ?? 0,
1062
- last_user_query: (r.last_user_query as string) ?? "",
1063
- source: r.source as string | undefined,
1064
- input_tokens: r.input_tokens as number | undefined,
1065
- output_tokens: r.output_tokens as number | undefined,
1066
- }));
1067
- }
1068
-
1069
- /**
1070
- * Read all skill invocation records from SQLite.
1071
- * Replaces: readEffectiveSkillUsageRecords()
1072
- */
1073
- export function querySkillRecords(db: Database): Array<{
1074
- timestamp: string;
1075
- session_id: string;
1076
- skill_name: string;
1077
- skill_path: string;
1078
- skill_scope?: string;
1079
- query: string;
1080
- triggered: boolean;
1081
- source?: string;
1082
- }> {
1083
- const rows = db
1084
- .query(
1085
- `SELECT occurred_at, session_id, skill_name, skill_path, skill_scope, query, triggered, source
1086
- FROM skill_invocations ORDER BY occurred_at DESC`,
1087
- )
1088
- .all() as Array<Record<string, unknown>>;
1089
- return rows.map((r) => ({
1090
- timestamp: r.occurred_at as string,
1091
- session_id: r.session_id as string,
1092
- skill_name: r.skill_name as string,
1093
- skill_path: r.skill_path as string,
1094
- skill_scope: r.skill_scope as string | undefined,
1095
- query: r.query as string,
1096
- triggered: (r.triggered as number) === 1,
1097
- source: r.source as string | undefined,
1098
- }));
1099
- }
1100
-
1101
- /** @deprecated Use querySkillRecords instead. Kept for backward compatibility. */
1102
- export const querySkillUsageRecords = querySkillRecords;
1103
-
1104
- /**
1105
- * Read all query log records from SQLite.
1106
- * Replaces: readJsonl<QueryLogRecord>(QUERY_LOG)
1107
- */
1108
- export function queryQueryLog(db: Database): Array<{
1109
- timestamp: string;
1110
- session_id: string;
1111
- query: string;
1112
- source?: string;
1113
- }> {
1114
- return db
1115
- .query(`SELECT timestamp, session_id, query, source FROM queries ORDER BY timestamp DESC`)
1116
- .all() as Array<{ timestamp: string; session_id: string; query: string; source?: string }>;
1117
- }
1118
-
1119
- /**
1120
- * Read all evolution audit entries from SQLite.
1121
- * Replaces: readJsonl<EvolutionAuditEntry>(EVOLUTION_AUDIT_LOG)
1122
- */
1123
- export function queryEvolutionAudit(
1124
- db: Database,
1125
- skillName?: string,
1126
- ): Array<{
1127
- timestamp: string;
1128
- proposal_id: string;
1129
- skill_name?: string;
1130
- action: string;
1131
- details: string;
1132
- eval_snapshot?: Record<string, unknown>;
1133
- validation_mode?: string;
1134
- validation_agent?: string;
1135
- validation_fixture_id?: string;
1136
- validation_evidence_ref?: string;
1137
- }> {
1138
- const sql = skillName
1139
- ? `SELECT * FROM evolution_audit
1140
- WHERE skill_name = ?
1141
- OR (skill_name IS NULL AND proposal_id LIKE 'evo-' || ? || '-%')
1142
- ORDER BY timestamp DESC`
1143
- : `SELECT * FROM evolution_audit ORDER BY timestamp DESC`;
1144
- const rows = (skillName ? db.query(sql).all(skillName, skillName) : db.query(sql).all()) as Array<
1145
- Record<string, unknown>
1146
- >;
1147
- return rows.map((r) => ({
1148
- timestamp: r.timestamp as string,
1149
- proposal_id: r.proposal_id as string,
1150
- skill_name: typeof r.skill_name === "string" ? r.skill_name : undefined,
1151
- action: r.action as string,
1152
- details: r.details as string,
1153
- eval_snapshot: r.eval_snapshot_json
1154
- ? (safeParseJson(r.eval_snapshot_json as string) as Record<string, unknown>)
1155
- : undefined,
1156
- validation_mode: typeof r.validation_mode === "string" ? r.validation_mode : undefined,
1157
- validation_agent: typeof r.validation_agent === "string" ? r.validation_agent : undefined,
1158
- validation_fixture_id:
1159
- typeof r.validation_fixture_id === "string" ? r.validation_fixture_id : undefined,
1160
- validation_evidence_ref:
1161
- typeof r.validation_evidence_ref === "string" ? r.validation_evidence_ref : undefined,
1162
- }));
1163
- }
1164
-
1165
- /**
1166
- * Read all evolution evidence entries from SQLite.
1167
- * Replaces: readEvidenceTrail() / readJsonl<EvolutionEvidenceEntry>(EVOLUTION_EVIDENCE_LOG)
1168
- */
1169
- export function queryEvolutionEvidence(
1170
- db: Database,
1171
- skillName?: string,
1172
- ): Array<{
1173
- timestamp: string;
1174
- proposal_id: string;
1175
- skill_name: string;
1176
- skill_path: string;
1177
- target: string;
1178
- stage: string;
1179
- rationale?: string;
1180
- confidence?: number;
1181
- details?: string;
1182
- original_text?: string;
1183
- proposed_text?: string;
1184
- eval_set?: Record<string, unknown>[];
1185
- validation?: Record<string, unknown>;
1186
- }> {
1187
- const sql = skillName
1188
- ? `SELECT * FROM evolution_evidence WHERE skill_name = ? ORDER BY timestamp DESC`
1189
- : `SELECT * FROM evolution_evidence ORDER BY timestamp DESC`;
1190
- const rows = (skillName ? db.query(sql).all(skillName) : db.query(sql).all()) as Array<
1191
- Record<string, unknown>
1192
- >;
1193
- return rows.map((r) => ({
1194
- timestamp: r.timestamp as string,
1195
- proposal_id: r.proposal_id as string,
1196
- skill_name: r.skill_name as string,
1197
- skill_path: r.skill_path as string,
1198
- target: r.target as string,
1199
- stage: r.stage as string,
1200
- rationale: r.rationale as string | undefined,
1201
- confidence: r.confidence as number | undefined,
1202
- details: r.details as string | undefined,
1203
- original_text: r.original_text as string | undefined,
1204
- proposed_text: r.proposed_text as string | undefined,
1205
- eval_set: r.eval_set_json
1206
- ? safeParseJsonArray<Record<string, unknown>>(r.eval_set_json as string)
1207
- : undefined,
1208
- validation: r.validation_json
1209
- ? (safeParseJson(r.validation_json as string) as Record<string, unknown>)
1210
- : undefined,
1211
- }));
1212
- }
1213
-
1214
- /**
1215
- * Read improvement signals from SQLite.
1216
- * Replaces: readJsonl<ImprovementSignalRecord>(SIGNAL_LOG)
1217
- */
1218
- export function queryImprovementSignals(
1219
- db: Database,
1220
- consumedOnly?: boolean,
1221
- ): Array<{
1222
- timestamp: string;
1223
- session_id: string;
1224
- query: string;
1225
- signal_type: string;
1226
- mentioned_skill?: string;
1227
- consumed: boolean;
1228
- consumed_at?: string;
1229
- consumed_by_run?: string;
1230
- }> {
1231
- const where =
1232
- consumedOnly === undefined ? "" : consumedOnly ? " WHERE consumed = 1" : " WHERE consumed = 0";
1233
- const rows = db
1234
- .query(`SELECT * FROM improvement_signals${where} ORDER BY timestamp DESC`)
1235
- .all() as Array<Record<string, unknown>>;
1236
- return rows.map((r) => ({
1237
- timestamp: r.timestamp as string,
1238
- session_id: r.session_id as string,
1239
- query: r.query as string,
1240
- signal_type: r.signal_type as string,
1241
- mentioned_skill: r.mentioned_skill as string | undefined,
1242
- consumed: (r.consumed as number) === 1,
1243
- consumed_at: r.consumed_at as string | undefined,
1244
- consumed_by_run: r.consumed_by_run as string | undefined,
1245
- }));
1246
- }
1247
-
1248
- // -- Grading results query ----------------------------------------------------
1249
-
1250
- /**
1251
- * Read grading results from SQLite for upload staging.
1252
- */
1253
- export function queryGradingResults(db: Database): Array<{
1254
- grading_id: string;
1255
- session_id: string;
1256
- skill_name: string;
1257
- transcript_path: string | null;
1258
- graded_at: string;
1259
- pass_rate: number | null;
1260
- mean_score: number | null;
1261
- score_std_dev: number | null;
1262
- passed_count: number | null;
1263
- failed_count: number | null;
1264
- total_count: number | null;
1265
- expectations_json: string | null;
1266
- claims_json: string | null;
1267
- eval_feedback_json: string | null;
1268
- failure_feedback_json: string | null;
1269
- execution_metrics_json: string | null;
1270
- }> {
1271
- return db
1272
- .query(
1273
- `SELECT grading_id, session_id, skill_name, transcript_path, graded_at,
1274
- pass_rate, mean_score, score_std_dev, passed_count, failed_count, total_count,
1275
- expectations_json, claims_json, eval_feedback_json, failure_feedback_json,
1276
- execution_metrics_json
1277
- FROM grading_results
1278
- ORDER BY graded_at DESC`,
1279
- )
1280
- .all() as Array<{
1281
- grading_id: string;
1282
- session_id: string;
1283
- skill_name: string;
1284
- transcript_path: string | null;
1285
- graded_at: string;
1286
- pass_rate: number | null;
1287
- mean_score: number | null;
1288
- score_std_dev: number | null;
1289
- passed_count: number | null;
1290
- failed_count: number | null;
1291
- total_count: number | null;
1292
- expectations_json: string | null;
1293
- claims_json: string | null;
1294
- eval_feedback_json: string | null;
1295
- failure_feedback_json: string | null;
1296
- execution_metrics_json: string | null;
1297
- }>;
1298
- }
1299
-
1300
- export function getCreatorContributionStagingCounts(db: Database): Array<{
1301
- skill_name: string;
1302
- pending_count: number;
1303
- }> {
1304
- return db
1305
- .query(
1306
- `SELECT skill_name, COUNT(*) AS pending_count
1307
- FROM creator_contribution_staging
1308
- WHERE status = 'pending'
1309
- GROUP BY skill_name
1310
- ORDER BY skill_name`,
1311
- )
1312
- .all() as Array<{
1313
- skill_name: string;
1314
- pending_count: number;
1315
- }>;
1316
- }
1317
-
1318
- export interface CreatorContributionRelayStats {
1319
- pending: number;
1320
- sending: number;
1321
- sent: number;
1322
- failed: number;
1323
- }
1324
-
1325
- export interface CreatorContributionStagingRow {
1326
- id: number;
1327
- dedupe_key: string;
1328
- skill_name: string;
1329
- creator_id: string;
1330
- payload_json: string;
1331
- status: string;
1332
- staged_at: string;
1333
- updated_at: string;
1334
- last_error: string | null;
1335
- }
1336
-
1337
- export function getCreatorContributionRelayStats(db: Database): CreatorContributionRelayStats {
1338
- const row = db
1339
- .query(
1340
- `SELECT
1341
- COALESCE(SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END), 0) AS pending,
1342
- COALESCE(SUM(CASE WHEN status = 'sending' THEN 1 ELSE 0 END), 0) AS sending,
1343
- COALESCE(SUM(CASE WHEN status = 'sent' THEN 1 ELSE 0 END), 0) AS sent,
1344
- COALESCE(SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END), 0) AS failed
1345
- FROM creator_contribution_staging`,
1346
- )
1347
- .get() as CreatorContributionRelayStats | null;
1348
- return row ?? { pending: 0, sending: 0, sent: 0, failed: 0 };
1349
- }
1350
-
1351
- export function getPendingCreatorContributionRows(
1352
- db: Database,
1353
- limit = 50,
1354
- ): CreatorContributionStagingRow[] {
1355
- return db
1356
- .query(
1357
- `SELECT id, dedupe_key, skill_name, creator_id, payload_json, status, staged_at, updated_at, last_error
1358
- FROM creator_contribution_staging
1359
- WHERE status = 'pending'
1360
- ORDER BY id ASC
1361
- LIMIT ?`,
1362
- )
1363
- .all(limit) as CreatorContributionStagingRow[];
1364
- }
1365
-
1366
- // -- Canonical record staging query -------------------------------------------
1367
-
1368
- /**
1369
- * Query canonical records from SQLite tables for upload staging.
1370
- *
1371
- * Reads from sessions, prompts, skill_invocations, and execution_facts tables,
1372
- * shaping each row into a CanonicalRecord-compatible object with record_kind.
1373
- *
1374
- * Returns all records; dedup is handled by INSERT OR IGNORE in the staging table.
1375
- */
1376
- export function queryCanonicalRecordsForStaging(db: Database): Record<string, unknown>[] {
1377
- const records: Record<string, unknown>[] = [];
1378
-
1379
- // Sessions
1380
- const sessions = db
1381
- .query(
1382
- `SELECT session_id, started_at, ended_at, platform, model, completion_status,
1383
- source_session_kind, agent_cli, workspace_path, repo_remote, branch,
1384
- schema_version, normalized_at, normalizer_version, capture_mode, raw_source_ref
1385
- FROM sessions ORDER BY normalized_at`,
1386
- )
1387
- .all() as Array<Record<string, unknown>>;
1388
- const sessionById = new Map(sessions.map((s) => [s.session_id as string, s]));
1389
- for (const s of sessions) {
1390
- records.push({
1391
- record_kind: "session",
1392
- schema_version: s.schema_version ?? undefined,
1393
- normalizer_version: s.normalizer_version ?? undefined,
1394
- normalized_at: s.normalized_at ?? undefined,
1395
- platform: s.platform ?? undefined,
1396
- capture_mode: s.capture_mode ?? undefined,
1397
- raw_source_ref: safeParseJson(s.raw_source_ref as string | null) ?? undefined,
1398
- source_session_kind: s.source_session_kind ?? undefined,
1399
- session_id: s.session_id,
1400
- started_at: s.started_at ?? undefined,
1401
- ended_at: s.ended_at ?? undefined,
1402
- model: s.model ?? undefined,
1403
- completion_status: s.completion_status ?? undefined,
1404
- agent_cli: s.agent_cli ?? undefined,
1405
- workspace_path: s.workspace_path ?? undefined,
1406
- repo_remote: s.repo_remote ?? undefined,
1407
- branch: s.branch ?? undefined,
1408
- });
1409
- }
1410
-
1411
- // Prompts
1412
- const prompts = db
1413
- .query(
1414
- `SELECT prompt_id, session_id, occurred_at, prompt_kind, is_actionable, prompt_index, prompt_text,
1415
- schema_version, platform, normalized_at, normalizer_version, capture_mode, raw_source_ref
1416
- FROM prompts ORDER BY occurred_at`,
1417
- )
1418
- .all() as Array<Record<string, unknown>>;
1419
- for (const p of prompts) {
1420
- // Fall back to session-level envelope fields if prompt doesn't have its own
1421
- const sessionEnvelope = sessionById.get(p.session_id as string);
1422
- records.push({
1423
- record_kind: "prompt",
1424
- schema_version: p.schema_version ?? sessionEnvelope?.schema_version ?? undefined,
1425
- normalizer_version: p.normalizer_version ?? sessionEnvelope?.normalizer_version ?? undefined,
1426
- normalized_at: p.normalized_at ?? sessionEnvelope?.normalized_at ?? undefined,
1427
- platform: p.platform ?? sessionEnvelope?.platform ?? undefined,
1428
- capture_mode: p.capture_mode ?? sessionEnvelope?.capture_mode ?? undefined,
1429
- raw_source_ref:
1430
- safeParseJson(p.raw_source_ref as string | null) ??
1431
- safeParseJson(sessionEnvelope?.raw_source_ref as string | null) ??
1432
- undefined,
1433
- source_session_kind: sessionEnvelope?.source_session_kind ?? undefined,
1434
- session_id: p.session_id,
1435
- prompt_id: p.prompt_id,
1436
- occurred_at: p.occurred_at,
1437
- prompt_text: p.prompt_text,
1438
- prompt_kind: p.prompt_kind,
1439
- is_actionable: (p.is_actionable as number) === 1,
1440
- prompt_index: p.prompt_index ?? undefined,
1441
- });
1442
- }
1443
-
1444
- // Skill invocations
1445
- const invocations = db
1446
- .query(
1447
- `SELECT skill_invocation_id, session_id, occurred_at, skill_name, skill_path, invocation_mode,
1448
- triggered, confidence, tool_name, matched_prompt_id, agent_type,
1449
- schema_version, platform, normalized_at, normalizer_version, capture_mode, raw_source_ref
1450
- FROM skill_invocations ORDER BY occurred_at`,
1451
- )
1452
- .all() as Array<Record<string, unknown>>;
1453
- for (const si of invocations) {
1454
- const sessionEnvelope = sessionById.get(si.session_id as string);
1455
- records.push({
1456
- record_kind: "skill_invocation",
1457
- schema_version: si.schema_version ?? sessionEnvelope?.schema_version ?? undefined,
1458
- normalizer_version: si.normalizer_version ?? sessionEnvelope?.normalizer_version ?? undefined,
1459
- normalized_at: si.normalized_at ?? sessionEnvelope?.normalized_at ?? undefined,
1460
- platform: si.platform ?? sessionEnvelope?.platform ?? undefined,
1461
- capture_mode: si.capture_mode ?? sessionEnvelope?.capture_mode ?? undefined,
1462
- raw_source_ref:
1463
- safeParseJson(si.raw_source_ref as string | null) ??
1464
- safeParseJson(sessionEnvelope?.raw_source_ref as string | null) ??
1465
- undefined,
1466
- source_session_kind: sessionEnvelope?.source_session_kind ?? undefined,
1467
- session_id: si.session_id,
1468
- skill_invocation_id: si.skill_invocation_id,
1469
- occurred_at: si.occurred_at,
1470
- skill_name: si.skill_name,
1471
- skill_path: si.skill_path ?? undefined,
1472
- invocation_mode: si.invocation_mode,
1473
- triggered: (si.triggered as number) === 1,
1474
- confidence: si.confidence,
1475
- tool_name: si.tool_name ?? undefined,
1476
- matched_prompt_id: si.matched_prompt_id ?? undefined,
1477
- agent_type: si.agent_type ?? undefined,
1478
- });
1479
- }
1480
-
1481
- // Execution facts
1482
- const facts = db
1483
- .query(
1484
- `SELECT id AS execution_fact_id, session_id, occurred_at, prompt_id, tool_calls_json, total_tool_calls,
1485
- assistant_turns, errors_encountered, input_tokens, output_tokens,
1486
- duration_ms, completion_status,
1487
- schema_version, platform, normalized_at, normalizer_version, capture_mode, raw_source_ref
1488
- FROM execution_facts ORDER BY occurred_at`,
1489
- )
1490
- .all() as Array<Record<string, unknown>>;
1491
- for (const ef of facts) {
1492
- const sessionEnvelope = sessionById.get(ef.session_id as string);
1493
- records.push({
1494
- record_kind: "execution_fact",
1495
- schema_version: ef.schema_version ?? sessionEnvelope?.schema_version ?? undefined,
1496
- normalizer_version: ef.normalizer_version ?? sessionEnvelope?.normalizer_version ?? undefined,
1497
- normalized_at: ef.normalized_at ?? sessionEnvelope?.normalized_at ?? undefined,
1498
- platform: ef.platform ?? sessionEnvelope?.platform ?? undefined,
1499
- capture_mode: ef.capture_mode ?? sessionEnvelope?.capture_mode ?? undefined,
1500
- raw_source_ref:
1501
- safeParseJson(ef.raw_source_ref as string | null) ??
1502
- safeParseJson(sessionEnvelope?.raw_source_ref as string | null) ??
1503
- undefined,
1504
- source_session_kind: sessionEnvelope?.source_session_kind ?? undefined,
1505
- session_id: ef.session_id,
1506
- execution_fact_id: String(ef.execution_fact_id),
1507
- occurred_at: ef.occurred_at,
1508
- prompt_id: ef.prompt_id ?? undefined,
1509
- tool_calls_json: safeParseJson(ef.tool_calls_json as string | null) ?? {},
1510
- total_tool_calls: ef.total_tool_calls,
1511
- assistant_turns: ef.assistant_turns,
1512
- errors_encountered: ef.errors_encountered,
1513
- input_tokens: ef.input_tokens ?? undefined,
1514
- output_tokens: ef.output_tokens ?? undefined,
1515
- duration_ms: ef.duration_ms ?? undefined,
1516
- completion_status: ef.completion_status ?? undefined,
1517
- });
1518
- }
1519
-
1520
- return records;
1521
- }
1522
-
1523
- // -- Alpha upload query helpers -----------------------------------------------
1524
-
1525
- /**
1526
- * Get the most recent failed queue item's error and timestamp.
1527
- * Returns null if no failed items exist.
1528
- */
1529
- export function getLastUploadError(
1530
- db: Database,
1531
- ): { last_error: string | null; updated_at: string } | null {
1532
- try {
1533
- const row = db
1534
- .query(
1535
- `SELECT last_error, updated_at
1536
- FROM upload_queue
1537
- WHERE status = 'failed'
1538
- ORDER BY updated_at DESC
1539
- LIMIT 1`,
1540
- )
1541
- .get() as { last_error: string | null; updated_at: string } | null;
1542
- return row ?? null;
1543
- } catch {
1544
- return null;
1545
- }
1546
- }
1547
-
1548
- /**
1549
- * Get the most recent sent queue item's timestamp.
1550
- * Returns null if no sent items exist.
1551
- */
1552
- export function getLastUploadSuccess(db: Database): { updated_at: string } | null {
1553
- try {
1554
- const row = db
1555
- .query(
1556
- `SELECT updated_at
1557
- FROM upload_queue
1558
- WHERE status = 'sent'
1559
- ORDER BY updated_at DESC
1560
- LIMIT 1`,
1561
- )
1562
- .get() as { updated_at: string } | null;
1563
- return row ?? null;
1564
- } catch {
1565
- return null;
1566
- }
1567
- }
1568
-
1569
- /**
1570
- * Get the age in seconds of the oldest pending queue item.
1571
- * Returns null if no pending items exist.
1572
- */
1573
- export function getOldestPendingAge(db: Database): number | null {
1574
- try {
1575
- const row = db
1576
- .query(
1577
- `SELECT created_at
1578
- FROM upload_queue
1579
- WHERE status = 'pending'
1580
- ORDER BY created_at ASC
1581
- LIMIT 1`,
1582
- )
1583
- .get() as { created_at: string } | null;
1584
- if (!row) return null;
1585
- const ageMs = Date.now() - new Date(row.created_at).getTime();
1586
- return Math.floor(ageMs / 1000);
1587
- } catch {
1588
- return null;
1589
- }
1590
- }
1591
-
1592
- // -- Execution metrics & commit tracking queries ------------------------------
1593
-
1594
- /**
1595
- * Aggregate execution_facts enrichment columns for a set of session IDs.
1596
- *
1597
- * Returns file change stats, cost totals, token breakdowns, artifact counts,
1598
- * and session_type distribution across the provided sessions.
1599
- */
1600
- export function getExecutionMetrics(db: Database, sessionIds: string[]): ExecutionMetrics {
1601
- const empty: ExecutionMetrics = {
1602
- avg_files_changed: 0,
1603
- total_lines_added: 0,
1604
- total_lines_removed: 0,
1605
- total_cost_usd: 0,
1606
- avg_cost_usd: 0,
1607
- cached_input_tokens_total: 0,
1608
- reasoning_output_tokens_total: 0,
1609
- artifact_count: 0,
1610
- session_type_distribution: {},
1611
- };
1612
- if (sessionIds.length === 0) return empty;
1613
-
1614
- const placeholders = sessionIds.map(() => "?").join(",");
1615
- const row = db
1616
- .query(
1617
- `SELECT
1618
- COALESCE(AVG(files_changed), 0) AS avg_files_changed,
1619
- COALESCE(SUM(lines_added), 0) AS total_lines_added,
1620
- COALESCE(SUM(lines_removed), 0) AS total_lines_removed,
1621
- COALESCE(SUM(cost_usd), 0) AS total_cost_usd,
1622
- COALESCE(AVG(cost_usd), 0) AS avg_cost_usd,
1623
- COALESCE(SUM(cached_input_tokens), 0) AS cached_input_tokens_total,
1624
- COALESCE(SUM(reasoning_output_tokens), 0) AS reasoning_output_tokens_total,
1625
- COALESCE(SUM(artifact_count), 0) AS artifact_count
1626
- FROM execution_facts
1627
- WHERE session_id IN (${placeholders})`,
1628
- )
1629
- .get(...sessionIds) as {
1630
- avg_files_changed: number;
1631
- total_lines_added: number;
1632
- total_lines_removed: number;
1633
- total_cost_usd: number;
1634
- avg_cost_usd: number;
1635
- cached_input_tokens_total: number;
1636
- reasoning_output_tokens_total: number;
1637
- artifact_count: number;
1638
- } | null;
1639
-
1640
- // Session type distribution
1641
- const typeRows = db
1642
- .query(
1643
- `SELECT session_type, COUNT(*) AS count
1644
- FROM execution_facts
1645
- WHERE session_id IN (${placeholders}) AND session_type IS NOT NULL
1646
- GROUP BY session_type`,
1647
- )
1648
- .all(...sessionIds) as Array<{ session_type: string; count: number }>;
1649
-
1650
- const session_type_distribution: Record<string, number> = {};
1651
- for (const tr of typeRows) {
1652
- session_type_distribution[tr.session_type] = tr.count;
1653
- }
1654
-
1655
- return {
1656
- avg_files_changed: row?.avg_files_changed ?? 0,
1657
- total_lines_added: row?.total_lines_added ?? 0,
1658
- total_lines_removed: row?.total_lines_removed ?? 0,
1659
- total_cost_usd: row?.total_cost_usd ?? 0,
1660
- avg_cost_usd: row?.avg_cost_usd ?? 0,
1661
- cached_input_tokens_total: row?.cached_input_tokens_total ?? 0,
1662
- reasoning_output_tokens_total: row?.reasoning_output_tokens_total ?? 0,
1663
- artifact_count: row?.artifact_count ?? 0,
1664
- session_type_distribution,
1665
- };
1666
- }
1667
-
1668
- /**
1669
- * Get all commits tracked for a given session.
1670
- */
1671
- export function getSessionCommits(db: Database, sessionId: string): CommitRecord[] {
1672
- return db
1673
- .query(
1674
- `SELECT commit_sha, commit_title, branch, repo_remote, timestamp
1675
- FROM commit_tracking
1676
- WHERE session_id = ?
1677
- ORDER BY timestamp DESC`,
1678
- )
1679
- .all(sessionId) as CommitRecord[];
1680
- }
1681
-
1682
- /**
1683
- * Aggregate commit stats for a skill by joining commit_tracking to skill_invocations
1684
- * via shared session_id.
1685
- */
1686
- export function getSkillCommitSummary(db: Database, skillName: string): CommitSummary {
1687
- const empty: CommitSummary = {
1688
- total_commits: 0,
1689
- unique_branches: 0,
1690
- recent_commits: [],
1691
- };
1692
-
1693
- const statsRow = db
1694
- .query(
1695
- `WITH skill_sessions AS (
1696
- SELECT DISTINCT session_id FROM skill_invocations WHERE skill_name = ?
1697
- )
1698
- SELECT
1699
- COUNT(*) AS total_commits,
1700
- COUNT(DISTINCT ct.branch) AS unique_branches
1701
- FROM commit_tracking ct
1702
- WHERE ct.session_id IN (SELECT session_id FROM skill_sessions)`,
1703
- )
1704
- .get(skillName) as { total_commits: number; unique_branches: number } | null;
1705
-
1706
- if (!statsRow || statsRow.total_commits === 0) return empty;
1707
-
1708
- const recentRows = db
1709
- .query(
1710
- `WITH skill_sessions AS (
1711
- SELECT DISTINCT session_id FROM skill_invocations WHERE skill_name = ?
1712
- )
1713
- SELECT ct.commit_sha, ct.commit_title, ct.branch, ct.timestamp
1714
- FROM commit_tracking ct
1715
- WHERE ct.session_id IN (SELECT session_id FROM skill_sessions)
1716
- ORDER BY ct.timestamp DESC
1717
- LIMIT 20`,
1718
- )
1719
- .all(skillName) as Array<{
1720
- commit_sha: string;
1721
- commit_title: string | null;
1722
- branch: string | null;
1723
- timestamp: string;
1724
- }>;
1725
-
1726
- return {
1727
- total_commits: statsRow.total_commits,
1728
- unique_branches: statsRow.unique_branches,
1729
- recent_commits: recentRows.map((r) => ({
1730
- sha: r.commit_sha,
1731
- title: r.commit_title ?? "",
1732
- branch: r.branch ?? "",
1733
- timestamp: r.timestamp,
1734
- })),
1735
- };
1736
- }
1737
-
1738
- // -- Helpers ------------------------------------------------------------------
1739
-
1740
- // -- Autonomy-first dashboard queries -----------------------------------------
1741
-
1742
- export interface SkillTrustSummary {
1743
- skill_name: string;
1744
- total_checks: number;
1745
- triggered_count: number;
1746
- miss_rate: number;
1747
- system_like_count: number;
1748
- system_like_rate: number;
1749
- prompt_link_rate: number;
1750
- latest_action: string | null;
1751
- pass_rate: number;
1752
- last_seen: string | null;
1753
- }
1754
-
1755
- export interface TrustedSkillObservationRow {
1756
- skill_name: string;
1757
- session_id: string;
1758
- occurred_at: string | null;
1759
- triggered: number;
1760
- matched_prompt_id: string | null;
1761
- confidence: number | null;
1762
- invocation_mode: string | null;
1763
- query_text: string;
1764
- }
1765
-
1766
- export function queryTrustedSkillObservationRows(db: Database): TrustedSkillObservationRow[] {
1767
- const SYSTEM_LIKE_PREFIXES = ["<system_instruction>", "<system-instruction>", "<command-name>"];
1768
- const INTERNAL_EVAL_MARKERS = [
1769
- "you are an evaluation assistant",
1770
- "you are a skill description optimizer",
1771
- "would each query trigger this skill",
1772
- "propose an improved description",
1773
- "failure patterns:",
1774
- "output only valid json",
1775
- ];
1776
- const isSystemLike = (text: string | null | undefined): boolean => {
1777
- if (!text) return false;
1778
- const trimmed = text.trimStart();
1779
- return SYSTEM_LIKE_PREFIXES.some((p) => trimmed.startsWith(p));
1780
- };
1781
- const isInternalSelftunePrompt = (
1782
- text: string | null | undefined,
1783
- promptKind: string | null | undefined,
1784
- ): boolean => {
1785
- if (!text) return false;
1786
- const lowered = text.toLowerCase();
1787
- return (
1788
- promptKind === "meta" && INTERNAL_EVAL_MARKERS.some((marker) => lowered.includes(marker))
1789
- );
1790
- };
1791
- const isPollutingPrompt = (
1792
- text: string | null | undefined,
1793
- promptKind: string | null | undefined,
1794
- ): boolean => isSystemLike(text) || isInternalSelftunePrompt(text, promptKind);
1795
- const classifyObservationKind = (
1796
- skillInvocationId: string,
1797
- captureMode: string | null,
1798
- triggered: number,
1799
- rawSourceRefJson: string | null,
1800
- ): "canonical" | "repaired_trigger" | "repaired_contextual_miss" | "legacy_materialized" => {
1801
- if (skillInvocationId.includes(":su:")) return "legacy_materialized";
1802
- if (captureMode === "repair") {
1803
- const rawSourceRef = safeParseJson(rawSourceRefJson) as {
1804
- metadata?: { miss_type?: string };
1805
- } | null;
1806
- if (triggered === 0 && rawSourceRef?.metadata?.miss_type === "contextual_read") {
1807
- return "repaired_contextual_miss";
1808
- }
1809
- return "repaired_trigger";
1810
- }
1811
- return "canonical";
1812
- };
1813
- const normalizeQueryForGrouping = (query: string) =>
1814
- query.replace(/\s+/g, " ").trim().toLowerCase();
1815
-
1816
- const rows = db
1817
- .query(
1818
- `SELECT
1819
- si.skill_name,
1820
- si.session_id,
1821
- si.occurred_at,
1822
- si.triggered,
1823
- si.matched_prompt_id,
1824
- si.confidence,
1825
- si.invocation_mode,
1826
- si.skill_invocation_id,
1827
- si.capture_mode,
1828
- si.raw_source_ref,
1829
- si.query,
1830
- p.prompt_text,
1831
- p.prompt_kind
1832
- FROM skill_invocations si
1833
- LEFT JOIN prompts p ON si.matched_prompt_id = p.prompt_id`,
1834
- )
1835
- .all() as Array<{
1836
- skill_name: string;
1837
- session_id: string;
1838
- occurred_at: string | null;
1839
- triggered: number;
1840
- matched_prompt_id: string | null;
1841
- confidence: number | null;
1842
- invocation_mode: string | null;
1843
- skill_invocation_id: string;
1844
- capture_mode: string | null;
1845
- raw_source_ref: string | null;
1846
- query: string | null;
1847
- prompt_text: string | null;
1848
- prompt_kind: string | null;
1849
- }>;
1850
-
1851
- const bySkill = new Map<
1852
- string,
1853
- Array<{
1854
- skill_name: string;
1855
- session_id: string;
1856
- occurred_at: string | null;
1857
- triggered: number;
1858
- matched_prompt_id: string | null;
1859
- confidence: number | null;
1860
- invocation_mode: string | null;
1861
- queryText: string;
1862
- isPolluting: boolean;
1863
- observation_kind:
1864
- | "canonical"
1865
- | "repaired_trigger"
1866
- | "repaired_contextual_miss"
1867
- | "legacy_materialized";
1868
- groupKey: string;
1869
- }>
1870
- >();
1871
- const trustedRows: Array<{
1872
- skill_name: string;
1873
- session_id: string;
1874
- occurred_at: string | null;
1875
- triggered: number;
1876
- matched_prompt_id: string | null;
1877
- confidence: number | null;
1878
- invocation_mode: string | null;
1879
- query_text: string;
1880
- }> = [];
1881
-
1882
- for (const row of rows) {
1883
- const queryText = row.query || row.prompt_text || "";
1884
- const pollutionText = row.prompt_text || row.query || "";
1885
- const observation_kind = classifyObservationKind(
1886
- row.skill_invocation_id,
1887
- row.capture_mode,
1888
- row.triggered,
1889
- row.raw_source_ref,
1890
- );
1891
- if (isPollutingPrompt(pollutionText, row.prompt_kind)) continue;
1892
- if (observation_kind === "legacy_materialized") continue;
1893
-
1894
- const normalizedQuery = normalizeQueryForGrouping(queryText);
1895
- const groupKey =
1896
- normalizedQuery.length > 0
1897
- ? `${row.session_id}::${normalizedQuery}`
1898
- : `${row.skill_invocation_id}`;
1899
- const arr = bySkill.get(row.skill_name);
1900
- const enriched = {
1901
- skill_name: row.skill_name,
1902
- session_id: row.session_id,
1903
- occurred_at: row.occurred_at,
1904
- triggered: row.triggered,
1905
- matched_prompt_id: row.matched_prompt_id,
1906
- confidence: row.confidence,
1907
- invocation_mode: row.invocation_mode,
1908
- queryText,
1909
- isPolluting: false,
1910
- observation_kind,
1911
- groupKey,
1912
- };
1913
- if (arr) arr.push(enriched);
1914
- else bySkill.set(row.skill_name, [enriched]);
1915
- }
1916
-
1917
- for (const [, skillRows] of bySkill.entries()) {
1918
- const grouped = new Map<string, typeof skillRows>();
1919
- for (const row of skillRows) {
1920
- const arr = grouped.get(row.groupKey);
1921
- if (arr) arr.push(row);
1922
- else grouped.set(row.groupKey, [row]);
1923
- }
1924
-
1925
- const deduped = [...grouped.values()].map((group) => {
1926
- const sorted = [...group].sort((a, b) => {
1927
- const aScore =
1928
- (a.triggered === 1 ? 100 : 0) +
1929
- (a.observation_kind === "canonical" ? 20 : 0) +
1930
- (a.observation_kind === "repaired_trigger" ? 15 : 0);
1931
- const bScore =
1932
- (b.triggered === 1 ? 100 : 0) +
1933
- (b.observation_kind === "canonical" ? 20 : 0) +
1934
- (b.observation_kind === "repaired_trigger" ? 15 : 0);
1935
- if (aScore !== bScore) return bScore - aScore;
1936
- return (b.occurred_at ?? "").localeCompare(a.occurred_at ?? "");
1937
- });
1938
- return sorted[0]!;
1939
- });
1940
-
1941
- trustedRows.push(
1942
- ...deduped.map((row) => ({
1943
- skill_name: row.skill_name,
1944
- session_id: row.session_id,
1945
- occurred_at: row.occurred_at,
1946
- triggered: row.triggered,
1947
- matched_prompt_id: row.matched_prompt_id,
1948
- confidence: row.confidence,
1949
- invocation_mode: row.invocation_mode,
1950
- query_text: row.queryText,
1951
- })),
1952
- );
1953
- }
1954
-
1955
- return trustedRows;
1956
- }
1957
-
1958
- export function getSkillTrustSummaries(db: Database): SkillTrustSummary[] {
1959
- const rows = queryTrustedSkillObservationRows(db);
1960
-
1961
- // Build latest_action map from evolution_audit
1962
- const auditRows = db
1963
- .query(
1964
- `SELECT skill_name, action, timestamp
1965
- FROM evolution_audit
1966
- WHERE skill_name IS NOT NULL
1967
- ORDER BY timestamp DESC`,
1968
- )
1969
- .all() as Array<{
1970
- skill_name: string | null;
1971
- action: string;
1972
- timestamp: string;
1973
- }>;
1974
-
1975
- const latestActions = new Map<string, string>();
1976
- for (const row of auditRows) {
1977
- if (row.skill_name && !latestActions.has(row.skill_name)) {
1978
- latestActions.set(row.skill_name, row.action);
1979
- }
1980
- }
1981
-
1982
- const rowsBySkill = new Map<string, typeof rows>();
1983
- for (const row of rows) {
1984
- const arr = rowsBySkill.get(row.skill_name);
1985
- if (arr) arr.push(row);
1986
- else rowsBySkill.set(row.skill_name, [row]);
1987
- }
1988
-
1989
- const summaries: SkillTrustSummary[] = [];
1990
- for (const [skillName, skillRows] of rowsBySkill.entries()) {
1991
- const total = skillRows.length;
1992
- const triggered = skillRows.filter((row) => row.triggered === 1).length;
1993
- const promptLinked = skillRows.filter((row) => row.matched_prompt_id != null).length;
1994
- const lastSeen =
1995
- skillRows
1996
- .map((row) => row.occurred_at)
1997
- .filter((value): value is string => value != null)
1998
- .sort((a, b) => b.localeCompare(a))[0] ?? null;
1999
-
2000
- summaries.push({
2001
- skill_name: skillName,
2002
- total_checks: total,
2003
- triggered_count: triggered,
2004
- miss_rate: total > 0 ? (total - triggered) / total : 0,
2005
- system_like_count: 0,
2006
- system_like_rate: 0,
2007
- prompt_link_rate: total > 0 ? promptLinked / total : 0,
2008
- latest_action: latestActions.get(skillName) ?? null,
2009
- pass_rate: total > 0 ? triggered / total : 0,
2010
- last_seen: lastSeen,
2011
- });
2012
- }
2013
-
2014
- return summaries;
2015
- }
2016
-
2017
- export function getAttentionQueue(db: Database): AttentionItem[] {
2018
- const summaries = getSkillTrustSummaries(db);
2019
- const pending = getPendingProposals(db);
2020
- const pendingSkills = new Set(pending.map((p) => p.skill_name).filter(Boolean));
2021
-
2022
- const items: AttentionItem[] = [];
2023
-
2024
- for (const s of summaries) {
2025
- if (s.latest_action === "rolled_back") {
2026
- items.push({
2027
- skill_name: s.skill_name,
2028
- category: "needs_review",
2029
- severity: "critical",
2030
- reason: "Rolled back after deployment",
2031
- recommended_action: "Review rollback evidence and decide whether to re-evolve",
2032
- timestamp: s.last_seen ?? "",
2033
- });
2034
- continue;
2035
- }
2036
-
2037
- if (pendingSkills.has(s.skill_name)) {
2038
- items.push({
2039
- skill_name: s.skill_name,
2040
- category: "needs_review",
2041
- severity: "info",
2042
- reason: "Proposal awaiting review",
2043
- recommended_action: "Review and approve or reject the pending proposal",
2044
- timestamp: s.last_seen ?? "",
2045
- });
2046
- continue;
2047
- }
2048
-
2049
- if (s.total_checks < 5) continue;
2050
-
2051
- if (s.miss_rate > 0.1) {
2052
- items.push({
2053
- skill_name: s.skill_name,
2054
- category: "regression",
2055
- severity: "warning",
2056
- reason: `High miss rate (${Math.round(s.miss_rate * 100)}%)`,
2057
- recommended_action: "Review missed invocations and consider evolving the skill description",
2058
- timestamp: s.last_seen ?? "",
2059
- });
2060
- continue;
2061
- }
2062
-
2063
- if (s.system_like_rate > 0.1) {
2064
- items.push({
2065
- skill_name: s.skill_name,
2066
- category: "polluted",
2067
- severity: "warning",
2068
- reason: `Possible telemetry pollution (${Math.round(s.system_like_rate * 100)}% system-like)`,
2069
- recommended_action: "Inspect prompts for system-injected noise",
2070
- timestamp: s.last_seen ?? "",
2071
- });
2072
- continue;
2073
- }
2074
- }
2075
-
2076
- return items;
2077
- }
2078
-
2079
- export function getRecentDecisions(db: Database, limit = 20): AutonomousDecision[] {
2080
- const rows = db
2081
- .query(
2082
- `SELECT timestamp, proposal_id, skill_name, action, details, eval_snapshot_json
2083
- FROM evolution_audit
2084
- WHERE timestamp >= datetime('now', '-7 days')
2085
- ORDER BY timestamp DESC
2086
- LIMIT ?`,
2087
- )
2088
- .all(limit) as Array<{
2089
- timestamp: string;
2090
- proposal_id: string;
2091
- skill_name: string | null;
2092
- action: string;
2093
- details: string;
2094
- eval_snapshot_json: string | null;
2095
- }>;
2096
-
2097
- return rows
2098
- .filter((row) => row.skill_name != null)
2099
- .flatMap((row) => {
2100
- const evalSnapshot = safeParseJson(row.eval_snapshot_json) as {
2101
- regressions?: unknown[];
2102
- } | null;
2103
-
2104
- let kind: DecisionKind | null;
2105
- switch (row.action) {
2106
- case "proposed":
2107
- case "created":
2108
- kind = "proposal_created";
2109
- break;
2110
- case "rejected":
2111
- kind = "proposal_rejected";
2112
- break;
2113
- case "validated":
2114
- kind =
2115
- evalSnapshot?.regressions && evalSnapshot.regressions.length > 0
2116
- ? "validation_failed"
2117
- : "proposal_created"; // validated without regressions is still a creation step
2118
- break;
2119
- case "deployed":
2120
- kind = "proposal_deployed";
2121
- break;
2122
- case "rolled_back":
2123
- kind = "rollback_triggered";
2124
- break;
2125
- default:
2126
- kind = null;
2127
- }
2128
-
2129
- if (!kind) return [];
2130
-
2131
- return [
2132
- {
2133
- timestamp: row.timestamp,
2134
- kind,
2135
- skill_name: row.skill_name!,
2136
- proposal_id: row.proposal_id,
2137
- summary: row.details ?? "",
2138
- },
2139
- ];
2140
- });
2141
- }
2142
-
2143
- // -- Helpers ------------------------------------------------------------------
2144
-
2145
- export function safeParseJsonArray<T = string>(json: string | null): T[] {
2146
- if (!json) return [];
2147
- try {
2148
- const parsed = JSON.parse(json);
2149
- return Array.isArray(parsed) ? (parsed as T[]) : [];
2150
- } catch {
2151
- return [];
2152
- }
2153
- }
2154
-
2155
- export function safeParseJson(json: string | null): Record<string, unknown> | null {
2156
- if (!json) return null;
2157
- try {
2158
- return JSON.parse(json);
2159
- } catch {
2160
- return null;
2161
- }
2162
- }
1
+ export { getCronRunsByJob, getRecentCronRuns, type CronRun } from "./queries/cron.js";
2
+ export {
3
+ getAnalyticsPayload,
4
+ getOverviewPayload,
5
+ getOverviewPayloadPaginated,
6
+ getSkillReportPayload,
7
+ getSkillReportPayloadPaginated,
8
+ getSkillsList,
9
+ type OverviewPaginationOptions,
10
+ type SkillReportPaginationOptions,
11
+ } from "./queries/dashboard.js";
12
+ export {
13
+ getOrchestrateRuns,
14
+ getPendingProposals,
15
+ queryEvolutionAudit,
16
+ queryEvolutionEvidence,
17
+ } from "./queries/evolution.js";
18
+ export {
19
+ getExecutionMetrics,
20
+ getSessionCommits,
21
+ getSkillCommitSummary,
22
+ } from "./queries/execution.js";
23
+ export {
24
+ type GradeRegressionResult,
25
+ type GradingBaselineRow,
26
+ type RecentGradingResultRow,
27
+ queryGradeRegression,
28
+ queryGradingBaseline,
29
+ queryGradingResults,
30
+ queryImprovementSignals,
31
+ queryRecentGradingResults,
32
+ queryReplayEntryResults,
33
+ queryReplayRegressions,
34
+ } from "./queries/monitoring.js";
35
+ export {
36
+ type CreatorContributionRelayStats,
37
+ type CreatorContributionStagingRow,
38
+ getCreatorContributionRelayStats,
39
+ getCreatorContributionStagingCounts,
40
+ getLastUploadError,
41
+ getLastUploadSuccess,
42
+ getOldestPendingAge,
43
+ getPendingCreatorContributionRows,
44
+ queryCanonicalRecordsForStaging,
45
+ } from "./queries/staging.js";
46
+ export {
47
+ getAttentionQueue,
48
+ getRecentDecisions,
49
+ getSkillTrustSummaries,
50
+ queryTrustedSkillObservationRows,
51
+ type SkillTrustSummary,
52
+ type TrustedSkillObservationRow,
53
+ } from "./queries/trust.js";
54
+ export { safeParseJson, safeParseJsonArray } from "./queries/json.js";
55
+ export {
56
+ queryQueryLog,
57
+ querySessionTelemetry,
58
+ querySkillRecords,
59
+ querySkillUsageRecords,
60
+ } from "./queries/raw.js";