tuneloop 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +256 -0
- package/dist/chunk-RB45XK57.js +6941 -0
- package/dist/chunk-RB45XK57.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +154 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/app.js +4937 -0
- package/dist/client/app.js.map +1 -0
- package/dist/client/favicon.svg +7 -0
- package/dist/client/index.html +74 -0
- package/dist/client/styles.css +698 -0
- package/dist/index.d.ts +1623 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1623 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { Server } from 'node:http';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The normalized session model — the contract every processor reads and every
|
|
6
|
+
* adapter produces. Adapters translate a vendor's transcript into this shape;
|
|
7
|
+
* processors and the store never need to know which harness a session came from.
|
|
8
|
+
*
|
|
9
|
+
* `raw` is always preserved as an escape hatch for processors that need
|
|
10
|
+
* vendor-specific detail the canonical view doesn't capture.
|
|
11
|
+
*/
|
|
12
|
+
/** Vendor-neutral classification of a tool call. The per-vendor mapping lives in the adapter. */
|
|
13
|
+
type CanonicalAction = 'file_write' | 'file_read' | 'shell' | 'search' | 'task_spawn' | 'mcp_call' | 'web' | 'todo' | 'skill' | 'other';
|
|
14
|
+
interface TokenUsage {
|
|
15
|
+
input: number;
|
|
16
|
+
output: number;
|
|
17
|
+
cacheCreate: number;
|
|
18
|
+
cacheRead: number;
|
|
19
|
+
}
|
|
20
|
+
declare function emptyUsage(): TokenUsage;
|
|
21
|
+
declare function addUsage(a: TokenUsage, b: TokenUsage): TokenUsage;
|
|
22
|
+
type ContentBlock = {
|
|
23
|
+
type: 'text';
|
|
24
|
+
text: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'thinking';
|
|
27
|
+
text: string;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'tool_use';
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
input: unknown;
|
|
33
|
+
} | {
|
|
34
|
+
type: 'tool_result';
|
|
35
|
+
toolUseId: string;
|
|
36
|
+
isError: boolean;
|
|
37
|
+
content: unknown;
|
|
38
|
+
};
|
|
39
|
+
interface BaseEvent {
|
|
40
|
+
uuid?: string;
|
|
41
|
+
parentUuid?: string | null;
|
|
42
|
+
ts?: string;
|
|
43
|
+
isSidechain: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Dense ordinal over MAIN-THREAD events (sidechain events have none), assigned
|
|
46
|
+
* post-merge by assignSeq() (core/blocks.ts). The coordinate the block partition
|
|
47
|
+
* is defined in; persisted in the session blob.
|
|
48
|
+
*/
|
|
49
|
+
seq?: number;
|
|
50
|
+
/**
|
|
51
|
+
* For sidechain (subagent) events, the stable id of the subagent that emitted
|
|
52
|
+
* them — Claude Code's per-subagent transcript id. Lets the viewer group a
|
|
53
|
+
* subagent's turns into their own thread instead of interleaving them with the
|
|
54
|
+
* main conversation. Undefined for main-thread events.
|
|
55
|
+
*/
|
|
56
|
+
agentId?: string;
|
|
57
|
+
}
|
|
58
|
+
interface UserMessage extends BaseEvent {
|
|
59
|
+
kind: 'user';
|
|
60
|
+
text: string;
|
|
61
|
+
blocks: ContentBlock[];
|
|
62
|
+
}
|
|
63
|
+
interface AssistantMessage extends BaseEvent {
|
|
64
|
+
kind: 'assistant';
|
|
65
|
+
model?: string;
|
|
66
|
+
blocks: ContentBlock[];
|
|
67
|
+
usage: TokenUsage;
|
|
68
|
+
/**
|
|
69
|
+
* Native cost (USD) for this message as reported by the source, when the source
|
|
70
|
+
* computes its own cost (e.g. OpenCode, which routes to many providers tuneloop's
|
|
71
|
+
* rate table doesn't cover). Used by computeSessionCost as a fallback when the
|
|
72
|
+
* model has no entry in models.json. Absent for sources priced from tokens.
|
|
73
|
+
*/
|
|
74
|
+
costUsd?: number;
|
|
75
|
+
}
|
|
76
|
+
interface SystemEvent extends BaseEvent {
|
|
77
|
+
kind: 'system';
|
|
78
|
+
subtype?: string;
|
|
79
|
+
text?: string;
|
|
80
|
+
}
|
|
81
|
+
type Event = UserMessage | AssistantMessage | SystemEvent;
|
|
82
|
+
/** A tool_use joined to its tool_result, classified into a canonical action. */
|
|
83
|
+
interface ToolCall {
|
|
84
|
+
id: string;
|
|
85
|
+
name: string;
|
|
86
|
+
action: CanonicalAction;
|
|
87
|
+
input: unknown;
|
|
88
|
+
/** Normalized fields per action (paths for file ops, command for shell, etc.). */
|
|
89
|
+
target: {
|
|
90
|
+
paths?: string[];
|
|
91
|
+
command?: string;
|
|
92
|
+
};
|
|
93
|
+
result: {
|
|
94
|
+
ok: boolean;
|
|
95
|
+
isError: boolean;
|
|
96
|
+
raw?: unknown;
|
|
97
|
+
};
|
|
98
|
+
isSidechain: boolean;
|
|
99
|
+
ts?: string;
|
|
100
|
+
durationMs?: number;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A subagent (sidechain) spawned within a session. Claude Code writes each
|
|
104
|
+
* subagent's transcript to its own file with a sibling `.meta.json`; this is the
|
|
105
|
+
* normalized view of that metadata. `toolUseId` is the id of the spawning tool
|
|
106
|
+
* call (the `Task`/`Agent` tool_use) in the parent thread, which lets the viewer
|
|
107
|
+
* link that call to the subagent's transcript. Workflow subagents have no
|
|
108
|
+
* spawning tool call, so `toolUseId` is absent for them.
|
|
109
|
+
*/
|
|
110
|
+
interface SubagentMeta {
|
|
111
|
+
agentId: string;
|
|
112
|
+
agentType?: string;
|
|
113
|
+
description?: string;
|
|
114
|
+
toolUseId?: string;
|
|
115
|
+
}
|
|
116
|
+
interface Session {
|
|
117
|
+
/** Namespaced id, e.g. `claude-code:<uuid>` — unique across vendors. */
|
|
118
|
+
id: string;
|
|
119
|
+
/** Raw vendor session id. */
|
|
120
|
+
sessionId: string;
|
|
121
|
+
/** Adapter / harness id, e.g. `claude-code`. */
|
|
122
|
+
source: string;
|
|
123
|
+
/** LLM vendor family for slicing, e.g. `anthropic`. */
|
|
124
|
+
provider: string;
|
|
125
|
+
title?: string;
|
|
126
|
+
/**
|
|
127
|
+
* For a child transcript that lives in its own file (Codex sub-agent or `/fork`),
|
|
128
|
+
* the parent session's raw id. Used to (a) fold sub-agents into the parent as
|
|
129
|
+
* sidechains and (b) trim the replayed parent prefix both kinds inherit
|
|
130
|
+
* (see analyze.ts / merge.ts, ADR-0005). Undefined for top-level sessions.
|
|
131
|
+
*/
|
|
132
|
+
forkedFromId?: string;
|
|
133
|
+
/**
|
|
134
|
+
* True only for a sub-agent (sidechain) child. Distinguishes it from a `/fork`,
|
|
135
|
+
* which also carries `forkedFromId` but is its own top-level session: only
|
|
136
|
+
* sub-agents fold into the parent group (ADR-0005).
|
|
137
|
+
*/
|
|
138
|
+
isSubagent?: boolean;
|
|
139
|
+
project: {
|
|
140
|
+
cwd?: string;
|
|
141
|
+
repo?: string;
|
|
142
|
+
branch?: string;
|
|
143
|
+
};
|
|
144
|
+
startedAt?: string;
|
|
145
|
+
endedAt?: string;
|
|
146
|
+
/** Distinct models seen across assistant messages (model is per-message). */
|
|
147
|
+
models: string[];
|
|
148
|
+
/** Rolled-up token usage across all assistant messages (incl. sidechains). */
|
|
149
|
+
tokens: TokenUsage;
|
|
150
|
+
events: Event[];
|
|
151
|
+
/** Flattened convenience view of every tool call, incl. sidechains. */
|
|
152
|
+
toolCalls: ToolCall[];
|
|
153
|
+
/** Subagents spawned in this session (one per sidechain transcript). */
|
|
154
|
+
subagents?: SubagentMeta[];
|
|
155
|
+
raw: {
|
|
156
|
+
path: string;
|
|
157
|
+
contentHash: string;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Translates a vendor's transcripts into the normalized session model. */
|
|
162
|
+
interface SourceAdapter {
|
|
163
|
+
/** Stable adapter id, e.g. `claude-code`. */
|
|
164
|
+
id: string;
|
|
165
|
+
/** LLM vendor family, e.g. `anthropic`. */
|
|
166
|
+
provider: string;
|
|
167
|
+
/**
|
|
168
|
+
* Version of THIS adapter's parse output. Bumped when the adapter extracts more
|
|
169
|
+
* (or different) data from the same transcript bytes. Combined with the shared
|
|
170
|
+
* `NORMALIZE_VERSION` into the stored `parse_version` (see analyze.ts), so a
|
|
171
|
+
* per-vendor bump re-ingests only that vendor's sessions.
|
|
172
|
+
*/
|
|
173
|
+
parseVersion: number;
|
|
174
|
+
/** Locations to scan when the user passes no directories. */
|
|
175
|
+
defaultRoots(): string[];
|
|
176
|
+
/** Find candidate session files under the given roots. */
|
|
177
|
+
discover(roots: string[]): Promise<string[]>;
|
|
178
|
+
/** Parse one file into a Session; null if it isn't a session this adapter owns. */
|
|
179
|
+
parse(path: string): Promise<Session | null>;
|
|
180
|
+
/**
|
|
181
|
+
* Store-backed alternative to discover/parse. Adapters whose sessions live in a
|
|
182
|
+
* single database (not one file per session) implement this to yield sessions
|
|
183
|
+
* directly; analyze.ts prefers it over the discover→parse file loop when present.
|
|
184
|
+
*/
|
|
185
|
+
discoverSessions?(roots: string[]): Promise<Session[]>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The facet registry: the single source of truth for the categorical dimensions
|
|
190
|
+
* the dashboard charts, filters, and (later) compares by.
|
|
191
|
+
*
|
|
192
|
+
* A facet's `source` names WHERE its value lives — which also implies its grain.
|
|
193
|
+
* `multi` is cardinality (array vs scalar). `type` is the element type. The query
|
|
194
|
+
* builder (Store.facetDistribution / facetPredicate) derives the exact read shape
|
|
195
|
+
* — raw column / json_extract / json_each / EXISTS — from `(source, multi)`, so
|
|
196
|
+
* nothing above the store hardcodes which dimensions exist.
|
|
197
|
+
*
|
|
198
|
+
* Two sources of facets, both persisted to the `facets` table at analyze time so
|
|
199
|
+
* the separate serve process can read them without importing processors:
|
|
200
|
+
* - intrinsic facets (below): structural, present without any processor
|
|
201
|
+
* - processor-declared facets: a processor's `facets` field (e.g. enrichment)
|
|
202
|
+
*/
|
|
203
|
+
/** Where a facet's value lives — implies its grain. */
|
|
204
|
+
type FacetSource = 'session' | 'annotation' | 'tool-call' | 'usage' | 'block';
|
|
205
|
+
type FacetType = 'string' | 'number' | 'boolean' | 'enum';
|
|
206
|
+
/** Where a facet may surface in the UI. */
|
|
207
|
+
type FacetRole = 'chart' | 'filter' | 'detail';
|
|
208
|
+
interface FacetSpec {
|
|
209
|
+
key: string;
|
|
210
|
+
label?: string;
|
|
211
|
+
/** Element type (drives rendering); never 'array' — array-ness is `multi`. */
|
|
212
|
+
type: FacetType;
|
|
213
|
+
source: FacetSource;
|
|
214
|
+
/**
|
|
215
|
+
* Physical column for session / tool-call / usage facets; defaults to `key`.
|
|
216
|
+
* Unused for `annotation` (there `key` IS the annotation key).
|
|
217
|
+
*/
|
|
218
|
+
column?: string;
|
|
219
|
+
/** Base predicate scoping rows for tool-call / usage facets, e.g. action='skill'. */
|
|
220
|
+
base?: string;
|
|
221
|
+
/**
|
|
222
|
+
* Array-valued (json_each) vs scalar. Only meaningful for session/annotation
|
|
223
|
+
* storage; for tool-call/usage the to-many-ness is intrinsic to the grain.
|
|
224
|
+
*/
|
|
225
|
+
multi?: boolean;
|
|
226
|
+
roles?: FacetRole[];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The measure registry: the "how much" axis, parallel to the facet registry.
|
|
231
|
+
* A measure is an aggregation (`agg`) of an expression (`expr`) over the
|
|
232
|
+
* population at its grain. Crossed with a facet (the "which" axis) by
|
|
233
|
+
* Store.breakdown, it produces every "<measure> by <facet>" view.
|
|
234
|
+
*
|
|
235
|
+
* Like facets: intrinsic measures live here; processors add more via
|
|
236
|
+
* Processor.measures; both persist to the `measures` table at analyze time so
|
|
237
|
+
* the serve process discovers them without importing processors.
|
|
238
|
+
*
|
|
239
|
+
* `source` (reused from facets) says WHERE the value lives and implies the grain
|
|
240
|
+
* (grainOf). `expr` is SQL over that source's anchor alias — s (sessions),
|
|
241
|
+
* u (usage_facts), t (tool_calls). For `rate`, expr is a 0/1 (boolean) predicate.
|
|
242
|
+
*/
|
|
243
|
+
|
|
244
|
+
type MeasureAgg = 'sum' | 'count' | 'count_distinct' | 'avg' | 'rate';
|
|
245
|
+
interface MeasureSpec {
|
|
246
|
+
key: string;
|
|
247
|
+
label?: string;
|
|
248
|
+
source: FacetSource;
|
|
249
|
+
/** SQL over the anchor alias (s/u/t). For `rate`, a 0/1 boolean expression. */
|
|
250
|
+
expr: string;
|
|
251
|
+
agg: MeasureAgg;
|
|
252
|
+
/** Optional base predicate restricting the population. */
|
|
253
|
+
base?: string;
|
|
254
|
+
format?: 'usd' | 'int' | 'pct';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Persistence-facing record shapes. Kept separate from both the normalized
|
|
259
|
+
* model (core/model.ts) and the Store implementation so processors can import
|
|
260
|
+
* these without pulling in better-sqlite3.
|
|
261
|
+
*/
|
|
262
|
+
|
|
263
|
+
type ArtifactKind = 'file' | 'commit' | 'pr' | 'ticket' | 'feature';
|
|
264
|
+
type LinkSource = 'explicit' | 'transitive' | 'derived' | 'user';
|
|
265
|
+
type ArtifactRelation = 'part_of' | 'resolves' | 'child_of' | 'caused_by';
|
|
266
|
+
type SessionArtifactRole = 'created' | 'edited' | 'contributed' | 'reviewed';
|
|
267
|
+
interface ArtifactInput {
|
|
268
|
+
id: string;
|
|
269
|
+
kind: ArtifactKind;
|
|
270
|
+
repo?: string;
|
|
271
|
+
ident?: string;
|
|
272
|
+
externalId?: string;
|
|
273
|
+
/** github | jira | linear | asana | user | codebase-inferred | transcript | ... */
|
|
274
|
+
source?: string;
|
|
275
|
+
title?: string;
|
|
276
|
+
/** PR author / ticket assignee / feature owner — the "team" artifact filter. */
|
|
277
|
+
owner?: string;
|
|
278
|
+
complexity?: number;
|
|
279
|
+
/** story_points | diff_size | equal_split */
|
|
280
|
+
complexityBasis?: string;
|
|
281
|
+
status?: string;
|
|
282
|
+
createdAt?: string;
|
|
283
|
+
/** Merge / resolve / ship date. NULL until the artifact completes. */
|
|
284
|
+
completedAt?: string;
|
|
285
|
+
parentArtifactId?: string;
|
|
286
|
+
json?: unknown;
|
|
287
|
+
}
|
|
288
|
+
interface ArtifactLinkInput {
|
|
289
|
+
fromId: string;
|
|
290
|
+
toId: string;
|
|
291
|
+
relation: ArtifactRelation;
|
|
292
|
+
source: LinkSource;
|
|
293
|
+
confidence?: number;
|
|
294
|
+
}
|
|
295
|
+
interface SessionArtifactInput {
|
|
296
|
+
artifactId: string;
|
|
297
|
+
role: SessionArtifactRole;
|
|
298
|
+
source: LinkSource;
|
|
299
|
+
confidence?: number;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* A reparent of an existing feature, for an enrichment processor that maintains
|
|
303
|
+
* the feature hierarchy as it sees more sessions. Applied only to machine-derived
|
|
304
|
+
* features — `user`-authored features are never touched. Auto-rename is
|
|
305
|
+
* deliberately NOT supported: a bad rename retroactively mislabels every session
|
|
306
|
+
* under the feature, so titles are fixed at creation (the dashboard can rename).
|
|
307
|
+
*/
|
|
308
|
+
interface FeatureRevisionInput {
|
|
309
|
+
id: string;
|
|
310
|
+
/** New parent id; `null` = make top-level; omit (`undefined`) = keep. */
|
|
311
|
+
parentId?: string | null;
|
|
312
|
+
}
|
|
313
|
+
interface OutcomeInput {
|
|
314
|
+
type: string;
|
|
315
|
+
/** NULL for session-level outcomes (session_success, plan_drafted, ...). */
|
|
316
|
+
artifactId?: string | null;
|
|
317
|
+
ts?: string;
|
|
318
|
+
}
|
|
319
|
+
interface FileIndexInput {
|
|
320
|
+
repo?: string;
|
|
321
|
+
path: string;
|
|
322
|
+
}
|
|
323
|
+
interface AnnotationInput {
|
|
324
|
+
key: string;
|
|
325
|
+
value: unknown;
|
|
326
|
+
}
|
|
327
|
+
/** A contiguous deterministic slice of a session's main thread. */
|
|
328
|
+
interface BlockInput {
|
|
329
|
+
idx: number;
|
|
330
|
+
startSeq: number;
|
|
331
|
+
endSeq: number;
|
|
332
|
+
boundaryKind: string;
|
|
333
|
+
tsStart?: string;
|
|
334
|
+
tsEnd?: string;
|
|
335
|
+
}
|
|
336
|
+
/** usage_facts.idx -> block idx (a total partition; non-overlap is PK-enforced). */
|
|
337
|
+
interface BlockUsageInput {
|
|
338
|
+
usageIdx: number;
|
|
339
|
+
blockIdx: number;
|
|
340
|
+
}
|
|
341
|
+
/** tool_calls.idx -> block idx. */
|
|
342
|
+
interface BlockToolInput {
|
|
343
|
+
toolIdx: number;
|
|
344
|
+
blockIdx: number;
|
|
345
|
+
}
|
|
346
|
+
/** A label on one block (e.g. use_case), parallel to AnnotationInput. */
|
|
347
|
+
interface BlockAnnotationInput {
|
|
348
|
+
blockIdx: number;
|
|
349
|
+
key: string;
|
|
350
|
+
value: unknown;
|
|
351
|
+
}
|
|
352
|
+
/** A block -> artifact link (block→PR/commit deterministic; block→feature derived). */
|
|
353
|
+
interface BlockArtifactInput {
|
|
354
|
+
blockIdx: number;
|
|
355
|
+
artifactId: string;
|
|
356
|
+
role: SessionArtifactRole;
|
|
357
|
+
source?: LinkSource;
|
|
358
|
+
confidence?: number;
|
|
359
|
+
}
|
|
360
|
+
/** One assistant message's usage + cost — a row in the `usage_facts` table. */
|
|
361
|
+
interface UsageFactInput {
|
|
362
|
+
idx: number;
|
|
363
|
+
model: string;
|
|
364
|
+
isSidechain: boolean;
|
|
365
|
+
ts?: string;
|
|
366
|
+
tokens: TokenUsage;
|
|
367
|
+
usd: number;
|
|
368
|
+
}
|
|
369
|
+
interface SessionRow {
|
|
370
|
+
id: string;
|
|
371
|
+
sessionId: string;
|
|
372
|
+
source: string;
|
|
373
|
+
provider: string;
|
|
374
|
+
title?: string;
|
|
375
|
+
repo?: string;
|
|
376
|
+
branch?: string;
|
|
377
|
+
cwd?: string;
|
|
378
|
+
startedAt?: string;
|
|
379
|
+
endedAt?: string;
|
|
380
|
+
nTurns: number;
|
|
381
|
+
nToolCalls: number;
|
|
382
|
+
models: string[];
|
|
383
|
+
tokens: TokenUsage;
|
|
384
|
+
costUsd: number;
|
|
385
|
+
priceTableVersion: string;
|
|
386
|
+
contentHash: string;
|
|
387
|
+
parseVersion: number;
|
|
388
|
+
}
|
|
389
|
+
interface ProcessorRunRow {
|
|
390
|
+
version: number;
|
|
391
|
+
inputHash: string;
|
|
392
|
+
model: string | null;
|
|
393
|
+
invalidated: boolean;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** A JSON Schema object describing the structured output a completion must return. */
|
|
397
|
+
type JsonSchema = Record<string, unknown>;
|
|
398
|
+
interface StructuredRequest {
|
|
399
|
+
system: string;
|
|
400
|
+
user: string;
|
|
401
|
+
/** JSON Schema for the result — the forced tool's input schema. */
|
|
402
|
+
schema: JsonSchema;
|
|
403
|
+
/** Name of the single forced tool. */
|
|
404
|
+
toolName: string;
|
|
405
|
+
maxTokens?: number;
|
|
406
|
+
}
|
|
407
|
+
interface LlmResult {
|
|
408
|
+
/** The model's structured output (the forced tool's input), normalized by the caller. */
|
|
409
|
+
data: Record<string, unknown>;
|
|
410
|
+
usage: TokenUsage;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Thin provider-neutral client. Enrichment uses a single structured completion
|
|
414
|
+
* per session: the output schema is exposed as one forced tool call (the tool
|
|
415
|
+
* input IS the result), which works identically across Anthropic and every
|
|
416
|
+
* OpenAI-compatible endpoint — unlike provider-specific structured-output modes.
|
|
417
|
+
* This requires a tool-call-capable model; non-tool models are unsupported.
|
|
418
|
+
*/
|
|
419
|
+
interface LlmClient {
|
|
420
|
+
provider: string;
|
|
421
|
+
model: string;
|
|
422
|
+
completeStructured(req: StructuredRequest): Promise<LlmResult>;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
interface Logger {
|
|
426
|
+
debug(msg: string): void;
|
|
427
|
+
info(msg: string): void;
|
|
428
|
+
warn(msg: string): void;
|
|
429
|
+
error(msg: string): void;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* The processor contract — the main extension point.
|
|
434
|
+
*
|
|
435
|
+
* Everything derived from a session is a registered processor with this uniform
|
|
436
|
+
* interface: token/cost, files touched, git/PR outcomes, and (later) LLM
|
|
437
|
+
* enrichment. To add a new fact, implement Processor and register it — no
|
|
438
|
+
* changes to the runner, the store schema, or the dashboard.
|
|
439
|
+
*/
|
|
440
|
+
|
|
441
|
+
type ProcessorKind = 'static' | 'enrichment';
|
|
442
|
+
interface ShResult {
|
|
443
|
+
stdout: string;
|
|
444
|
+
code: number;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* An existing feature a processor can link a session to. Carries enough of the
|
|
448
|
+
* hierarchy for an enrichment processor to attach a session to the most specific
|
|
449
|
+
* feature, place a new feature under the right parent, and refine the tree.
|
|
450
|
+
*/
|
|
451
|
+
interface FeatureRef {
|
|
452
|
+
id: string;
|
|
453
|
+
title: string;
|
|
454
|
+
/** Parent feature id (null = top-level) — the shape of the hierarchy. */
|
|
455
|
+
parentId?: string | null;
|
|
456
|
+
/** Provenance; `user`-authored features are locked from auto-rename/reparent. */
|
|
457
|
+
source?: string | null;
|
|
458
|
+
/**
|
|
459
|
+
* Repos associated anywhere in this feature's subtree (itself + descendants),
|
|
460
|
+
* from linked sessions and any explicit repo. Empty = unscoped/global (e.g. a
|
|
461
|
+
* cross-repo epic or a fresh user feature). Auto-derived linkage is allowed
|
|
462
|
+
* only to a feature that is global or already includes the session's repo.
|
|
463
|
+
*/
|
|
464
|
+
repos?: string[];
|
|
465
|
+
}
|
|
466
|
+
/** A user-linked artifact that needs block-level attribution. */
|
|
467
|
+
interface UserLinkedArtifact {
|
|
468
|
+
artifactId: string;
|
|
469
|
+
kind: 'pr' | 'feature';
|
|
470
|
+
title: string | null;
|
|
471
|
+
ident: string | null;
|
|
472
|
+
}
|
|
473
|
+
/** Block indices already attributed to a PR by deterministic processors. */
|
|
474
|
+
interface PrBlockAttribution {
|
|
475
|
+
blockIdx: number;
|
|
476
|
+
artifactId: string;
|
|
477
|
+
title: string | null;
|
|
478
|
+
}
|
|
479
|
+
interface ProcessorContext {
|
|
480
|
+
session: Session;
|
|
481
|
+
log: Logger;
|
|
482
|
+
/** Whether an LLM provider + key is configured this run. */
|
|
483
|
+
llmEnabled: boolean;
|
|
484
|
+
/** LLM client for enrichment processors; null when not configured. */
|
|
485
|
+
llm: LlmClient | null;
|
|
486
|
+
/** Existing features in the store, to bias derived feature linkage toward. */
|
|
487
|
+
existingFeatures: FeatureRef[];
|
|
488
|
+
/** Titles of features the user has rejected for this session (tombstoned). */
|
|
489
|
+
rejectedFeatureTitles: string[];
|
|
490
|
+
/** User-linked PRs/features for this session that have no block-level attribution yet. */
|
|
491
|
+
userLinkedArtifacts: UserLinkedArtifact[];
|
|
492
|
+
/** Blocks already attributed to PRs by deterministic processors (outcomes-git). */
|
|
493
|
+
prBlockAttributions: PrBlockAttribution[];
|
|
494
|
+
/** Run a local binary (git, gh). Resolves null if the binary is missing. */
|
|
495
|
+
sh: (cmd: string, args: string[], opts?: {
|
|
496
|
+
cwd?: string;
|
|
497
|
+
}) => Promise<ShResult | null>;
|
|
498
|
+
}
|
|
499
|
+
/** Everything a processor can emit. The runner stamps each row with the processor name. */
|
|
500
|
+
interface ProcessorResult {
|
|
501
|
+
annotations?: AnnotationInput[];
|
|
502
|
+
artifacts?: ArtifactInput[];
|
|
503
|
+
links?: ArtifactLinkInput[];
|
|
504
|
+
sessionArtifacts?: SessionArtifactInput[];
|
|
505
|
+
/** In-place edits to existing (non-user) features — rename / reparent. */
|
|
506
|
+
featureRevisions?: FeatureRevisionInput[];
|
|
507
|
+
outcomes?: OutcomeInput[];
|
|
508
|
+
files?: FileIndexInput[];
|
|
509
|
+
/** Block partition + membership (owned by segment-blocks). */
|
|
510
|
+
blocks?: BlockInput[];
|
|
511
|
+
blockUsage?: BlockUsageInput[];
|
|
512
|
+
blockTool?: BlockToolInput[];
|
|
513
|
+
/** Per-block labels / links (use_case from enrich-session, PR/commit from outcomes-git, feature from enrich-session). */
|
|
514
|
+
blockAnnotations?: BlockAnnotationInput[];
|
|
515
|
+
blockArtifacts?: BlockArtifactInput[];
|
|
516
|
+
/** For enrichment processors: the LLM spend this processor incurred. */
|
|
517
|
+
selfCost?: {
|
|
518
|
+
tokens: TokenUsage;
|
|
519
|
+
usd: number;
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
interface RefreshContext {
|
|
523
|
+
artifacts: ArtifactInput[];
|
|
524
|
+
log: Logger;
|
|
525
|
+
sh: (cmd: string, args: string[], opts?: {
|
|
526
|
+
cwd?: string;
|
|
527
|
+
}) => Promise<ShResult | null>;
|
|
528
|
+
}
|
|
529
|
+
interface RefreshResult {
|
|
530
|
+
artifacts?: ArtifactInput[];
|
|
531
|
+
outcomes?: OutcomeInput[];
|
|
532
|
+
}
|
|
533
|
+
interface Processor {
|
|
534
|
+
name: string;
|
|
535
|
+
/** Bump to invalidate cached results and force reprocessing. */
|
|
536
|
+
version: number;
|
|
537
|
+
kind: ProcessorKind;
|
|
538
|
+
/** Gates execution: `llm` skips when no provider is configured. */
|
|
539
|
+
needs?: {
|
|
540
|
+
llm?: boolean;
|
|
541
|
+
network?: boolean;
|
|
542
|
+
};
|
|
543
|
+
/** Names of processors that must run first (topo-sorted). */
|
|
544
|
+
requires?: string[];
|
|
545
|
+
/** Facets this processor contributes to the dashboard registry. */
|
|
546
|
+
facets?: FacetSpec[];
|
|
547
|
+
/** Measures this processor contributes (over numeric facts it emits). */
|
|
548
|
+
measures?: MeasureSpec[];
|
|
549
|
+
run(ctx: ProcessorContext): Promise<ProcessorResult> | ProcessorResult;
|
|
550
|
+
/** Re-check artifacts this processor owns that may have gone stale. */
|
|
551
|
+
refresh?(ctx: RefreshContext): Promise<RefreshResult>;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
declare function registerAdapter(adapter: SourceAdapter): void;
|
|
555
|
+
declare function registerProcessor(processor: Processor): void;
|
|
556
|
+
declare function getAdapters(): SourceAdapter[];
|
|
557
|
+
declare function getProcessors(): Processor[];
|
|
558
|
+
|
|
559
|
+
type DB = Database.Database;
|
|
560
|
+
|
|
561
|
+
interface Dist {
|
|
562
|
+
value: string;
|
|
563
|
+
count: number;
|
|
564
|
+
}
|
|
565
|
+
/** One failed tool call in the "Errors by category" drill-down (see errorOccurrences). */
|
|
566
|
+
interface ErrorOccurrence {
|
|
567
|
+
sessionId: string;
|
|
568
|
+
title: string | null;
|
|
569
|
+
idx: number;
|
|
570
|
+
name: string;
|
|
571
|
+
action: string;
|
|
572
|
+
command: string | null;
|
|
573
|
+
targetPath: string | null;
|
|
574
|
+
message: string | null;
|
|
575
|
+
ts: string | null;
|
|
576
|
+
startedAt: string | null;
|
|
577
|
+
}
|
|
578
|
+
interface Summary {
|
|
579
|
+
sessions: number;
|
|
580
|
+
costUsd: number;
|
|
581
|
+
tokens: number;
|
|
582
|
+
firstAt: string | null;
|
|
583
|
+
lastAt: string | null;
|
|
584
|
+
models: Array<{
|
|
585
|
+
model: string;
|
|
586
|
+
count: number;
|
|
587
|
+
}>;
|
|
588
|
+
outcomes: Array<{
|
|
589
|
+
type: string;
|
|
590
|
+
count: number;
|
|
591
|
+
}>;
|
|
592
|
+
topTools: Array<{
|
|
593
|
+
name: string;
|
|
594
|
+
calls: number;
|
|
595
|
+
errors: number;
|
|
596
|
+
}>;
|
|
597
|
+
costPerMergedPr: {
|
|
598
|
+
count: number;
|
|
599
|
+
costPerUnit: number | null;
|
|
600
|
+
};
|
|
601
|
+
/** Spend on enrichment (the "cost of running the analysis itself"). */
|
|
602
|
+
analysisCostUsd: number;
|
|
603
|
+
/** Whether LLM enrichment has run (any processor recorded an LLM model). */
|
|
604
|
+
enrichmentRan: boolean;
|
|
605
|
+
/** ISO timestamp of the most recent `analyze` run (null if never recorded). */
|
|
606
|
+
lastAnalyzedAt: string | null;
|
|
607
|
+
/** Source directories scanned, each with its own last-analyzed time (empty until an analyze runs on this schema). */
|
|
608
|
+
analyzedRoots: Array<{
|
|
609
|
+
source: string | null;
|
|
610
|
+
path: string;
|
|
611
|
+
lastAnalyzedAt: string | null;
|
|
612
|
+
}>;
|
|
613
|
+
/** Enrichment dimension distributions, empty when enrichment hasn't run. */
|
|
614
|
+
useCases: Dist[];
|
|
615
|
+
complexity: Dist[];
|
|
616
|
+
autonomy: Dist[];
|
|
617
|
+
features: {
|
|
618
|
+
total: number;
|
|
619
|
+
derived: number;
|
|
620
|
+
linked: number;
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
/** One computed insight for the Highlights digest. The client maps `kind` to the
|
|
624
|
+
* display sentence + its drill-in; the payload carries the data. */
|
|
625
|
+
interface Highlight {
|
|
626
|
+
kind: string;
|
|
627
|
+
[field: string]: unknown;
|
|
628
|
+
}
|
|
629
|
+
declare class Store {
|
|
630
|
+
private db;
|
|
631
|
+
constructor(db: DB);
|
|
632
|
+
/** Read a value from the key-value `meta` table (undefined when absent). */
|
|
633
|
+
getMeta(key: string): string | undefined;
|
|
634
|
+
/** Upsert a value into the key-value `meta` table. */
|
|
635
|
+
setMeta(key: string, value: string): void;
|
|
636
|
+
/**
|
|
637
|
+
* Stamp each source directory scanned this run with the run timestamp. Upsert,
|
|
638
|
+
* so roots a scoped re-run didn't touch keep their prior stamp — the table then
|
|
639
|
+
* answers "when was THIS directory last analyzed" per directory.
|
|
640
|
+
*/
|
|
641
|
+
recordAnalyzedRoots(roots: Array<{
|
|
642
|
+
source: string;
|
|
643
|
+
path: string;
|
|
644
|
+
}>, at: string): void;
|
|
645
|
+
/**
|
|
646
|
+
* Content hash + parse version for a session, if already ingested. Both feed
|
|
647
|
+
* the re-ingest decision: content_hash catches changed transcripts, parse
|
|
648
|
+
* version catches a smarter parser (new fields extracted from the same bytes).
|
|
649
|
+
*/
|
|
650
|
+
storedMeta(id: string): {
|
|
651
|
+
hash: string;
|
|
652
|
+
parseVersion: number;
|
|
653
|
+
} | undefined;
|
|
654
|
+
/** Set a session's resolved repo. Used to backfill repo without a full re-ingest. */
|
|
655
|
+
setSessionRepo(id: string, repo: string): void;
|
|
656
|
+
ingestSession(session: Session, costUsd: number, facts: UsageFactInput[], priceTableVersion: string, parseVersion: number): void;
|
|
657
|
+
/**
|
|
658
|
+
* Token/cost rolled up by model from `usage_facts` — the honest cost-by-model
|
|
659
|
+
* the `sessions.models` array can't give (exploding it double-counts cost).
|
|
660
|
+
*/
|
|
661
|
+
usageByModel(): Array<{
|
|
662
|
+
model: string;
|
|
663
|
+
sessions: number;
|
|
664
|
+
costUsd: number;
|
|
665
|
+
tokens: number;
|
|
666
|
+
}>;
|
|
667
|
+
/** Prior run record for cache checks. */
|
|
668
|
+
processorRun(sessionId: string, processor: string): ProcessorRunRow | undefined;
|
|
669
|
+
unresolvedArtifacts(producer: string): ArtifactInput[];
|
|
670
|
+
persistRefresh(producer: string, result: RefreshResult): void;
|
|
671
|
+
/**
|
|
672
|
+
* Persist one processor's output. Replaces this processor's prior rows for the
|
|
673
|
+
* session (provenance via `producer`); never touches other processors' or
|
|
674
|
+
* user-authored rows. Records the run for caching + analysis-cost accounting.
|
|
675
|
+
*/
|
|
676
|
+
persistResult(sessionId: string, processor: string, version: number, inputHash: string, model: string | null, result: ProcessorResult): void;
|
|
677
|
+
/**
|
|
678
|
+
* Apply an enrichment processor's hierarchy edits: REPARENT existing features.
|
|
679
|
+
* (Auto-rename is intentionally unsupported — see FeatureRevisionInput.) Skips
|
|
680
|
+
* user-authored features (locked) and any reparent that would form a cycle or
|
|
681
|
+
* self-parent. Caller runs this inside a transaction.
|
|
682
|
+
*/
|
|
683
|
+
private applyFeatureRevisions;
|
|
684
|
+
/** True if parenting `id` under `newParentId` would create a cycle (walks ancestors). */
|
|
685
|
+
private wouldCreateFeatureCycle;
|
|
686
|
+
/**
|
|
687
|
+
* The WHOLE feature hierarchy — what an enrichment processor needs to attach a
|
|
688
|
+
* session to the most specific feature, slot a new feature under the right
|
|
689
|
+
* parent, and refine the tree. The hierarchy is global and human-managed (a
|
|
690
|
+
* single epic may span repos), so the processor sees everything; repo isolation
|
|
691
|
+
* is enforced only on auto-derived *linkage* (see `repos`). `source` flags
|
|
692
|
+
* user-authored features so the processor leaves them locked.
|
|
693
|
+
*
|
|
694
|
+
* `repos` = repos associated anywhere in a feature's subtree (itself + every
|
|
695
|
+
* descendant), unioned from linked sessions and any explicit `repo` column.
|
|
696
|
+
* Empty = unscoped/global. A feature is a safe auto-link target for a session
|
|
697
|
+
* iff its `repos` is empty or already contains the session's repo.
|
|
698
|
+
*/
|
|
699
|
+
listFeatures(): Array<{
|
|
700
|
+
id: string;
|
|
701
|
+
title: string;
|
|
702
|
+
parentId: string | null;
|
|
703
|
+
source: string | null;
|
|
704
|
+
repos: string[];
|
|
705
|
+
}>;
|
|
706
|
+
/**
|
|
707
|
+
* Per-feature subtree repo set: the repos associated anywhere in a feature's
|
|
708
|
+
* subtree (itself + every descendant), unioned from each feature's explicit
|
|
709
|
+
* `repo` column and the repos of sessions linked to it. Shared by feature
|
|
710
|
+
* extraction (linkage isolation) and the dashboard (the Features repo column).
|
|
711
|
+
*/
|
|
712
|
+
private featureRepoSets;
|
|
713
|
+
/** Persist facets (intrinsic + processor-declared) so the dashboard discovers them generically. */
|
|
714
|
+
registerFacets(producer: string, specs: FacetSpec[]): void;
|
|
715
|
+
summary(): Summary;
|
|
716
|
+
/** The single most significant week-over-week move to lead the digest with:
|
|
717
|
+
* compares this window's headline KPIs (spend, success rate, session count) to
|
|
718
|
+
* the prior equal-length window and returns the biggest mover — normalized to
|
|
719
|
+
* how many times over its own "notable" bar each one is, so one metric type
|
|
720
|
+
* can't dominate just because its natural swings run larger. Null when nothing
|
|
721
|
+
* clears its bar, or the prior window's base is too thin to trust the delta.
|
|
722
|
+
* Only called for a bounded [from, to). */
|
|
723
|
+
private trendHeadline;
|
|
724
|
+
/** The windowed "reliable facts" behind the Highlights digest — most-spend
|
|
725
|
+
* shipped artifact, its stalled (not-yet-shipped) counterpart, converted spend,
|
|
726
|
+
* and the busiest source file — all scoped to [from, to) (omit for all-time) so
|
|
727
|
+
* the whole digest honors the dashboard window. */
|
|
728
|
+
private windowedFacts;
|
|
729
|
+
/** The Highlights digest: a few reliably-interesting facts plus facet-WALKED
|
|
730
|
+
* comparisons (spend concentration, outcome-rate spread) — for each, we go down
|
|
731
|
+
* an ordered facet list and keep the FIRST facet whose breakdown clears an
|
|
732
|
+
* interestingness threshold, so nothing is hardcoded to `repo` and a dominated
|
|
733
|
+
* split (e.g. one harness at 99%) is skipped. Each insight is a typed payload;
|
|
734
|
+
* the client renders the sentence + drill-in. `from`/`to` window everything;
|
|
735
|
+
* omit for all-time. */
|
|
736
|
+
highlights(from?: string, to?: string): Highlight[];
|
|
737
|
+
/** Distribution of a scalar annotation value across sessions. */
|
|
738
|
+
private scalarDist;
|
|
739
|
+
/**
|
|
740
|
+
* Windowed cost-per-shipped-artifact KPI (no window = all time). The numerator is
|
|
741
|
+
* the cost of the BLOCKS that produced each in-window completed artifact (block→PR
|
|
742
|
+
* is deterministic; block→feature is the LLM feature_runs). Blocks partition the
|
|
743
|
+
* session, so a session that also did unshipped/other work is NOT charged whole —
|
|
744
|
+
* the old unique-session approximation dissolves (handling_long_sessions P1/P2).
|
|
745
|
+
* Falls back to whole-session cost for any artifact with NO block links (a feature
|
|
746
|
+
* the model never block-linked, or pre-block data). Both paths are at usage grain
|
|
747
|
+
* and UNION-deduped, so a usage row shared across in-window artifacts counts once.
|
|
748
|
+
*/
|
|
749
|
+
costPerArtifact(kind: string, from?: string, to?: string, complexity?: string): {
|
|
750
|
+
count: number;
|
|
751
|
+
costPerUnit: number | null;
|
|
752
|
+
};
|
|
753
|
+
/**
|
|
754
|
+
* The headline KPI row for one time window. Session-grain metrics (count,
|
|
755
|
+
* spend, outcome rate) window by session start; cost-per-artifact windows by
|
|
756
|
+
* completion (see costPerArtifact). The API calls this twice — current and the
|
|
757
|
+
* same-length prior period — to derive deltas. No window = all time.
|
|
758
|
+
*/
|
|
759
|
+
kpis(from?: string, to?: string, outcomes?: string[]): KpiSnapshot;
|
|
760
|
+
/**
|
|
761
|
+
* The two decomposition curves for the cost-per-artifact section
|
|
762
|
+
* (cost_per_shipped_artifact.md). Both are PURE SUMS (0 is a real value, no
|
|
763
|
+
* attribution): burn = AI spend per bucket dated at SESSION time (with a
|
|
764
|
+
* `shippedSpend` sub-band = spend of sessions linked to a completed `kind`
|
|
765
|
+
* artifact — the gap to `spend` is in-flight/never-shipped spend); throughput
|
|
766
|
+
* = count of `kind` artifacts per bucket dated at COMPLETION. Both honor the
|
|
767
|
+
* optional window (burn by session start, throughput by completion); no window
|
|
768
|
+
* = full history. The `bucket` granularity is the caller's (day/week/month).
|
|
769
|
+
*/
|
|
770
|
+
costCurves(kind: string, bucket: Bucket, from?: string, to?: string, complexity?: string): {
|
|
771
|
+
burn: Array<{
|
|
772
|
+
bucket: string;
|
|
773
|
+
spend: number;
|
|
774
|
+
shippedSpend: number;
|
|
775
|
+
}>;
|
|
776
|
+
throughput: Array<{
|
|
777
|
+
bucket: string;
|
|
778
|
+
count: number;
|
|
779
|
+
}>;
|
|
780
|
+
/** PRs reviewed per bucket, dated at REVIEW time (the pr_reviewed outcome ts). PRs only. */
|
|
781
|
+
reviewed: Array<{
|
|
782
|
+
bucket: string;
|
|
783
|
+
count: number;
|
|
784
|
+
}>;
|
|
785
|
+
buckets: string[];
|
|
786
|
+
};
|
|
787
|
+
/**
|
|
788
|
+
* The complete ordered list of bucket labels spanning [from, to] at the given
|
|
789
|
+
* granularity. Walks one calendar day at a time in SQL and buckets each with
|
|
790
|
+
* the same expression as the data, so the labels match exactly (no JS attempt
|
|
791
|
+
* to reproduce SQLite's %W week numbering). Used to give the cost curves a
|
|
792
|
+
* continuous x-axis across the window.
|
|
793
|
+
*/
|
|
794
|
+
private bucketAxis;
|
|
795
|
+
/**
|
|
796
|
+
* The x-axis for a windowed time series: every bucket from `from` to `to`
|
|
797
|
+
* (so the chart spans the whole window and empty periods show as gaps),
|
|
798
|
+
* unioned with the data's own buckets as a safety net. No window → the data's
|
|
799
|
+
* buckets as-is (all-time). Shared by the dashboard time-series endpoints.
|
|
800
|
+
*/
|
|
801
|
+
private fullAxis;
|
|
802
|
+
/**
|
|
803
|
+
* The "burn efficiency" lens for a window: Σ session spend in the window ÷
|
|
804
|
+
* count of `kind` artifacts completed in the window. Deliberately distinct
|
|
805
|
+
* from the unit-cost KPI (whose numerator includes pre-window spend) — the doc
|
|
806
|
+
* insists both be shown so dividing the curves doesn't read as a contradiction.
|
|
807
|
+
* `throughput` here equals the KPI denominator exactly. No window = all time.
|
|
808
|
+
*/
|
|
809
|
+
costPeriod(kind: string, from?: string, to?: string, complexity?: string): {
|
|
810
|
+
burn: number;
|
|
811
|
+
throughput: number;
|
|
812
|
+
efficiency: number | null;
|
|
813
|
+
};
|
|
814
|
+
/**
|
|
815
|
+
* Spend over time, optionally split into one series per facet value — the doc's
|
|
816
|
+
* non-headline "total spend breakdown" / burn view (cost_per_shipped_artifact.md
|
|
817
|
+
* §Separate). Anchored on usage_facts so cost-by-model splits HONESTLY (each
|
|
818
|
+
* usage row attributed to its own model), not by charging a multi-model
|
|
819
|
+
* session's whole cost to each model. Spend is dated at session start (matching
|
|
820
|
+
* the burn curve). Only usage/session-grain facets are valid (the cost measure's
|
|
821
|
+
* grain guard); tool-call facets (skill) are rejected. Multi-valued facets
|
|
822
|
+
* (use_case) presence-inflate — flagged via `presenceInflated`.
|
|
823
|
+
*/
|
|
824
|
+
spendOverTime(q: SpendOverTimeQuery): SpendOverTimeResult | {
|
|
825
|
+
error: string;
|
|
826
|
+
};
|
|
827
|
+
/**
|
|
828
|
+
* Session COUNT over time, optionally split into one series per COMPOSITE label
|
|
829
|
+
* — the time-series form of the distribution cards. Each session is labeled by
|
|
830
|
+
* the sorted set of its distinct values for the dimension (e.g. <opus, haiku>)
|
|
831
|
+
* and grouped by it, so every session lands in exactly one series and the
|
|
832
|
+
* counts partition the total (honest to STACK) — no presence-inflation. The
|
|
833
|
+
* tail past top-K collapses into "Other".
|
|
834
|
+
*/
|
|
835
|
+
sessionsOverTime(q: SessionsOverTimeQuery): SessionsOverTimeResult;
|
|
836
|
+
/**
|
|
837
|
+
* Operational tool-call metrics over time. One anchor (tool_calls t JOIN
|
|
838
|
+
* sessions s, dated at session start); the `view` selects what to plot:
|
|
839
|
+
* tool_calls = COUNT(*), error_rate = SUM(is_error)/COUNT(*), skill_usage =
|
|
840
|
+
* COUNT(*) WHERE action='skill'. `by:'name'` splits by tool_calls.name (tool
|
|
841
|
+
* name in general; skill name when skills-only), ranked top-K by call volume.
|
|
842
|
+
*/
|
|
843
|
+
opsOverTime(q: OpsOverTimeQuery): OpsOverTimeResult;
|
|
844
|
+
/** Distinct tool-call names, busiest first — feeds the Ops error-rate tool filter. */
|
|
845
|
+
toolNames(): string[];
|
|
846
|
+
/**
|
|
847
|
+
* Outcome types present in the data, with the count of distinct sessions that
|
|
848
|
+
* produced each — feeds the success-rate "what counts as success" selector.
|
|
849
|
+
* (A first-class outcome-type registry, parallel to facets/measures, is a
|
|
850
|
+
* deferred follow-up; for now the selector reflects what's actually in the DB.)
|
|
851
|
+
*/
|
|
852
|
+
outcomeTypes(): Array<{
|
|
853
|
+
type: string;
|
|
854
|
+
sessions: number;
|
|
855
|
+
}>;
|
|
856
|
+
/**
|
|
857
|
+
* Session Outcome Rate over time (headline_metrics.md): the fraction of
|
|
858
|
+
* sessions — cohorted by START date — that produced any outcome in the
|
|
859
|
+
* selected set. Numerator = sessions with an outcome in `outcomes`; denominator
|
|
860
|
+
* = all sessions in the bucket. Session-level filters apply to BOTH (so the
|
|
861
|
+
* rate is honest). With `by`, returns one series per COMPOSITE label (top-K by
|
|
862
|
+
* volume, the tail collapsed into "Other"): each session is labeled by the
|
|
863
|
+
* sorted set of its distinct values for the dimension (e.g. <opus, haiku>), so
|
|
864
|
+
* every session falls in exactly one series and the bars partition the
|
|
865
|
+
* population — multi-valued sessions are counted once, not fanned out.
|
|
866
|
+
*/
|
|
867
|
+
successRate(q: SuccessRateQuery): SuccessRateResult;
|
|
868
|
+
/** Spend, session count, and shipped-PR count per time bucket. */
|
|
869
|
+
timeseries(bucket: Bucket, from?: string, to?: string): TimePoint[];
|
|
870
|
+
/** The facet registry — drives dist cards, filters, and (later) breakdowns. */
|
|
871
|
+
facetList(): FacetSpec[];
|
|
872
|
+
facet(key: string): FacetSpec | undefined;
|
|
873
|
+
/**
|
|
874
|
+
* Sessions per value of a facet — the generic dist card. The read shape is
|
|
875
|
+
* derived from (source, multi): raw column, json_each, json_extract, or a child
|
|
876
|
+
* table. This is a COUNT, so exploding a multi-valued facet is safe (a session
|
|
877
|
+
* present under two values is intended); SUM measures are a separate concern.
|
|
878
|
+
*/
|
|
879
|
+
facetDistribution(key: string): Dist[];
|
|
880
|
+
/**
|
|
881
|
+
* Compile a facet + value into a session-scoped boolean SQL fragment (alias `s`).
|
|
882
|
+
* One compiler, reused by session filters today and cohort splits later. Column
|
|
883
|
+
* identifiers and `base` are registry-defined (trusted); the value is a bound param.
|
|
884
|
+
*/
|
|
885
|
+
private facetPredicate;
|
|
886
|
+
/**
|
|
887
|
+
* Correlated subquery (alias `s`) yielding a session's DISTINCT values for a
|
|
888
|
+
* facet as one alpha-sorted, ", "-joined string — the composite label for the
|
|
889
|
+
* success-rate breakdown; empty set → NULL. Mirrors facetPredicate's source
|
|
890
|
+
* switch (identifiers/base are registry-defined and trusted, values are data).
|
|
891
|
+
*/
|
|
892
|
+
private comboExpr;
|
|
893
|
+
/** Persist measures (intrinsic + processor-declared) for the dashboard. */
|
|
894
|
+
registerMeasures(producer: string, specs: MeasureSpec[]): void;
|
|
895
|
+
measureList(): MeasureSpec[];
|
|
896
|
+
measure(key: string): MeasureSpec | undefined;
|
|
897
|
+
/**
|
|
898
|
+
* The breakdown engine: aggregate a measure, optionally grouped by a facet,
|
|
899
|
+
* with session-scoped filters. The grain guard keeps SUM/AVG honest — a facet
|
|
900
|
+
* is valid here only at the measure's grain or session-grain (the common
|
|
901
|
+
* ancestor). Finer / sibling facets need the pre-reduction (cohort) path, not
|
|
902
|
+
* built yet, and return an error rather than a silently double-counted number.
|
|
903
|
+
*/
|
|
904
|
+
breakdown(measureKey: string, byFacetKey?: string, filters?: Record<string, string>, window?: {
|
|
905
|
+
from?: string;
|
|
906
|
+
to?: string;
|
|
907
|
+
}, toolNames?: string[]): {
|
|
908
|
+
rows: Array<{
|
|
909
|
+
bucket: string | null;
|
|
910
|
+
value: number;
|
|
911
|
+
}>;
|
|
912
|
+
total: number;
|
|
913
|
+
} | {
|
|
914
|
+
error: string;
|
|
915
|
+
};
|
|
916
|
+
/**
|
|
917
|
+
* Every failed tool call of one error category — the occurrence list behind the
|
|
918
|
+
* "Errors by category" drill-down. Newest session first; `idx` is the tool call's
|
|
919
|
+
* position in its session, which the transcript anchors as `txerr-<idx>` so a row
|
|
920
|
+
* deep-links to that exact error block. Windowed like breakdown. Capped at 50; the
|
|
921
|
+
* widget shows the true total (the bar count) with a "+N more" note past the cap.
|
|
922
|
+
*/
|
|
923
|
+
errorOccurrences(category: string, window?: {
|
|
924
|
+
from?: string;
|
|
925
|
+
to?: string;
|
|
926
|
+
}, toolNames?: string[]): ErrorOccurrence[];
|
|
927
|
+
/** Filtered session list. Filter VALUES are bound params; keys are hardcoded. */
|
|
928
|
+
sessionList(f: SessionFilter): SessionListItem[];
|
|
929
|
+
/** Full detail for one session, including a viewer-ready transcript from the blob. */
|
|
930
|
+
/** Per-block labels (use_case / PR / feature) for the transcript filter bar. */
|
|
931
|
+
private blockLabels;
|
|
932
|
+
sessionDetail(id: string): SessionDetail | null;
|
|
933
|
+
/**
|
|
934
|
+
* The session's value for every facet flagged for the `detail` role — the
|
|
935
|
+
* registry-driven metadata list the drawer renders. Keeps the drawer from
|
|
936
|
+
* hardcoding which dimensions exist: a new processor facet with a `detail`
|
|
937
|
+
* role appears here with no store or client edits. Ordered by registration
|
|
938
|
+
* (intrinsic first, then processors) rather than alphabetically.
|
|
939
|
+
*/
|
|
940
|
+
facetValues(id: string): FacetValue[];
|
|
941
|
+
/** Resolve one facet's value(s) for a session, branching on (source, multi) like facetPredicate. */
|
|
942
|
+
private facetValueFor;
|
|
943
|
+
/** Gunzip + parse a session's stored blob (the full normalized Session), or null. */
|
|
944
|
+
private loadSession;
|
|
945
|
+
/**
|
|
946
|
+
* The session's successful file edits as a flat, CHRONOLOGICAL list — the
|
|
947
|
+
* Files-changed view. Each carries its raw before/after (Edit), full content
|
|
948
|
+
* (Write), or hunks (MultiEdit), plus the transcript turn it happened in and
|
|
949
|
+
* the preceding (non-synthetic) user turn, so the UI can group by file or by
|
|
950
|
+
* prompt and link each change to its intent. Rejected / not-yet-read edits are
|
|
951
|
+
* excluded (they changed nothing). Reconstructs from the blob at read time.
|
|
952
|
+
*/
|
|
953
|
+
fileChanges(id: string): FileEdit[];
|
|
954
|
+
/**
|
|
955
|
+
* Shippable artifacts (PRs + features) with session count and fully-loaded
|
|
956
|
+
* cost. Cost sums the UNIQUE sessions linked to each artifact; a session
|
|
957
|
+
* spanning several artifacts is counted in each, so the column can exceed
|
|
958
|
+
* total spend (per-artifact attribution, by design).
|
|
959
|
+
*/
|
|
960
|
+
artifactList(kind?: string, complexity?: string, from?: string, to?: string, shippedOnly?: boolean): ArtifactListItem[];
|
|
961
|
+
/**
|
|
962
|
+
* Per-feature last session time: the most recent start of any session linked to
|
|
963
|
+
* the feature OR any descendant (subtree max), so a parent reflects the latest
|
|
964
|
+
* activity beneath it. Null when nothing under it has a dated session.
|
|
965
|
+
*/
|
|
966
|
+
private featureLastSession;
|
|
967
|
+
/**
|
|
968
|
+
* Per-feature cost rolled up over the feature hierarchy, for the hierarchical
|
|
969
|
+
* cost-breakdown charts. `ownCost` is the spend attributed DIRECTLY to a feature
|
|
970
|
+
* (the same block-attributed-with-whole-session-fallback cost as the artifactList
|
|
971
|
+
* column); `subtreeCost` adds every descendant's own cost, so a parent epic
|
|
972
|
+
* reflects the total invested beneath it (subtreeCost − Σ children.subtreeCost =
|
|
973
|
+
* ownCost). All-time, not windowed: total investment per feature, matching the
|
|
974
|
+
* artifactList cost semantics. Cycle-safe + memoized, mirroring featureLastSession.
|
|
975
|
+
* parentId is normalized to null when it doesn't point at another feature, so the
|
|
976
|
+
* client can treat such rows as roots.
|
|
977
|
+
*/
|
|
978
|
+
featureCostTree(complexity?: string, from?: string, to?: string): Array<{
|
|
979
|
+
id: string;
|
|
980
|
+
title: string | null;
|
|
981
|
+
parentId: string | null;
|
|
982
|
+
ownCost: number;
|
|
983
|
+
subtreeCost: number;
|
|
984
|
+
}>;
|
|
985
|
+
/**
|
|
986
|
+
* Build a SQL condition + params for artifact text search that handles plain
|
|
987
|
+
* terms, `#N` (PR number with hash prefix), and `repo#N` (repo + number).
|
|
988
|
+
*/
|
|
989
|
+
private artifactSearchCond;
|
|
990
|
+
/**
|
|
991
|
+
* Typeahead suggestions for the session-list artifact search. Only artifacts
|
|
992
|
+
* actually linked to a session (so a pick yields results), matched on the same
|
|
993
|
+
* columns the filter uses (ident/title/external_id/repo). `value` is what to
|
|
994
|
+
* put in the filter input (feature→title, pr→external_id|ident, file→path);
|
|
995
|
+
* `label` is for display. Features/PRs rank above the many file rows.
|
|
996
|
+
*/
|
|
997
|
+
suggestArtifacts(q: string, kind: string | undefined, limit?: number): Array<{
|
|
998
|
+
kind: string;
|
|
999
|
+
value: string;
|
|
1000
|
+
label: string;
|
|
1001
|
+
}>;
|
|
1002
|
+
/** Create a user-authored feature (source='user' — never clobbered by analyze). */
|
|
1003
|
+
createFeature(title: string, parentId?: string, complexity?: number): {
|
|
1004
|
+
id: string;
|
|
1005
|
+
};
|
|
1006
|
+
/** Mark complete/reopen, rename, reparent, or set complexity of a feature. */
|
|
1007
|
+
updateFeature(id: string, patch: {
|
|
1008
|
+
completed?: boolean;
|
|
1009
|
+
parentId?: string | null;
|
|
1010
|
+
title?: string;
|
|
1011
|
+
complexity?: number | null;
|
|
1012
|
+
}): boolean;
|
|
1013
|
+
/** Delete a feature; promote its children to its parent and remove its links. */
|
|
1014
|
+
deleteFeature(id: string): boolean;
|
|
1015
|
+
/**
|
|
1016
|
+
* Delete machine-derived artifacts no longer referenced by any session or
|
|
1017
|
+
* link (e.g. PRs whose false-positive links were removed on re-derivation).
|
|
1018
|
+
* Never touches user-authored artifacts.
|
|
1019
|
+
*/
|
|
1020
|
+
/**
|
|
1021
|
+
* Delete sessions whose parse_version is below the current version for their
|
|
1022
|
+
* source — these are sessions the parser now returns null for (e.g. synthetic-only).
|
|
1023
|
+
*/
|
|
1024
|
+
pruneStaleSessionsByVersion(versionBySource: Map<string, number>): number;
|
|
1025
|
+
pruneOrphanArtifacts(): number;
|
|
1026
|
+
/** Link an existing artifact to a session (user-authored, never overwritten by processors). Clears any prior rejection tombstone. */
|
|
1027
|
+
addSessionLink(sessionId: string, artifactId: string, role?: SessionArtifactRole): boolean;
|
|
1028
|
+
/** Reject a session→artifact link: delete existing rows and insert a tombstone so re-enrichment won't recreate it. */
|
|
1029
|
+
rejectSessionLink(sessionId: string, artifactId: string): boolean;
|
|
1030
|
+
/**
|
|
1031
|
+
* Mark every processor run for a session stale so the next analyze re-runs them.
|
|
1032
|
+
*
|
|
1033
|
+
* Called on any user link/unlink. The user-linked set feeds enrichment, and
|
|
1034
|
+
* unlink deletes block_artifacts across producers, so the deterministic
|
|
1035
|
+
* processors (outcomes-git) must re-derive too — enrich-only invalidation
|
|
1036
|
+
* would leave their wiped rows unregenerated. We flag rather than delete so
|
|
1037
|
+
* the row's cost_usd/tokens survive; persistResult resets the flag on the
|
|
1038
|
+
* next successful run. Cost: at most one extra analyze per explicit user
|
|
1039
|
+
* action (not on every analyze).
|
|
1040
|
+
*/
|
|
1041
|
+
private invalidateSessionProcessors;
|
|
1042
|
+
/**
|
|
1043
|
+
* The single writer for user-created session links. Every dashboard link path
|
|
1044
|
+
* funnels through here so the write and its cache invalidation can never drift
|
|
1045
|
+
* apart (that drift is how add-pr/create-feature previously skipped it). Call
|
|
1046
|
+
* within the caller's own transaction so the link and invalidation commit atomically.
|
|
1047
|
+
*/
|
|
1048
|
+
private linkUserArtifact;
|
|
1049
|
+
/** Titles of features the user rejected for this session (for LLM prompt context). */
|
|
1050
|
+
rejectedFeatureTitles(sessionId: string): string[];
|
|
1051
|
+
/** Blocks already attributed to PRs for a session (deterministic, from outcomes-git). */
|
|
1052
|
+
prBlockAttributions(sessionId: string): Array<{
|
|
1053
|
+
blockIdx: number;
|
|
1054
|
+
artifactId: string;
|
|
1055
|
+
title: string | null;
|
|
1056
|
+
}>;
|
|
1057
|
+
/** All user-linked PRs/features for a session, with a flag indicating deterministic block ownership. */
|
|
1058
|
+
userLinkedArtifactsAll(sessionId: string): Array<{
|
|
1059
|
+
artifactId: string;
|
|
1060
|
+
kind: 'pr' | 'feature';
|
|
1061
|
+
title: string | null;
|
|
1062
|
+
ident: string | null;
|
|
1063
|
+
hasNonEnrichBlocks: boolean;
|
|
1064
|
+
}>;
|
|
1065
|
+
/** Create a new feature and link it to a session in one transaction. */
|
|
1066
|
+
createAndLinkFeature(sessionId: string, title: string, parentId?: string): {
|
|
1067
|
+
id: string;
|
|
1068
|
+
} | null;
|
|
1069
|
+
/** Upsert a PR artifact and link it to a session. */
|
|
1070
|
+
upsertAndLinkPr(sessionId: string, repo: string, prNumber: string, meta?: {
|
|
1071
|
+
title?: string;
|
|
1072
|
+
status?: string;
|
|
1073
|
+
externalId?: string;
|
|
1074
|
+
}): {
|
|
1075
|
+
id: string;
|
|
1076
|
+
} | null;
|
|
1077
|
+
/** Typeahead for linkable artifacts (excludes those already linked to the session). */
|
|
1078
|
+
suggestLinkableArtifacts(sessionId: string, q: string, kind?: string, limit?: number): Array<{
|
|
1079
|
+
id: string;
|
|
1080
|
+
kind: string;
|
|
1081
|
+
label: string;
|
|
1082
|
+
}>;
|
|
1083
|
+
close(): void;
|
|
1084
|
+
}
|
|
1085
|
+
interface ArtifactListItem {
|
|
1086
|
+
id: string;
|
|
1087
|
+
kind: string;
|
|
1088
|
+
title: string | null;
|
|
1089
|
+
ident: string | null;
|
|
1090
|
+
repo: string | null;
|
|
1091
|
+
/** Repos this artifact spans — a feature's full subtree union; one entry for a PR. */
|
|
1092
|
+
repos: string[];
|
|
1093
|
+
/** Most recent linked session start; for a feature, the max across its subtree. Null if none. */
|
|
1094
|
+
lastSessionAt: string | null;
|
|
1095
|
+
status: string | null;
|
|
1096
|
+
source: string | null;
|
|
1097
|
+
externalId: string | null;
|
|
1098
|
+
/** PR creation time (from `gh`); null when not captured (offline / pre-backfill). */
|
|
1099
|
+
createdAt: string | null;
|
|
1100
|
+
completedAt: string | null;
|
|
1101
|
+
parentId: string | null;
|
|
1102
|
+
complexity: number | null;
|
|
1103
|
+
complexityBasis: string | null;
|
|
1104
|
+
sessions: number;
|
|
1105
|
+
costUsd: number;
|
|
1106
|
+
/** Max content-match AI-attribution fraction across the artifact's session links (0–1);
|
|
1107
|
+
* null when no content-match link exists (e.g. an explicit-only PR). PRs only. */
|
|
1108
|
+
aiPct: number | null;
|
|
1109
|
+
}
|
|
1110
|
+
/** One window's worth of headline KPIs (see Store.kpis). */
|
|
1111
|
+
interface KpiSnapshot {
|
|
1112
|
+
sessions: number;
|
|
1113
|
+
totalSpend: number;
|
|
1114
|
+
/** Fraction of sessions judged success; null when the window has no sessions. */
|
|
1115
|
+
successRate: number | null;
|
|
1116
|
+
/** Tool-call error rate (fraction); null when the window has no tool calls. */
|
|
1117
|
+
errorRate: number | null;
|
|
1118
|
+
costPerFeature: {
|
|
1119
|
+
count: number;
|
|
1120
|
+
costPerUnit: number | null;
|
|
1121
|
+
};
|
|
1122
|
+
costPerPr: {
|
|
1123
|
+
count: number;
|
|
1124
|
+
costPerUnit: number | null;
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
/** One bucket of a success-rate series: the cohort's numerator/denominator/rate. */
|
|
1128
|
+
interface RatePoint {
|
|
1129
|
+
bucket: string;
|
|
1130
|
+
num: number;
|
|
1131
|
+
denom: number;
|
|
1132
|
+
/** Total session cost (USD) in the bucket — feeds the per-value cost table
|
|
1133
|
+
* (total spend, and $/session = spend/denom). Summed over the SAME population
|
|
1134
|
+
* as `denom` (all sessions, not just successful ones), so spend/denom is an
|
|
1135
|
+
* honest avg cost per session. */
|
|
1136
|
+
spend: number;
|
|
1137
|
+
/** num/denom, or null when the bucket has no sessions (drawn as a gap). */
|
|
1138
|
+
rate: number | null;
|
|
1139
|
+
}
|
|
1140
|
+
/** A success-rate line: per-bucket points plus the windowed totals/rate. */
|
|
1141
|
+
interface RateSeries {
|
|
1142
|
+
key: string;
|
|
1143
|
+
points: RatePoint[];
|
|
1144
|
+
num: number;
|
|
1145
|
+
denom: number;
|
|
1146
|
+
/** Window-total session cost (USD); spend/denom = avg cost per session. */
|
|
1147
|
+
spend: number;
|
|
1148
|
+
rate: number | null;
|
|
1149
|
+
}
|
|
1150
|
+
interface SuccessRateQuery {
|
|
1151
|
+
/** Outcome types counting as success (numerator). Empty → ['session_success']. */
|
|
1152
|
+
outcomes: string[];
|
|
1153
|
+
bucket: Bucket;
|
|
1154
|
+
/** Facet key to split into one series per value (top-K by volume). */
|
|
1155
|
+
by?: string;
|
|
1156
|
+
from?: string;
|
|
1157
|
+
to?: string;
|
|
1158
|
+
/** Session-level facet filters (multi-value OR within a facet), applied to
|
|
1159
|
+
* numerator and denominator alike. */
|
|
1160
|
+
filters?: Record<string, string[]>;
|
|
1161
|
+
topK?: number;
|
|
1162
|
+
}
|
|
1163
|
+
interface SuccessRateResult {
|
|
1164
|
+
outcomes: string[];
|
|
1165
|
+
bucket: Bucket;
|
|
1166
|
+
/** The x-axis: every bucket label the overall line spans. */
|
|
1167
|
+
buckets: string[];
|
|
1168
|
+
overall: RateSeries;
|
|
1169
|
+
/** Present when `by` is set. */
|
|
1170
|
+
series?: RateSeries[];
|
|
1171
|
+
/** Set when more facet values existed than were drawn. */
|
|
1172
|
+
truncated?: {
|
|
1173
|
+
shown: number;
|
|
1174
|
+
total: number;
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
/** One bucket of an operational (tool-call) series: count + error split. */
|
|
1178
|
+
interface OpsPoint {
|
|
1179
|
+
bucket: string;
|
|
1180
|
+
/** The plotted metric: call count (count views) or error rate (rate view), null if no calls. */
|
|
1181
|
+
value: number | null;
|
|
1182
|
+
calls: number;
|
|
1183
|
+
errors: number;
|
|
1184
|
+
}
|
|
1185
|
+
interface OpsSeries {
|
|
1186
|
+
key: string;
|
|
1187
|
+
/** Display label when the key isn't already human-readable (error categories). */
|
|
1188
|
+
label?: string;
|
|
1189
|
+
points: OpsPoint[];
|
|
1190
|
+
/** Total count, or overall error rate, depending on the view. */
|
|
1191
|
+
total: number | null;
|
|
1192
|
+
/** Call volume — used to rank series (top-K most-used tools/skills). */
|
|
1193
|
+
calls: number;
|
|
1194
|
+
}
|
|
1195
|
+
interface OpsOverTimeQuery {
|
|
1196
|
+
/** tool_calls = count all; error_rate = AVG(is_error); skill_usage = count where action='skill'. */
|
|
1197
|
+
view: 'tool_calls' | 'error_rate' | 'skill_usage';
|
|
1198
|
+
bucket: Bucket;
|
|
1199
|
+
/** 'name' splits by tool name; 'error_category' decomposes the rate by category. */
|
|
1200
|
+
by?: string;
|
|
1201
|
+
from?: string;
|
|
1202
|
+
to?: string;
|
|
1203
|
+
/** Generic session-level facet filters (harness/repo/model); unused by the Ops UI today. */
|
|
1204
|
+
filters?: Record<string, string[]>;
|
|
1205
|
+
/** Row-level scope: only count calls of these tool names (denominator + numerator). */
|
|
1206
|
+
toolNames?: string[];
|
|
1207
|
+
/** Row-level scope: only these categories count as errors (numerator only). */
|
|
1208
|
+
errorCategories?: string[];
|
|
1209
|
+
topK?: number;
|
|
1210
|
+
}
|
|
1211
|
+
interface OpsOverTimeResult {
|
|
1212
|
+
view: string;
|
|
1213
|
+
bucket: Bucket;
|
|
1214
|
+
/** The active breakdown dimension, echoed back so the client can label series. */
|
|
1215
|
+
by?: string;
|
|
1216
|
+
buckets: string[];
|
|
1217
|
+
overall: {
|
|
1218
|
+
points: OpsPoint[];
|
|
1219
|
+
total: number | null;
|
|
1220
|
+
};
|
|
1221
|
+
series?: OpsSeries[];
|
|
1222
|
+
truncated?: {
|
|
1223
|
+
shown: number;
|
|
1224
|
+
total: number;
|
|
1225
|
+
};
|
|
1226
|
+
format: 'int' | 'pct';
|
|
1227
|
+
}
|
|
1228
|
+
/** One bucket of a session-count series. */
|
|
1229
|
+
interface CountPoint {
|
|
1230
|
+
bucket: string;
|
|
1231
|
+
count: number;
|
|
1232
|
+
}
|
|
1233
|
+
interface CountSeries {
|
|
1234
|
+
key: string;
|
|
1235
|
+
points: CountPoint[];
|
|
1236
|
+
total: number;
|
|
1237
|
+
}
|
|
1238
|
+
interface SessionsOverTimeQuery {
|
|
1239
|
+
bucket: Bucket;
|
|
1240
|
+
by?: string;
|
|
1241
|
+
from?: string;
|
|
1242
|
+
to?: string;
|
|
1243
|
+
filters?: Record<string, string[]>;
|
|
1244
|
+
topK?: number;
|
|
1245
|
+
}
|
|
1246
|
+
interface SessionsOverTimeResult {
|
|
1247
|
+
bucket: Bucket;
|
|
1248
|
+
buckets: string[];
|
|
1249
|
+
overall: {
|
|
1250
|
+
points: CountPoint[];
|
|
1251
|
+
total: number;
|
|
1252
|
+
};
|
|
1253
|
+
series?: CountSeries[];
|
|
1254
|
+
truncated?: {
|
|
1255
|
+
shown: number;
|
|
1256
|
+
total: number;
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
/** One bucket of a spend series. */
|
|
1260
|
+
interface SpendPoint {
|
|
1261
|
+
bucket: string;
|
|
1262
|
+
spend: number;
|
|
1263
|
+
}
|
|
1264
|
+
/** A spend line: per-bucket points plus the total over the range. */
|
|
1265
|
+
interface SpendSeries {
|
|
1266
|
+
key: string;
|
|
1267
|
+
points: SpendPoint[];
|
|
1268
|
+
total: number;
|
|
1269
|
+
}
|
|
1270
|
+
interface SpendOverTimeQuery {
|
|
1271
|
+
bucket: Bucket;
|
|
1272
|
+
/** Facet key to split into one series per value (top-K by total spend). */
|
|
1273
|
+
by?: string;
|
|
1274
|
+
from?: string;
|
|
1275
|
+
to?: string;
|
|
1276
|
+
filters?: Record<string, string[]>;
|
|
1277
|
+
topK?: number;
|
|
1278
|
+
}
|
|
1279
|
+
interface SpendOverTimeResult {
|
|
1280
|
+
bucket: Bucket;
|
|
1281
|
+
buckets: string[];
|
|
1282
|
+
overall: {
|
|
1283
|
+
points: SpendPoint[];
|
|
1284
|
+
total: number;
|
|
1285
|
+
};
|
|
1286
|
+
series?: SpendSeries[];
|
|
1287
|
+
truncated?: {
|
|
1288
|
+
shown: number;
|
|
1289
|
+
total: number;
|
|
1290
|
+
};
|
|
1291
|
+
/** True when the breakdown facet is multi-valued, so series sum past overall. */
|
|
1292
|
+
presenceInflated?: boolean;
|
|
1293
|
+
}
|
|
1294
|
+
type Bucket = 'day' | 'week' | 'month';
|
|
1295
|
+
interface TimePoint {
|
|
1296
|
+
bucket: string;
|
|
1297
|
+
sessions: number;
|
|
1298
|
+
spend: number;
|
|
1299
|
+
shipped: number;
|
|
1300
|
+
}
|
|
1301
|
+
interface SessionFilter {
|
|
1302
|
+
/** facetKey -> value; compiled to predicates via the facet registry. */
|
|
1303
|
+
facets?: Record<string, string>;
|
|
1304
|
+
q?: string;
|
|
1305
|
+
/** Match sessions linked to an artifact whose path/PR/url/repo/feature-title matches. */
|
|
1306
|
+
artifact?: string;
|
|
1307
|
+
/** Restrict the artifact match to a kind: file | pr | feature | ticket | commit. */
|
|
1308
|
+
artifactKind?: string;
|
|
1309
|
+
/** Window on session start (ISO); inclusive lower / exclusive upper bound. */
|
|
1310
|
+
from?: string;
|
|
1311
|
+
to?: string;
|
|
1312
|
+
/** Match sessions that produced ANY of these outcome types (OR). */
|
|
1313
|
+
outcomeTypes?: string[];
|
|
1314
|
+
limit?: number;
|
|
1315
|
+
}
|
|
1316
|
+
interface SessionListItem {
|
|
1317
|
+
id: string;
|
|
1318
|
+
title: string;
|
|
1319
|
+
startedAt: string | null;
|
|
1320
|
+
costUsd: number;
|
|
1321
|
+
models: string[];
|
|
1322
|
+
complexity: string | null;
|
|
1323
|
+
useCase: string[];
|
|
1324
|
+
intent: string | null;
|
|
1325
|
+
/** Distinct outcome types this session produced (e.g. pr_merged, session_success). */
|
|
1326
|
+
outcomes: string[];
|
|
1327
|
+
}
|
|
1328
|
+
interface TranscriptTool {
|
|
1329
|
+
name: string;
|
|
1330
|
+
action: string;
|
|
1331
|
+
ok: boolean;
|
|
1332
|
+
/** This call's index in session.toolCalls — the transcript anchors a failed call as `txerr-<idx>`. */
|
|
1333
|
+
idx?: number;
|
|
1334
|
+
target?: string;
|
|
1335
|
+
/** Full tool input rendered as displayable text (key field or JSON). */
|
|
1336
|
+
command?: string;
|
|
1337
|
+
/** Tool output/result text (clipped to OUTPUT_MAX, with an explicit tail notice if cut). */
|
|
1338
|
+
output?: string;
|
|
1339
|
+
/** For Edit/Write: old→new hunks for inline diff rendering. */
|
|
1340
|
+
hunks?: {
|
|
1341
|
+
del: string;
|
|
1342
|
+
ins: string;
|
|
1343
|
+
}[];
|
|
1344
|
+
error?: string;
|
|
1345
|
+
/** For a subagent-spawning call (`Task`/`Agent`), the agentId it links to. */
|
|
1346
|
+
agentId?: string;
|
|
1347
|
+
}
|
|
1348
|
+
interface TranscriptTurn {
|
|
1349
|
+
role: 'user' | 'assistant' | 'system';
|
|
1350
|
+
ts?: string;
|
|
1351
|
+
sidechain: boolean;
|
|
1352
|
+
/** Which subagent emitted this turn; undefined for main-thread turns. */
|
|
1353
|
+
agentId?: string;
|
|
1354
|
+
/** Main-thread sequence index (undefined for sidechain turns). */
|
|
1355
|
+
seq?: number;
|
|
1356
|
+
/** Block this turn belongs to (handling_long_sessions); undefined if unmapped. */
|
|
1357
|
+
blockIdx?: number;
|
|
1358
|
+
text: string;
|
|
1359
|
+
tools: TranscriptTool[];
|
|
1360
|
+
}
|
|
1361
|
+
/** One subagent's identity, for the transcript's per-subagent tab + spawn link. */
|
|
1362
|
+
interface SubagentInfo {
|
|
1363
|
+
agentId: string;
|
|
1364
|
+
agentType?: string;
|
|
1365
|
+
description?: string;
|
|
1366
|
+
/** tool_use id of the spawning call in the parent thread (absent for workflow subagents). */
|
|
1367
|
+
toolUseId?: string;
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* A session's viewer-ready transcript: one flat, globally-indexed list of turns
|
|
1371
|
+
* (each tagged with its `agentId`, so the client can split the main thread from
|
|
1372
|
+
* each subagent into its own tab) plus the subagent roster.
|
|
1373
|
+
*/
|
|
1374
|
+
/** One block's identity + labels, for the transcript's filter bar. */
|
|
1375
|
+
interface TranscriptBlock {
|
|
1376
|
+
idx: number;
|
|
1377
|
+
useCase?: string | null;
|
|
1378
|
+
pr?: {
|
|
1379
|
+
ident: string;
|
|
1380
|
+
title?: string;
|
|
1381
|
+
} | null;
|
|
1382
|
+
feature?: {
|
|
1383
|
+
id: string;
|
|
1384
|
+
title?: string;
|
|
1385
|
+
} | null;
|
|
1386
|
+
}
|
|
1387
|
+
interface Transcript {
|
|
1388
|
+
turns: TranscriptTurn[];
|
|
1389
|
+
subagents: SubagentInfo[];
|
|
1390
|
+
/** Block partition + per-block labels, for filtering the transcript by PR / feature / use-case. */
|
|
1391
|
+
blocks: TranscriptBlock[];
|
|
1392
|
+
}
|
|
1393
|
+
/** A facet's resolved value for one session — the registry-driven detail row. */
|
|
1394
|
+
interface FacetValue {
|
|
1395
|
+
key: string;
|
|
1396
|
+
label: string;
|
|
1397
|
+
type: FacetType;
|
|
1398
|
+
/** scalar, list (multi / child-grain facets), or null when the session has none. */
|
|
1399
|
+
value: string | string[] | null;
|
|
1400
|
+
}
|
|
1401
|
+
interface SessionDetail {
|
|
1402
|
+
session: Record<string, unknown>;
|
|
1403
|
+
annotations: Record<string, unknown>;
|
|
1404
|
+
outcomes: Array<{
|
|
1405
|
+
type: string;
|
|
1406
|
+
artifactId: string | null;
|
|
1407
|
+
}>;
|
|
1408
|
+
artifacts: Array<Record<string, unknown>>;
|
|
1409
|
+
facets: FacetValue[];
|
|
1410
|
+
transcript: Transcript;
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* One successful file write in the session — a before/after (Edit), full content
|
|
1414
|
+
* (Write), or hunks (MultiEdit). Returned as a flat, chronological list so the
|
|
1415
|
+
* client can group it either by file or by prompt.
|
|
1416
|
+
*/
|
|
1417
|
+
interface FileEdit {
|
|
1418
|
+
path: string;
|
|
1419
|
+
op: 'edit' | 'multiedit' | 'write';
|
|
1420
|
+
hunks: Array<{
|
|
1421
|
+
del: string;
|
|
1422
|
+
ins: string;
|
|
1423
|
+
}>;
|
|
1424
|
+
ts?: string;
|
|
1425
|
+
/** Index into the transcript turns of the assistant turn that made the edit. */
|
|
1426
|
+
turn: number;
|
|
1427
|
+
/** Index of the preceding (non-synthetic) user turn — the prompting intent, or -1. */
|
|
1428
|
+
userTurn: number;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/** Topologically order processors so a processor runs after everything in `requires`. */
|
|
1432
|
+
declare function orderProcessors(procs: Processor[]): Processor[];
|
|
1433
|
+
interface RunOptions {
|
|
1434
|
+
session: Session;
|
|
1435
|
+
processors: Processor[];
|
|
1436
|
+
store: Store;
|
|
1437
|
+
log: Logger;
|
|
1438
|
+
llmEnabled: boolean;
|
|
1439
|
+
llmModel: string | null;
|
|
1440
|
+
llm: LlmClient | null;
|
|
1441
|
+
sh: ProcessorContext['sh'];
|
|
1442
|
+
}
|
|
1443
|
+
interface RunResult {
|
|
1444
|
+
costUsd: number;
|
|
1445
|
+
}
|
|
1446
|
+
/** Run every applicable processor for one session, honoring deps + the cache. */
|
|
1447
|
+
declare function runProcessors(opts: RunOptions): Promise<RunResult>;
|
|
1448
|
+
|
|
1449
|
+
/** Resolved runtime configuration for a single invocation. */
|
|
1450
|
+
interface TuneloopConfig {
|
|
1451
|
+
/** Directory holding the SQLite store and other local state. */
|
|
1452
|
+
dataDir: string;
|
|
1453
|
+
dbPath: string;
|
|
1454
|
+
/** LLM provider for enrichment (BYO key), or null when not configured. */
|
|
1455
|
+
llm: {
|
|
1456
|
+
provider: string;
|
|
1457
|
+
model: string;
|
|
1458
|
+
apiKey: string;
|
|
1459
|
+
baseURL?: string;
|
|
1460
|
+
} | null;
|
|
1461
|
+
}
|
|
1462
|
+
/** Non-secret LLM knobs settable via CLI flags; they override env. The API key is env-only. */
|
|
1463
|
+
interface LlmOverrides {
|
|
1464
|
+
provider?: string;
|
|
1465
|
+
model?: string;
|
|
1466
|
+
baseURL?: string;
|
|
1467
|
+
}
|
|
1468
|
+
declare function loadConfig(opts?: {
|
|
1469
|
+
dataDir?: string;
|
|
1470
|
+
db?: string;
|
|
1471
|
+
llm?: LlmOverrides;
|
|
1472
|
+
}): TuneloopConfig;
|
|
1473
|
+
|
|
1474
|
+
interface AnalyzeOptions {
|
|
1475
|
+
dirs?: string[];
|
|
1476
|
+
/** `--source` entries: a harness name, optionally `name=dir` to override its roots. */
|
|
1477
|
+
sources?: string[];
|
|
1478
|
+
db?: string;
|
|
1479
|
+
verbose?: boolean;
|
|
1480
|
+
/** Cap the number of sessions processed — handy for a cheap enrichment test. */
|
|
1481
|
+
limit?: number;
|
|
1482
|
+
/** Non-secret LLM flag overrides (provider/model/base-url); the key stays env-only. */
|
|
1483
|
+
llm?: LlmOverrides;
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Discover sessions → parse (adapter) → ingest changed ones → run processors
|
|
1487
|
+
* (cache-aware) → print a summary. Writes to the store only; the dashboard,
|
|
1488
|
+
* `search`, and `observe` all read it.
|
|
1489
|
+
*/
|
|
1490
|
+
declare function analyze(opts: AnalyzeOptions): Promise<void>;
|
|
1491
|
+
|
|
1492
|
+
interface ServeOptions {
|
|
1493
|
+
db?: string;
|
|
1494
|
+
port?: number;
|
|
1495
|
+
open?: boolean;
|
|
1496
|
+
}
|
|
1497
|
+
/** Serve the dashboard over an already-analyzed store. Reads only; Ctrl+C stops. */
|
|
1498
|
+
declare function serve(opts: ServeOptions): Promise<void>;
|
|
1499
|
+
|
|
1500
|
+
type ShFn = (cmd: string, args: string[]) => Promise<ShResult | null>;
|
|
1501
|
+
/**
|
|
1502
|
+
* JSON API + dashboard SPA over the analyzed store. Reads are queries at request
|
|
1503
|
+
* time; POST endpoints write user curation only (features + session↔artifact
|
|
1504
|
+
* links), stamped user-authored so `analyze` never clobbers them. Deriving facts
|
|
1505
|
+
* from transcripts stays in the `analyze` path.
|
|
1506
|
+
*/
|
|
1507
|
+
declare function createDashboardServer(store: Store, dbPath: string, sh?: ShFn): Server;
|
|
1508
|
+
|
|
1509
|
+
interface QueryOptions {
|
|
1510
|
+
/** Stop after this many rows (default 1000). */
|
|
1511
|
+
maxRows?: number;
|
|
1512
|
+
/** Stop once accumulated JSON size exceeds this (default 5MB). */
|
|
1513
|
+
maxBytes?: number;
|
|
1514
|
+
/** Stop if row production exceeds this wall-clock budget (default 5s). */
|
|
1515
|
+
timeoutMs?: number;
|
|
1516
|
+
/** Positional (?) or named (:name) bind parameters. */
|
|
1517
|
+
params?: unknown[] | Record<string, unknown>;
|
|
1518
|
+
}
|
|
1519
|
+
interface QueryResult {
|
|
1520
|
+
columns: string[];
|
|
1521
|
+
rows: Record<string, unknown>[];
|
|
1522
|
+
rowCount: number;
|
|
1523
|
+
/** Which cap ended the read early, or null if the full result fit. */
|
|
1524
|
+
truncated: 'rows' | 'bytes' | 'time' | null;
|
|
1525
|
+
elapsedMs: number;
|
|
1526
|
+
}
|
|
1527
|
+
/** Rejected before touching the DB: shape violations the SQL engine wouldn't flag. */
|
|
1528
|
+
declare class QueryError extends Error {
|
|
1529
|
+
constructor(message: string);
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Run a single read-only SELECT against the store at `dbPath`. Opens and closes
|
|
1533
|
+
* its own connection every call — cheap, and keeps this fully independent of any
|
|
1534
|
+
* live Store/serve handle. Throws {@link QueryError} for guard violations and
|
|
1535
|
+
* SQLite's own errors (syntax, unknown column) for genuine SQL mistakes.
|
|
1536
|
+
*/
|
|
1537
|
+
declare function runQuery(dbPath: string, sql: string, opts?: QueryOptions): QueryResult;
|
|
1538
|
+
interface SchemaTable {
|
|
1539
|
+
name: string;
|
|
1540
|
+
/** The CREATE statement as SQLite normalized it — guaranteed in sync with the store. */
|
|
1541
|
+
sql: string;
|
|
1542
|
+
}
|
|
1543
|
+
/** What's actually in the store — the extent, not the shape. Derived from `sessions`. */
|
|
1544
|
+
interface Coverage {
|
|
1545
|
+
sessions: number;
|
|
1546
|
+
firstAt: string | null;
|
|
1547
|
+
lastAt: string | null;
|
|
1548
|
+
lastAnalyzedAt: string | null;
|
|
1549
|
+
sources: {
|
|
1550
|
+
source: string | null;
|
|
1551
|
+
count: number;
|
|
1552
|
+
}[];
|
|
1553
|
+
repos: number;
|
|
1554
|
+
cwds: number;
|
|
1555
|
+
/** Source directories scanned, with each one's last-analyzed time (empty on pre-v9 stores). */
|
|
1556
|
+
roots: {
|
|
1557
|
+
source: string | null;
|
|
1558
|
+
path: string;
|
|
1559
|
+
lastAnalyzedAt: string | null;
|
|
1560
|
+
}[];
|
|
1561
|
+
}
|
|
1562
|
+
interface SchemaDump {
|
|
1563
|
+
schemaVersion: number | null;
|
|
1564
|
+
/** Store extent; null when reflecting the canonical (empty) schema. */
|
|
1565
|
+
coverage: Coverage | null;
|
|
1566
|
+
tables: SchemaTable[];
|
|
1567
|
+
facets: FacetSpec[];
|
|
1568
|
+
measures: MeasureSpec[];
|
|
1569
|
+
}
|
|
1570
|
+
/** Open the store read-only and dump its schema (see {@link schemaFromDb}). */
|
|
1571
|
+
declare function describeSchema(dbPath: string): SchemaDump;
|
|
1572
|
+
/** Build a fresh in-memory store purely to reflect the canonical schema (no data). */
|
|
1573
|
+
declare function canonicalSchema(): SchemaDump;
|
|
1574
|
+
|
|
1575
|
+
interface ModelPrice {
|
|
1576
|
+
input: number;
|
|
1577
|
+
output: number;
|
|
1578
|
+
cache_write_5m: number;
|
|
1579
|
+
cache_write_1h: number;
|
|
1580
|
+
cache_read: number;
|
|
1581
|
+
}
|
|
1582
|
+
/** Bump when models.json rates change so stored costs can be recomputed. */
|
|
1583
|
+
declare const PRICE_TABLE_VERSION = "2026-06-30";
|
|
1584
|
+
/**
|
|
1585
|
+
* Look up a price, tolerant of model-id drift: exact match, then strip a
|
|
1586
|
+
* trailing date snapshot (`-20251001` or `@20251001`), then prefix match.
|
|
1587
|
+
*/
|
|
1588
|
+
declare function priceFor(provider: string, model: string, opts?: {
|
|
1589
|
+
backfill?: boolean;
|
|
1590
|
+
}): ModelPrice | undefined;
|
|
1591
|
+
/** Cost of a single usage record at a given model's rates (0 if unpriced). */
|
|
1592
|
+
declare function costOfUsage(provider: string, model: string, u: TokenUsage): number;
|
|
1593
|
+
/**
|
|
1594
|
+
* Usage + cost for one assistant message — the atomic grain of token economics.
|
|
1595
|
+
* Persisted to `usage_facts` so model / main-vs-sidechain / time breakdowns are
|
|
1596
|
+
* all read-time GROUP BYs (cost can't be summed by model off the session row).
|
|
1597
|
+
*/
|
|
1598
|
+
interface UsageFact {
|
|
1599
|
+
idx: number;
|
|
1600
|
+
model: string;
|
|
1601
|
+
isSidechain: boolean;
|
|
1602
|
+
ts?: string;
|
|
1603
|
+
tokens: TokenUsage;
|
|
1604
|
+
usd: number;
|
|
1605
|
+
}
|
|
1606
|
+
interface CostResult {
|
|
1607
|
+
usd: number;
|
|
1608
|
+
/** Models we had no price for — their tokens count, but contribute $0. */
|
|
1609
|
+
unpriced: string[];
|
|
1610
|
+
/** One entry per assistant message, in order. Sums to `usd` / session tokens. */
|
|
1611
|
+
facts: UsageFact[];
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Cost of a session, priced per assistant message at that message's model, with
|
|
1615
|
+
* the per-message breakdown retained. Cache-creation tokens are priced at the
|
|
1616
|
+
* 5-minute rate (Claude Code's default).
|
|
1617
|
+
*/
|
|
1618
|
+
declare function computeSessionCost(session: Session): CostResult;
|
|
1619
|
+
|
|
1620
|
+
/** Build an LLM client from config, or null if enrichment isn't configured. */
|
|
1621
|
+
declare function createLlmClient(llm: TuneloopConfig['llm']): LlmClient | null;
|
|
1622
|
+
|
|
1623
|
+
export { type AnalyzeOptions, type AnnotationInput, type ArtifactInput, type ArtifactKind, type ArtifactLinkInput, type ArtifactRelation, type AssistantMessage, type BlockAnnotationInput, type BlockArtifactInput, type BlockInput, type BlockToolInput, type BlockUsageInput, type CanonicalAction, type ContentBlock, type Coverage, type Event, type FeatureRef, type FeatureRevisionInput, type FileIndexInput, type LinkSource, type LlmClient, type LlmResult, type OutcomeInput, PRICE_TABLE_VERSION, type PrBlockAttribution, type Processor, type ProcessorContext, type ProcessorKind, type ProcessorResult, type ProcessorRunRow, QueryError, type QueryOptions, type QueryResult, type RefreshContext, type RefreshResult, type RunOptions, type SchemaDump, type SchemaTable, type ServeOptions, type Session, type SessionArtifactInput, type SessionArtifactRole, type SessionRow, type ShResult, type SourceAdapter, Store, type StructuredRequest, type SubagentMeta, type Summary, type SystemEvent, type TokenUsage, type ToolCall, type TuneloopConfig, type UsageFactInput, type UserLinkedArtifact, type UserMessage, addUsage, analyze, canonicalSchema, computeSessionCost, costOfUsage, createDashboardServer, createLlmClient, describeSchema, emptyUsage, getAdapters, getProcessors, loadConfig, orderProcessors, priceFor, registerAdapter, registerProcessor, runProcessors, runQuery, serve };
|