@seanmars/tospec 0.14.0 → 0.14.2
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/CHANGELOG.md +38 -0
- package/README.md +9 -0
- package/assets/metrics/app.js +995 -0
- package/assets/metrics/chart.umd.js +14 -0
- package/assets/metrics/index.html +14 -0
- package/assets/metrics/style.css +193 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/dashboard.d.ts +0 -6
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +7 -120
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/metrics.d.ts +145 -0
- package/dist/commands/metrics.d.ts.map +1 -0
- package/dist/commands/metrics.js +380 -0
- package/dist/commands/metrics.js.map +1 -0
- package/dist/core/codex-metrics.d.ts +95 -0
- package/dist/core/codex-metrics.d.ts.map +1 -0
- package/dist/core/codex-metrics.js +293 -0
- package/dist/core/codex-metrics.js.map +1 -0
- package/dist/core/local-server.d.ts +67 -0
- package/dist/core/local-server.d.ts.map +1 -0
- package/dist/core/local-server.js +169 -0
- package/dist/core/local-server.js.map +1 -0
- package/dist/core/skill-metrics.d.ts +185 -0
- package/dist/core/skill-metrics.d.ts.map +1 -0
- package/dist/core/skill-metrics.js +321 -0
- package/dist/core/skill-metrics.js.map +1 -0
- package/package.json +2 -9
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill duration metrics, derived from Claude Code transcripts.
|
|
3
|
+
*
|
|
4
|
+
* There is no tospec-side event to measure. The CLI is stateless by design
|
|
5
|
+
* (status comes from file existence), so it never learns when a workflow
|
|
6
|
+
* starts or ends — and CLI-call telemetry would miss the workflows that
|
|
7
|
+
* barely touch the CLI at all: `grill` only ever runs `tospec list`, and
|
|
8
|
+
* `explore` only an optional `tospec decision new`.
|
|
9
|
+
*
|
|
10
|
+
* Claude Code already records what we need. Every transcript entry in
|
|
11
|
+
* `<config>/projects/<encoded-cwd>/<session>.jsonl` carries an
|
|
12
|
+
* `attributionSkill` field naming the skill that entry belongs to, so skill
|
|
13
|
+
* timing is a read-only analysis of data that already exists — retroactively,
|
|
14
|
+
* with nothing to install. The cost is that the field is recent: sessions
|
|
15
|
+
* predating it yield no runs, which is reported rather than hidden.
|
|
16
|
+
*
|
|
17
|
+
* Two durations are reported per run because they answer different questions
|
|
18
|
+
* and diverge sharply in conversational workflows:
|
|
19
|
+
*
|
|
20
|
+
* - `span` — wall clock, first to last entry. "How long did this take?"
|
|
21
|
+
* - `engaged` — span minus gaps over `idleGapMs`. "How much of that was work?"
|
|
22
|
+
*
|
|
23
|
+
* An interview loop like `grill` idles while the user thinks, so its span runs
|
|
24
|
+
* well above its engaged time; a continuous `apply` pass has the two nearly
|
|
25
|
+
* equal. Reporting only one of them would silently answer the other question.
|
|
26
|
+
*
|
|
27
|
+
* Pure parsing/aggregation is split from the directory-reading wrapper so
|
|
28
|
+
* vitest can exercise it without a transcript store on disk.
|
|
29
|
+
*/
|
|
30
|
+
/** Gap above which the user is considered away, excluded from `engaged`. */
|
|
31
|
+
export declare const DEFAULT_IDLE_GAP_MS: number;
|
|
32
|
+
/**
|
|
33
|
+
* Gap above which one skill's entries are treated as a second, separate run
|
|
34
|
+
* rather than one very long one. Without this, resuming `apply` the next
|
|
35
|
+
* morning reads as a single 14-hour run.
|
|
36
|
+
*/
|
|
37
|
+
export declare const DEFAULT_SPLIT_GAP_MS: number;
|
|
38
|
+
/** One timestamped transcript line, reduced to what timing needs. */
|
|
39
|
+
export interface TranscriptEntry {
|
|
40
|
+
timeMs: number;
|
|
41
|
+
/** Normalized skill name, or null for entries Claude Code did not attribute. */
|
|
42
|
+
skill: string | null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Which tool a run's timing was derived from. Claude Code's `attributionSkill`
|
|
46
|
+
* is read directly from recorded data; Codex's is inferred by a heuristic
|
|
47
|
+
* (see codex-metrics.ts) — the two are never combined into one figure because
|
|
48
|
+
* they carry different confidence.
|
|
49
|
+
*/
|
|
50
|
+
export type MetricsSourceKind = 'claude-code' | 'codex';
|
|
51
|
+
export interface SkillRun {
|
|
52
|
+
skill: string;
|
|
53
|
+
source: MetricsSourceKind;
|
|
54
|
+
/** Transcript file stem (the Claude Code session id, or the Codex rollout id). */
|
|
55
|
+
session: string;
|
|
56
|
+
startMs: number;
|
|
57
|
+
endMs: number;
|
|
58
|
+
spanMs: number;
|
|
59
|
+
engagedMs: number;
|
|
60
|
+
/** Entries inside the run window, including unattributed ones. */
|
|
61
|
+
entries: number;
|
|
62
|
+
}
|
|
63
|
+
export interface DurationStats {
|
|
64
|
+
medianMs: number;
|
|
65
|
+
maxMs: number;
|
|
66
|
+
totalMs: number;
|
|
67
|
+
}
|
|
68
|
+
export interface SkillAggregate {
|
|
69
|
+
skill: string;
|
|
70
|
+
source: MetricsSourceKind;
|
|
71
|
+
/** True when the skill is one of tospec's own generated workflows. */
|
|
72
|
+
workflow: boolean;
|
|
73
|
+
runs: number;
|
|
74
|
+
entries: number;
|
|
75
|
+
span: DurationStats;
|
|
76
|
+
engaged: DurationStats;
|
|
77
|
+
}
|
|
78
|
+
export interface MetricsThresholds {
|
|
79
|
+
idleGapMs: number;
|
|
80
|
+
splitGapMs: number;
|
|
81
|
+
}
|
|
82
|
+
export interface MetricsSource {
|
|
83
|
+
transcriptDir: string;
|
|
84
|
+
/** Null when the directory does not exist (no Claude Code history here). */
|
|
85
|
+
available: boolean;
|
|
86
|
+
sessions: number;
|
|
87
|
+
attributedSessions: number;
|
|
88
|
+
firstSeen: string | null;
|
|
89
|
+
lastSeen: string | null;
|
|
90
|
+
}
|
|
91
|
+
export interface SkillMetrics {
|
|
92
|
+
source: MetricsSource;
|
|
93
|
+
thresholds: MetricsThresholds;
|
|
94
|
+
skills: SkillAggregate[];
|
|
95
|
+
runs: SkillRun[];
|
|
96
|
+
}
|
|
97
|
+
export interface RunGroupingOptions {
|
|
98
|
+
idleGapMs?: number;
|
|
99
|
+
splitGapMs?: number;
|
|
100
|
+
/** Defaults to `'claude-code'` — every existing caller is that source. */
|
|
101
|
+
source?: MetricsSourceKind;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Claude Code's per-project transcript directory name: the absolute cwd with
|
|
105
|
+
* every non-alphanumeric character replaced by `-`. `D:\workspace\a.b` becomes
|
|
106
|
+
* `D--workspace-a-b` (the drive colon and the separator each contribute a dash,
|
|
107
|
+
* which is why the doubled dash is correct and not a bug).
|
|
108
|
+
*/
|
|
109
|
+
export declare function encodeProjectDirName(rootPath: string): string;
|
|
110
|
+
export interface TranscriptDirOptions {
|
|
111
|
+
env?: NodeJS.ProcessEnv;
|
|
112
|
+
homedir?: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Locates the transcript directory for a project root. `CLAUDE_CONFIG_DIR` is
|
|
116
|
+
* honoured because Claude Code itself does; without it the default is
|
|
117
|
+
* `~/.claude`. Injectable env/homedir follows global-config.ts so path
|
|
118
|
+
* resolution stays testable.
|
|
119
|
+
*/
|
|
120
|
+
export declare function resolveTranscriptDir(rootPath: string, options?: TranscriptDirOptions): string;
|
|
121
|
+
/**
|
|
122
|
+
* Collapses the two names one workflow is recorded under — the slash command
|
|
123
|
+
* (`tosx:apply`) and the skill (`tospec-apply`) — onto the workflow id. Without
|
|
124
|
+
* this every workflow's history is split across two unrelated-looking rows.
|
|
125
|
+
*/
|
|
126
|
+
export declare function normalizeSkillName(raw: string): string;
|
|
127
|
+
export declare function isWorkflowSkill(skill: string): boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Parses one `.jsonl` transcript into timestamped entries, sorted by time.
|
|
130
|
+
* Malformed lines are skipped rather than fatal: transcripts are an append-only
|
|
131
|
+
* log written by another process and can be torn at the tail while a session
|
|
132
|
+
* is live.
|
|
133
|
+
*/
|
|
134
|
+
export declare function parseTranscript(text: string): TranscriptEntry[];
|
|
135
|
+
/**
|
|
136
|
+
* Groups one session's entries into runs.
|
|
137
|
+
*
|
|
138
|
+
* Two rules carry the whole design:
|
|
139
|
+
*
|
|
140
|
+
* - An **unattributed** entry does not end a run. Tool results and the user's
|
|
141
|
+
* own messages carry no `attributionSkill`, so treating them as boundaries
|
|
142
|
+
* shatters a single 30-minute `apply` into a hundred two-entry fragments.
|
|
143
|
+
* They are folded into the open run instead, because they are its work.
|
|
144
|
+
* - A **different skill** does end a run. Grouping all of one skill's entries
|
|
145
|
+
* by min/max instead would make an A → B → A sequence count B's elapsed time
|
|
146
|
+
* inside A's engaged total. Splitting keeps run windows disjoint, at the cost
|
|
147
|
+
* of reporting a re-entered skill as several runs.
|
|
148
|
+
*
|
|
149
|
+
* A run is additionally split at any internal gap over `splitGapMs`.
|
|
150
|
+
*/
|
|
151
|
+
export declare function buildRuns(entries: TranscriptEntry[], session: string, options?: RunGroupingOptions): SkillRun[];
|
|
152
|
+
/**
|
|
153
|
+
* The composite key identifying one report row: a skill under one source.
|
|
154
|
+
* Shared by aggregation, the daily/weekly/monthly series, and the frontend
|
|
155
|
+
* (which reproduces this exact format in JS, since it cannot import a TS
|
|
156
|
+
* module) — one definition of "row" everywhere a skill and its source must
|
|
157
|
+
* be kept apart rather than merged.
|
|
158
|
+
*/
|
|
159
|
+
export declare function skillRowKey(skill: string, source: MetricsSourceKind): string;
|
|
160
|
+
/**
|
|
161
|
+
* Aggregates runs per skill-and-source pair, ordered by total engaged time
|
|
162
|
+
* (descending). Grouping by source too, not by skill alone, is what keeps a
|
|
163
|
+
* skill run under both tools from being summed into one figure — Claude
|
|
164
|
+
* Code's attribution and Codex's heuristic one carry different confidence and
|
|
165
|
+
* must stay distinguishable everywhere a skill is reported.
|
|
166
|
+
*/
|
|
167
|
+
export declare function aggregateRuns(runs: SkillRun[]): SkillAggregate[];
|
|
168
|
+
export interface CollectMetricsOptions extends RunGroupingOptions, TranscriptDirOptions {
|
|
169
|
+
/** Drop runs that ended before this instant. */
|
|
170
|
+
sinceMs?: number;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Reads every transcript for a project root and derives skill timings.
|
|
174
|
+
*
|
|
175
|
+
* Every skill found is reported, tospec's own workflows and unrelated ones
|
|
176
|
+
* alike — there is deliberately no filtering by skill name. `workflow` on each
|
|
177
|
+
* aggregate labels which is which, so a caller that cares can tell them apart
|
|
178
|
+
* without this function deciding for it.
|
|
179
|
+
*
|
|
180
|
+
* A missing transcript directory is a reportable state (`available: false`),
|
|
181
|
+
* not an error: the project may simply never have been opened in Claude Code,
|
|
182
|
+
* and the caller renders that far better than a stack trace.
|
|
183
|
+
*/
|
|
184
|
+
export declare function collectSkillMetrics(rootPath: string, options?: CollectMetricsOptions): Promise<SkillMetrics>;
|
|
185
|
+
//# sourceMappingURL=skill-metrics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-metrics.d.ts","sourceRoot":"","sources":["../../src/core/skill-metrics.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill duration metrics, derived from Claude Code transcripts.
|
|
3
|
+
*
|
|
4
|
+
* There is no tospec-side event to measure. The CLI is stateless by design
|
|
5
|
+
* (status comes from file existence), so it never learns when a workflow
|
|
6
|
+
* starts or ends — and CLI-call telemetry would miss the workflows that
|
|
7
|
+
* barely touch the CLI at all: `grill` only ever runs `tospec list`, and
|
|
8
|
+
* `explore` only an optional `tospec decision new`.
|
|
9
|
+
*
|
|
10
|
+
* Claude Code already records what we need. Every transcript entry in
|
|
11
|
+
* `<config>/projects/<encoded-cwd>/<session>.jsonl` carries an
|
|
12
|
+
* `attributionSkill` field naming the skill that entry belongs to, so skill
|
|
13
|
+
* timing is a read-only analysis of data that already exists — retroactively,
|
|
14
|
+
* with nothing to install. The cost is that the field is recent: sessions
|
|
15
|
+
* predating it yield no runs, which is reported rather than hidden.
|
|
16
|
+
*
|
|
17
|
+
* Two durations are reported per run because they answer different questions
|
|
18
|
+
* and diverge sharply in conversational workflows:
|
|
19
|
+
*
|
|
20
|
+
* - `span` — wall clock, first to last entry. "How long did this take?"
|
|
21
|
+
* - `engaged` — span minus gaps over `idleGapMs`. "How much of that was work?"
|
|
22
|
+
*
|
|
23
|
+
* An interview loop like `grill` idles while the user thinks, so its span runs
|
|
24
|
+
* well above its engaged time; a continuous `apply` pass has the two nearly
|
|
25
|
+
* equal. Reporting only one of them would silently answer the other question.
|
|
26
|
+
*
|
|
27
|
+
* Pure parsing/aggregation is split from the directory-reading wrapper so
|
|
28
|
+
* vitest can exercise it without a transcript store on disk.
|
|
29
|
+
*/
|
|
30
|
+
import { promises as fs } from 'node:fs';
|
|
31
|
+
import * as os from 'node:os';
|
|
32
|
+
import * as path from 'node:path';
|
|
33
|
+
import { ALL_WORKFLOWS } from './shared/skill-generation.js';
|
|
34
|
+
/** Gap above which the user is considered away, excluded from `engaged`. */
|
|
35
|
+
export const DEFAULT_IDLE_GAP_MS = 5 * 60 * 1000;
|
|
36
|
+
/**
|
|
37
|
+
* Gap above which one skill's entries are treated as a second, separate run
|
|
38
|
+
* rather than one very long one. Without this, resuming `apply` the next
|
|
39
|
+
* morning reads as a single 14-hour run.
|
|
40
|
+
*/
|
|
41
|
+
export const DEFAULT_SPLIT_GAP_MS = 30 * 60 * 1000;
|
|
42
|
+
const CLAUDE_CONFIG_DIR_NAME = '.claude';
|
|
43
|
+
// -----------------------------------------------------------------------------
|
|
44
|
+
// Pure: paths
|
|
45
|
+
// -----------------------------------------------------------------------------
|
|
46
|
+
/**
|
|
47
|
+
* Claude Code's per-project transcript directory name: the absolute cwd with
|
|
48
|
+
* every non-alphanumeric character replaced by `-`. `D:\workspace\a.b` becomes
|
|
49
|
+
* `D--workspace-a-b` (the drive colon and the separator each contribute a dash,
|
|
50
|
+
* which is why the doubled dash is correct and not a bug).
|
|
51
|
+
*/
|
|
52
|
+
export function encodeProjectDirName(rootPath) {
|
|
53
|
+
return path.resolve(rootPath).replace(/[^a-zA-Z0-9]/g, '-');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Locates the transcript directory for a project root. `CLAUDE_CONFIG_DIR` is
|
|
57
|
+
* honoured because Claude Code itself does; without it the default is
|
|
58
|
+
* `~/.claude`. Injectable env/homedir follows global-config.ts so path
|
|
59
|
+
* resolution stays testable.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveTranscriptDir(rootPath, options = {}) {
|
|
62
|
+
const env = options.env ?? process.env;
|
|
63
|
+
const configDir = env.CLAUDE_CONFIG_DIR
|
|
64
|
+
? path.resolve(env.CLAUDE_CONFIG_DIR)
|
|
65
|
+
: path.join(options.homedir ?? os.homedir(), CLAUDE_CONFIG_DIR_NAME);
|
|
66
|
+
return path.join(configDir, 'projects', encodeProjectDirName(rootPath));
|
|
67
|
+
}
|
|
68
|
+
// -----------------------------------------------------------------------------
|
|
69
|
+
// Pure: parsing
|
|
70
|
+
// -----------------------------------------------------------------------------
|
|
71
|
+
const WORKFLOW_IDS = new Set(ALL_WORKFLOWS);
|
|
72
|
+
/**
|
|
73
|
+
* Collapses the two names one workflow is recorded under — the slash command
|
|
74
|
+
* (`tosx:apply`) and the skill (`tospec-apply`) — onto the workflow id. Without
|
|
75
|
+
* this every workflow's history is split across two unrelated-looking rows.
|
|
76
|
+
*/
|
|
77
|
+
export function normalizeSkillName(raw) {
|
|
78
|
+
return raw.replace(/^tosx:/, '').replace(/^tospec-/, '');
|
|
79
|
+
}
|
|
80
|
+
export function isWorkflowSkill(skill) {
|
|
81
|
+
return WORKFLOW_IDS.has(skill);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Parses one `.jsonl` transcript into timestamped entries, sorted by time.
|
|
85
|
+
* Malformed lines are skipped rather than fatal: transcripts are an append-only
|
|
86
|
+
* log written by another process and can be torn at the tail while a session
|
|
87
|
+
* is live.
|
|
88
|
+
*/
|
|
89
|
+
export function parseTranscript(text) {
|
|
90
|
+
const entries = [];
|
|
91
|
+
for (const line of text.split(/\r?\n/)) {
|
|
92
|
+
if (!line.trim())
|
|
93
|
+
continue;
|
|
94
|
+
let record;
|
|
95
|
+
try {
|
|
96
|
+
record = JSON.parse(line);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (typeof record.timestamp !== 'string')
|
|
102
|
+
continue;
|
|
103
|
+
const timeMs = Date.parse(record.timestamp);
|
|
104
|
+
if (Number.isNaN(timeMs))
|
|
105
|
+
continue;
|
|
106
|
+
entries.push({
|
|
107
|
+
timeMs,
|
|
108
|
+
skill: typeof record.attributionSkill === 'string' && record.attributionSkill
|
|
109
|
+
? normalizeSkillName(record.attributionSkill)
|
|
110
|
+
: null,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return entries.sort((a, b) => a.timeMs - b.timeMs);
|
|
114
|
+
}
|
|
115
|
+
// -----------------------------------------------------------------------------
|
|
116
|
+
// Pure: run grouping
|
|
117
|
+
// -----------------------------------------------------------------------------
|
|
118
|
+
/**
|
|
119
|
+
* Groups one session's entries into runs.
|
|
120
|
+
*
|
|
121
|
+
* Two rules carry the whole design:
|
|
122
|
+
*
|
|
123
|
+
* - An **unattributed** entry does not end a run. Tool results and the user's
|
|
124
|
+
* own messages carry no `attributionSkill`, so treating them as boundaries
|
|
125
|
+
* shatters a single 30-minute `apply` into a hundred two-entry fragments.
|
|
126
|
+
* They are folded into the open run instead, because they are its work.
|
|
127
|
+
* - A **different skill** does end a run. Grouping all of one skill's entries
|
|
128
|
+
* by min/max instead would make an A → B → A sequence count B's elapsed time
|
|
129
|
+
* inside A's engaged total. Splitting keeps run windows disjoint, at the cost
|
|
130
|
+
* of reporting a re-entered skill as several runs.
|
|
131
|
+
*
|
|
132
|
+
* A run is additionally split at any internal gap over `splitGapMs`.
|
|
133
|
+
*/
|
|
134
|
+
export function buildRuns(entries, session, options = {}) {
|
|
135
|
+
const idleGapMs = options.idleGapMs ?? DEFAULT_IDLE_GAP_MS;
|
|
136
|
+
const splitGapMs = options.splitGapMs ?? DEFAULT_SPLIT_GAP_MS;
|
|
137
|
+
const source = options.source ?? 'claude-code';
|
|
138
|
+
const runs = [];
|
|
139
|
+
let skill = null;
|
|
140
|
+
let window = [];
|
|
141
|
+
const flush = () => {
|
|
142
|
+
if (skill === null || window.length === 0) {
|
|
143
|
+
window = [];
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
let engagedMs = 0;
|
|
147
|
+
for (let i = 1; i < window.length; i++) {
|
|
148
|
+
const gap = window[i] - window[i - 1];
|
|
149
|
+
if (gap < idleGapMs)
|
|
150
|
+
engagedMs += gap;
|
|
151
|
+
}
|
|
152
|
+
const startMs = window[0];
|
|
153
|
+
const endMs = window[window.length - 1];
|
|
154
|
+
runs.push({
|
|
155
|
+
skill,
|
|
156
|
+
source,
|
|
157
|
+
session,
|
|
158
|
+
startMs,
|
|
159
|
+
endMs,
|
|
160
|
+
spanMs: endMs - startMs,
|
|
161
|
+
engagedMs,
|
|
162
|
+
entries: window.length,
|
|
163
|
+
});
|
|
164
|
+
window = [];
|
|
165
|
+
};
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
if (entry.skill !== null && entry.skill !== skill) {
|
|
168
|
+
flush();
|
|
169
|
+
skill = entry.skill;
|
|
170
|
+
}
|
|
171
|
+
else if (window.length > 0 &&
|
|
172
|
+
entry.timeMs - window[window.length - 1] > splitGapMs) {
|
|
173
|
+
// Long silence: the same skill resumed later is a second run, not one
|
|
174
|
+
// run spanning the intervening hours.
|
|
175
|
+
flush();
|
|
176
|
+
}
|
|
177
|
+
// Entries before any attributed one belong to no skill; drop them.
|
|
178
|
+
if (skill === null)
|
|
179
|
+
continue;
|
|
180
|
+
window.push(entry.timeMs);
|
|
181
|
+
}
|
|
182
|
+
flush();
|
|
183
|
+
return runs;
|
|
184
|
+
}
|
|
185
|
+
// -----------------------------------------------------------------------------
|
|
186
|
+
// Pure: aggregation
|
|
187
|
+
// -----------------------------------------------------------------------------
|
|
188
|
+
/** Lower median: for an even count, the lower of the two middle values. */
|
|
189
|
+
function median(values) {
|
|
190
|
+
if (values.length === 0)
|
|
191
|
+
return 0;
|
|
192
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
193
|
+
return sorted[Math.floor((sorted.length - 1) / 2)];
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The composite key identifying one report row: a skill under one source.
|
|
197
|
+
* Shared by aggregation, the daily/weekly/monthly series, and the frontend
|
|
198
|
+
* (which reproduces this exact format in JS, since it cannot import a TS
|
|
199
|
+
* module) — one definition of "row" everywhere a skill and its source must
|
|
200
|
+
* be kept apart rather than merged.
|
|
201
|
+
*/
|
|
202
|
+
export function skillRowKey(skill, source) {
|
|
203
|
+
return `${skill}::${source}`;
|
|
204
|
+
}
|
|
205
|
+
function describe(values) {
|
|
206
|
+
return {
|
|
207
|
+
medianMs: median(values),
|
|
208
|
+
maxMs: values.length === 0 ? 0 : Math.max(...values),
|
|
209
|
+
totalMs: values.reduce((sum, v) => sum + v, 0),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Aggregates runs per skill-and-source pair, ordered by total engaged time
|
|
214
|
+
* (descending). Grouping by source too, not by skill alone, is what keeps a
|
|
215
|
+
* skill run under both tools from being summed into one figure — Claude
|
|
216
|
+
* Code's attribution and Codex's heuristic one carry different confidence and
|
|
217
|
+
* must stay distinguishable everywhere a skill is reported.
|
|
218
|
+
*/
|
|
219
|
+
export function aggregateRuns(runs) {
|
|
220
|
+
const bySkillSource = new Map();
|
|
221
|
+
for (const run of runs) {
|
|
222
|
+
const key = skillRowKey(run.skill, run.source);
|
|
223
|
+
const bucket = bySkillSource.get(key);
|
|
224
|
+
if (bucket)
|
|
225
|
+
bucket.push(run);
|
|
226
|
+
else
|
|
227
|
+
bySkillSource.set(key, [run]);
|
|
228
|
+
}
|
|
229
|
+
const aggregates = [];
|
|
230
|
+
for (const skillRuns of bySkillSource.values()) {
|
|
231
|
+
const skill = skillRuns[0].skill;
|
|
232
|
+
aggregates.push({
|
|
233
|
+
skill,
|
|
234
|
+
source: skillRuns[0].source,
|
|
235
|
+
workflow: isWorkflowSkill(skill),
|
|
236
|
+
runs: skillRuns.length,
|
|
237
|
+
entries: skillRuns.reduce((sum, r) => sum + r.entries, 0),
|
|
238
|
+
span: describe(skillRuns.map((r) => r.spanMs)),
|
|
239
|
+
engaged: describe(skillRuns.map((r) => r.engagedMs)),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return aggregates.sort((a, b) => b.engaged.totalMs - a.engaged.totalMs);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Reads every transcript for a project root and derives skill timings.
|
|
246
|
+
*
|
|
247
|
+
* Every skill found is reported, tospec's own workflows and unrelated ones
|
|
248
|
+
* alike — there is deliberately no filtering by skill name. `workflow` on each
|
|
249
|
+
* aggregate labels which is which, so a caller that cares can tell them apart
|
|
250
|
+
* without this function deciding for it.
|
|
251
|
+
*
|
|
252
|
+
* A missing transcript directory is a reportable state (`available: false`),
|
|
253
|
+
* not an error: the project may simply never have been opened in Claude Code,
|
|
254
|
+
* and the caller renders that far better than a stack trace.
|
|
255
|
+
*/
|
|
256
|
+
export async function collectSkillMetrics(rootPath, options = {}) {
|
|
257
|
+
const transcriptDir = resolveTranscriptDir(rootPath, options);
|
|
258
|
+
const thresholds = {
|
|
259
|
+
idleGapMs: options.idleGapMs ?? DEFAULT_IDLE_GAP_MS,
|
|
260
|
+
splitGapMs: options.splitGapMs ?? DEFAULT_SPLIT_GAP_MS,
|
|
261
|
+
};
|
|
262
|
+
const empty = {
|
|
263
|
+
source: {
|
|
264
|
+
transcriptDir,
|
|
265
|
+
available: false,
|
|
266
|
+
sessions: 0,
|
|
267
|
+
attributedSessions: 0,
|
|
268
|
+
firstSeen: null,
|
|
269
|
+
lastSeen: null,
|
|
270
|
+
},
|
|
271
|
+
thresholds,
|
|
272
|
+
skills: [],
|
|
273
|
+
runs: [],
|
|
274
|
+
};
|
|
275
|
+
let fileNames;
|
|
276
|
+
try {
|
|
277
|
+
fileNames = (await fs.readdir(transcriptDir)).filter((f) => f.endsWith('.jsonl'));
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
if (error.code === 'ENOENT')
|
|
281
|
+
return empty;
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
const allRuns = [];
|
|
285
|
+
let attributedSessions = 0;
|
|
286
|
+
for (const fileName of fileNames) {
|
|
287
|
+
let text;
|
|
288
|
+
try {
|
|
289
|
+
text = await fs.readFile(path.join(transcriptDir, fileName), 'utf8');
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// A session file can vanish between readdir and read; skip it.
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const runs = buildRuns(parseTranscript(text), path.basename(fileName, '.jsonl'), thresholds);
|
|
296
|
+
if (runs.length > 0)
|
|
297
|
+
attributedSessions++;
|
|
298
|
+
for (const run of runs) {
|
|
299
|
+
if (options.sinceMs !== undefined && run.endMs < options.sinceMs)
|
|
300
|
+
continue;
|
|
301
|
+
allRuns.push(run);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
allRuns.sort((a, b) => a.startMs - b.startMs);
|
|
305
|
+
return {
|
|
306
|
+
source: {
|
|
307
|
+
transcriptDir,
|
|
308
|
+
available: true,
|
|
309
|
+
sessions: fileNames.length,
|
|
310
|
+
attributedSessions,
|
|
311
|
+
firstSeen: allRuns.length > 0 ? new Date(allRuns[0].startMs).toISOString() : null,
|
|
312
|
+
lastSeen: allRuns.length > 0
|
|
313
|
+
? new Date(Math.max(...allRuns.map((r) => r.endMs))).toISOString()
|
|
314
|
+
: null,
|
|
315
|
+
},
|
|
316
|
+
thresholds,
|
|
317
|
+
skills: aggregateRuns(allRuns),
|
|
318
|
+
runs: allRuns,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
//# sourceMappingURL=skill-metrics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-metrics.js","sourceRoot":"","sources":["../../src/core/skill-metrics.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seanmars/tospec",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"description": "Spec-driven development CLI for structured requirements and issue workflows",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"homepage": "https://
|
|
7
|
-
"bugs": {
|
|
8
|
-
"url": "https://github.com/seanmars/tospec/issues"
|
|
9
|
-
},
|
|
10
|
-
"repository": {
|
|
11
|
-
"type": "git",
|
|
12
|
-
"url": "git+https://github.com/seanmars/tospec.git"
|
|
13
|
-
},
|
|
6
|
+
"homepage": "https://www.npmjs.com/package/@seanmars/tospec",
|
|
14
7
|
"publishConfig": {
|
|
15
8
|
"access": "public"
|
|
16
9
|
},
|